Have been stuck on this small problem for a few days now. I don’t even know what to title this at this point. Would appreciate help!
Problem: I am building a DSL where this is the notation (and must be the notation)
val foo = Foo(0).modify(
f = x + 100, // sets foo.f = f
g = "hello! " + y // sets foo.g = g
)
// ... later on
foo.modify(
g = "awesome " + y,
h = 10 * x
) // original f unchanged
// again later on
foo.modify(
f = 3 * x
) // most recent g and h remained unchanged
Foo
internally stores context lambdas f
,g
,h
, and dozens more, and modify
is simply a setter. The parameters for modify
need to be optional, as the user may only be needing to re-assign one or a few rather than all of them each time.
Since scala does not have optional parameters, I at first tried using union with Null
case class Foo(id: Int):
var f: (x: Int) ?=> Int = x + 3,
var g: (y: String) ?=> String = "ok " + y
var h: (x: Double) ?=> Double = y * 10
def modify(
f: Null | (x: Int) ?=> Int = null,
g: Null | (y: String) ?=> String = null,
h: Null | (x: Double) ?=> Double = null
) =
if f != null then this.f = f
if g != null then this.g = g
if h != null then this.h = h
this
But found that unions of all sorts completely break context functions. Thread here Type a parameter as either a contextual function | Null
Unlucky.
Next I attempt to have the default value of unspecified arguments simply be the original member
def modify(
f: (x: Int) ?=> Int = f, // if no new f, use original f
g: (y: String) ?=> String = g, // if no new g, use original g
h: (x: Double) ?=> Double = h // if no new h, use original h
) =
this.f = f
this.g = g
this.h = h
// override f and g defaults
val foo = Foo(0).modify(
f = x + 100,
g = "awesome " + y
) // since h is not specified it's default argument is just the same original h.
// later, override h without resetting previous f,g functions
foo.props(
h = x * 2
) // defaults this.f = f; // this.g = g
Unfortunately, this is being interpreted as a redefinition of each function pointing to themselves, creating infinite recursion and stack overflow on invocation.
given Int = 1
println(foo.f) // Stack overflow, infinite recursion
Third, I attempted to do a ==
check between the original and new lambdas to know if a new one has been given first before attempting to reassign.
def modify(
f: (x: Int) ?=> Int = f, // if no new f, use original f
g: (y: String) ?=> String = g, // if no new g, use original g
h: (x: Double) ?=> Double = h // if no new h, use original h
) =
if f != this.f then this.f = f
if g != this.g then this.g = g
if h != this.h then this.h = h
this
Unfortunately the compiler interprets this as me immediately attempting to invoke f
,g
,h
and compains about no givens found in the scope. 2 threads here:
I did find a hacky workaround, which involves pushing the lambdas into a Seq[(x: Int) ?=> Int]
and then indexing seq(0)
to retrieve it casted as Any
so the compiler doesn’t attempt to invoke it.
val seq = Seq[(x: Int) ?=> Int]
seq = f +: seq
seq.asInstanceOf[Seq[Any]](0) == seq.asInstanceOf[Seq[Any]](0)
// true! f is f!
but immediately found that this ==
check won’t work anyway as the parameter f
and the variable f
are unique values on the runtime.
val seq = Seq[(x: Int) ?=> Int]
seq = f +: seq
seq = this.f +: seq
if seq.asInstanceOf[Seq[Any]](0) != seq.asInstanceOf[Seq[Any]](1)
then // unfortunately this does return true.
this.f = f // thus causing infinite recursion here.
// f and this.f are not the same lambda :(
I’m at a lost for what to do next.
@Sporaum suggested to use Option
instead of Null
val foo = Foo(0).modify(
f = Some(x + 5),
g = Some("awesome" + y)
)
but this obviously destroys the ergonomics of the library. (you will type Some()
literally hundreds of times in large projects. A goal of this library is to prove that DSL’s don’t have to look and feel terrible / boilerplatey. This defeats that goal.)
I would be fine with the Option
approach if I could create an implicit conversion to avoid having to type Some()
on the outside.
Unluckily again, implicit conversions into Some
seem not to work for context functions uniquely.
def modify(
f: Option[(x: Int) ?=> Int] = None,
...
) = ...
given Conversion[(x: Int) ?=> Int, Option[(x: Int) ?=> Int]] with
def apply(f: (x: Int) ?=> Int): Option[(x: Int) ?=> Int] = Some(f)
val foo = Foo().modify(
f = x + 5 // Not found: x
)
Does anyone have any clever ideas? It is cruciual that the DSL notation shown does not need to change. Getting this exact syntax to work is the entire motivation, please do not recommend an alternative style. I have abstracted away the details of the use case for brevity and don’t want to justify the choice of the syntax, just trust me that this foo.modify(...)
notation results in extremely clean domain modeling and anything less will be unsatisfying.
Thanks!