5
votes

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:

  • make "bar: Option[Bar]" and "baz: Option[Baz]" case class parameters, and then I can use "copy" to make new versions of the Foo class with them set to something; but then I have to check them every single time they're accessed - inefficient, unsafe, not able to make a DecoratedFoo that just is guaranteed to have the right structure
  • make a second case class that is a copy-paste of all the structure in the first, but adding the two extra decorated parameters - but this means echoing the entire parameter list in the definition, and again when creating instances of it
  • Case class inheritance is apparently controversial, and in any case also appears to require me to repeat every single parameter anyway, in the subclass constructor?
  • Make a non-case superclass listing the common case class parameters. Then extend it in the case class. But this would seem to still require repeating every single parameter in the subclass constructor as well.
  • I see blogs with people talking about this problem and using reflection at runtime to populate the base attributes of their decorated copies - this avoids echo but now you have no type safety, specifying attribute names as strings, overhead, etc...

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?

3
This is a pain point for me as well. It seems to me that the general problem is declaring a core data model and then, in a DRY way, defining derived/augmented models that are transformations of the original one. So far, I've not found a general solution to the problem. - acjay

3 Answers

2
votes

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.

1
votes

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
)
1
votes

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))