chessman
chessman

Reputation: 47

Setters for case class in Scala

I want to have a flexible generator for case class.

case class ABC(a: String, b: String, c: String)

I want to write in any order:

val genAbc = (new Gen).withB("b").withA("a").withC("c")

And be able to convert it to case class if all parameters are set:

genAbc.toAbc

If not all parameters are set, I want to get compile error:

(new Gen).withA("a").toAbc //<- error

Is it possible?

Upvotes: 1

Views: 3923

Answers (3)

iuriisusuk
iuriisusuk

Reputation: 434

Maybe you are looking for something simple like a case class copy method (you indeed cannot miss one of the params and you can specify them in random order):

case class ABC(a: String, b: String, c: String)
val abc = ABC("a", "b", "c")
println(abc)
// ABC(a,b,c)
val acc = abc.copy(b = "c")
println(acc)
// ABC(a,c,c)

Upvotes: 4

Yawar
Yawar

Reputation: 11607

There is a type-safe version of the builder pattern in Scala, see http://blog.rafaelferreira.net/2008/07/type-safe-builder-pattern-in-scala.html

But, if you're willing to give up the requirement to be able to set the parameters in any order, there is a much simpler way:

scala> val abcBuilder = (Abc.apply _).curried
abcBuilder: String => (String => (String => Abc)) = <function1>

That creates a curried function using the Abc class's smart constructor (Abc.apply). A curried function lets you pass in each argument separately. In the interim you keep getting back more functions which take one argument and get you one step closer to an Abc instance. So:

scala> abcBuilder("a")
res11: String => (String => Abc) = <function1>

scala> res11("b")
res12: String => Abc = <function1>

scala> res12("c")
res13: Abc = Abc(a,b,c)

This is all also type-safe (to the extent that you use distinct types for the parameters); so e.g. abcBuilder(1) would give you a compile error.

Upvotes: 4

triggerNZ
triggerNZ

Reputation: 4751

You could do something like this:

class Gen {
        def withA(a: String) = GenWithA(a)
        def withB(b: String) = GenWithB(b)
        def withC(c: String) = GenWithC(c)
}

case class GenWithA(a: String) {
        def withB(b: String) = GenWithAB(a, b)
        def withC(c: String) = GenWithAC(a, c)
}

case class GenWithB(b: String) {
        def withA(a: String) = GenWithAB(a, b)
        def withC(c: String) = GenWithBC(b, c)
}

case class GenWithC(c: String) {
        def withA(a: String) = GenWithAC(a, c)
        def withB(b: String) = GenWithBC(b, c)
}

case class GenWithAB(a: String, b: String) {
        def withC(c: String) = GenWithABC(a,b,c)
}

case class GenWithAC(a: String, c: String) {
        def withB(b: String) = GenWithABC(a,b,c)
}

case class GenWithBC(b: String, c: String) {
        def withA(a: String) = GenWithABC(a,b,c)
}

case class GenWithABC(a: String, b: String, c: String) {
        def toABC = ABC(a,b,c)
}

Two issues here:

First you get a combinatorial explosion once you start going greater than 3 parameters. You can generate it in SBT or with macros or shapeless magic possibly but all are a lot of work.

Second, this kind of code won't typecheck the way you want it to:

val g0 = new Gen

val g1 = if (condition) g0.withA("a") else g0.withB("b")

val g2 = if (condition) g1.withB("b") else g1.withA("a")

val g3 = g2.withC("c")

Like theoretically this always produces something valid, but the scala compiler is not smart enough to figure that out, so you can't use different parts of the builder in different conditional branches.

That doesn't leave you with much. If your goal is to simply write parameters in any order, the simpler thing is to just specifiy them as named parameters:

ABC(
  c = "C First",
  b = "Then B",
  a = "Finally A"
)

Occasionally builders with a type for each stage like that are useful, say, in building query DSLs but for building a data case class, it is almost always clunky overkill

Upvotes: 1

Related Questions