Reputation: 59
Having the below builder pattern in Scala. To simplify it, I'm using 3 instances of A
such that instance1
contains only field1
and has no connection to field2
or field3
. The problem is that everywhere in the code I have to use val s = A.instance1.field1.get; doSomething(s)
, where the get
call is not potentially safe. For example A.instance1.field2.get
would fail on None.get
. In order to guard it I have to match case against the option and deal with None cases:
object A {
val instance1 = new ABuilder().withField1("abc").build1
val instance2 = new ABuilder().withField1("abc").withField2("def").build2
val instance3 = new ABuilder().withField1("abc").withField3("def").build1
}
case class A(builder: ABuilder) {
val field1: Option[String] = builder.field1
val field2: Option[String] = builder.field2
val field3: Option[String] = builder.field3
}
class ABuilder {
var field1: Option[String] = None
var field2: Option[String] = None
var field3: Option[String] = None
def withField1(f: String): ABuilder = {
this.field1 = Some(f)
this
}
def withField2(f: String): ABuilder = {
this.field2 = Some(f)
this
}
def withField3(f: String): ABuilder = {
this.field3 = Some(f)
this
}
def build1: A = {
require(field1.isDefined, "field 1 must not be None")
A(this)
}
def build2: A = {
require(field1.isDefined, "field 1 must not be None")
require(field2.isDefined, "field 2 must not be None")
A(this)
}
}
Another solution would be to use parameterized types, also called phantom types. I found very few good tutorials on that subject, and could not find in any of them how to implement a type safe builder pattern in Scala with phantom types and actual data (or state) - all examples describe methods only.
How can I use phantom types in my example to avoid getting runtime None
exceptions and get only nice type-mismatch exceptions? I'm trying to parameterize all the classes and methods mentioned and use sealed traits but had no success so far.
Upvotes: 0
Views: 1031
Reputation: 3699
If you really want to use phantom types you could do
object PhantomExample {
sealed trait BaseA
class BaseAWith1 extends BaseA
final class BaseAWith12 extends BaseAWith1
object A {
val instance1 = new ABuilder().withField1("abc").build1
val instance2 = new ABuilder().withField1("abc").withField2("def").build2
}
case class A[AType <: BaseA](builder: ABuilder) {
def field1[T >: AType <: BaseAWith1] = builder.field1.get
def field2[T >: AType <: BaseAWith12] = builder.field2.get
}
class ABuilder {
var field1: Option[String] = None
var field2: Option[String] = None
def withField1(f: String): ABuilder = {
this.field1 = Some(f)
this
}
def withField2(f: String): ABuilder = {
this.field2 = Some(f)
this
}
def build1: A[BaseAWith1] = {
require(field1.isDefined, "field 1 must not be None")
A(this)
}
def build2: A[BaseAWith12] = {
require(field1.isDefined, "field 1 must not be None")
require(field2.isDefined, "field 2 must not be None")
A(this)
}
}
val x = A.instance1.field1 //> x : String = abc
val x2 = A.instance2.field1 //> x2 : String = abc
val x3 = A.instance2.field2 //> x3 : String = def
// This gives compilation error
//val x2 = A.instance1.field2
}
However, I don't recommend using this kind of code in production. I think it looks ugly, the compilation error seems cryptic, and IMHO is not the best solution. Think about it, if your instances are so different, maybe they are not even instances of the same concrete class?
trait BaseA {
def field1
}
class A1 extends BaseA { }
class A2 extends BaseA { ... def field2 = ... }
Upvotes: 1
Reputation: 156
I'm not sure if this is what you want, but i think you can take it as a base.
First the A class:
case class A(field1: String = "",
field2: String = "",
field3: String = "")
The case class has default values of empty strings. This allow us to create any A object with any field value assigned without care of None values.
For example:
val b2 = A("abc", "def")
> b2: A = A(abc,def,)
val b1 = A("abc")
> b1: A = A(abc,,)
val notValidB = A(field2 = "xyz")
> notValidB: A = A(,xyz,)
As you can see b2 and b1 are valid objects, and notValidB is not valid since your object requires field1.
You can create another function that uses pattern matching to validate your A objects and then proceed with determinate actions.
def determineAObj(obj: A): Unit = obj match {
case A(f1, f2, _) if !f1.isEmpty && !f2.isEmpty => println("Is build2")
case A(f1, _, _) if !f1.isEmpty => println("Is build1")
case _ => println("This object doesn't match (build1 | build2)")
}
And then run:
determineAObj(b1)
> "Is build1"
determineAObj(b2)
> "Is build2"
determineAObj(notValidB)
> "This object doesn't match (build1 | build2)"
Upvotes: 0