0__ 0__ - 3 months ago 22
Scala Question

If-Then-Else DSL - define implicits to distinguish two return types

I am trying to build a small DSL that allows some if-then-else branches with two types of composition, one generic (

If
) and one with added capabilities (
IfGE
). I was under the impression that with mixing in a low priority implicit trait I can make Scala select the more precise return type for the
Else
operation, but it fails. Here is the construction:

Edit: Here is a minimal case. Further below is a more thorough walk-through of the context. For answering the question, probably focusing on this minimal case is sufficient, while to understand what I'm doing the longer example might be better.




trait GE
trait If[A] {
def Else[B >: A, Out](cond: => B)(implicit result: Result[B, Out]): Out
}
trait IfGE extends If[GE] with GE

case class SinOsc() extends GE
case class WhiteNoise() extends GE

trait LowPri {
implicit def AnyRes[A]: Result[A, If[A]] = ??? // !
}
object Result extends LowPri {
implicit def GERes: Result[GE, IfGE] = ???
}
sealed trait Result[A, Out]

def IfExample: If[SinOsc]

val res1: GE = IfExample.Else(WhiteNoise())
val res2: GE = IfExample.Else[GE, IfGE](WhiteNoise())


Here, type inference and implicit resolution fails for
res1
, while explicitly putting the types in
res2
makes it work. I need to get
res1
working, i.e. without specifying the type parameters.




Longer example:

(1) A graph element with some implicits to use numbers as graph elements and apply binary operators:

object GE {
implicit def intIsGE (x: Int ): GE = ???
implicit def doubleIsGE(x: Double): GE = ???

implicit class GEOps(private val ge: GE) extends AnyVal {
def <= (that: GE): GE = ???
def > (that: GE): GE = ???
def & (that: GE): GE = ???
def poll(): Unit = ???
}
}
trait GE


(2) A branching structure:

object If {
def apply(cond: => GE): IfBuilder = ???
}

trait IfBuilder {
def Then [A](branch: => A): If[A]
}

trait If[A] {
def Else: ElseBuilder[A]
def ElseIf(cond: => GE): ElseIfBuilder[A]
}

trait IfGE extends If[GE] with GE

object ElseBuilder {
trait LowPri {
implicit def AnyRes[A]: Result[A, If[A]] = ???
}
object Result extends LowPri {
implicit def GERes: Result[GE, IfGE] = ???
}
sealed trait Result[A, Out]
}
trait ElseBuilder[A] {
def apply[B >: A, Out](b: => B)(implicit res: ElseBuilder.Result[B, Out]): Out
}

trait ElseIfBuilder[A] {
def Then [B >: A](branch: => B): If[B]
}


(3) Some example graph elements:

case class Out(signal: GE)
case class SinOsc(freq: GE) extends GE
case class Dust(freq: GE) extends GE
case class WhiteNoise() extends GE


(4) A test suite:

trait Tests {
def freq: GE

// ---- Unit/Any result ----

val res0 = If (freq > 600) Then {
Out(SinOsc(freq))
}

val res1 = If (freq > 400 & freq <= 600) Then {
Out(SinOsc(freq))
} Else {
freq.poll()
}

// ---- GE result ----

val res2: GE = If (freq > 100) Then {
SinOsc(freq)
} Else {
WhiteNoise()
}

val res3: GE = If (freq > 1000) Then {
SinOsc(freq)
} ElseIf (freq > 100) Then {
Dust(freq)
} Else {
WhiteNoise()
}

Out(res3)
}


But the last two tests (
res2
and
res3
) do not work. The return type is not
IfGE
apparently but only
If[GE]
. How can I fix this, so that the last two examples find
GERes
instead of
AnyRes
?

Answer

Concerning your shorter example:

Definition implicit def GERes: Result[GE, IfGE] corresponds to type GE exactly, but WhiteNoise() has type WhiteNoise <: GE, and that doesn't fit the implicit.


You can either change the definition of the implicit to work for child types of GE:

object Result extends LowPri {
  implicit def GERes[T <: GE]: Result[T, IfGE] = ???
}

Or define Result to be contravariant in its first type parameter. That will make the implicits work for subtypes as well:

sealed trait Result[-A, Out]
object Result extends LowPri {
  implicit def GERes: Result[GE, IfGE] = ???
}