user2057354
user2057354

Reputation: 73

How to decorate an immutable object graph from scala case classes

I'm reading structured JSON, using Play Frameworks' JSON Reads to build up an object graph with case classes.

An example:

case class Foo (
                       id: Int,
                       bar_id: Int,
                       baz_id: Int,
                       x: Int,
                       y: String
                       )
{
  var bar: Bar = null
  var baz: Baz = null
}

After building the Foo, I must come back later and decorate it by setting bar and baz. Those are defined in other JSON files and only known when all parsing is complete. But this means Foo can't be immutable.

What is the "right" way in Scala to make an immutable object, and then a decorated version of it, without repeating every field of Foo multiple times, over and over?

I know several ways that feel wrong:

Surely Scala must have a way to let people compose more complicated immutable objects out of simpler ones without having to copy each and every part of them by hand?

Upvotes: 5

Views: 543

Answers (3)

Ben Reich
Ben Reich

Reputation: 16324

You could introduce a new trait for the processed types, a class that extends that trait, and an implicit conversion:

case class Foo(bar: Int)

trait HasBaz {
    val baz: Int
}

class FooWithBaz(val foo: Foo, val baz: Int) extends HasBaz

object FooWithBaz {
    implicit def innerFoo(fwb: FooWithBaz): Foo = fwb.foo

    implicit class RichFoo(val foo: Foo) extends AnyVal {
        def withBaz(baz: Int) = new FooWithBaz(foo, baz)
    }
}

So then you can do:

import FooWithBaz._
Foo(1).withBaz(5)

And, although withBaz returns a FooWithBaz, we can treat the return value like a Foo when necessary, because of the implicit conversion.

Upvotes: 2

Dimitri
Dimitri

Reputation: 1786

Combining Option and type parameters you can flag your case class, and track whether the processed fields are empty, statically:

import scala.language.higherKinds

object Acme {
  case class Foo[T[X] <: Option[X] forSome { type X }](a: Int,
                                                       b: String,
                                                       c: T[Boolean],
                                                       d: T[Double])

  // Necessary, Foo[None] won't compile
  type Unprocessed[_] = None.type
  // Just an alias
  type Processed[X] = Some[X]
}

Example use case:

import Acme._

val raw: Foo[Unprocessed] = Foo[Unprocessed](42, "b", None, None)

def process(unprocessed: Foo[Unprocessed]): Foo[Processed] =
  unprocessed.copy[Processed](c = Some(true), d = Some(42d))

val processed: Foo[Processed] = process(raw)

// No need to pattern match, use directly the x from the Some case class
println(processed.c.x)
println(processed.d.x)

I used this once in my current project. The main problem I encountered is when I want Foo to be covariant.


Alternatively, if you don't care about the bound on T:

case class Foo[+T[_]](a: Int, b: String, c: T[Boolean], d: T[Double])

then you can use Foo[Unprocessed] or Foo[Processed] when you need a Foo[Option].

scala> val foo: Foo[Option] = processed
foo: Acme.Foo[Option] = Foo(42,b,Some(true),Some(42.0))

Upvotes: 1

Ionuț G. Stan
Ionuț G. Stan

Reputation: 179139

One other strategy might be to create yet another case class:

case class Foo(
  id: Int,
  bar_id: Int,
  baz_id: Int,
  x: Int,
  y: String
)

case class ProcessedFoo(
  foo: Foo,
  bar: Bar,
  baz: Baz
)

Upvotes: 1

Related Questions