Here's an example that makes me scratch my head:
// A trait that transforms A -> B
trait Transformer[A, B] {
def transform(a: A): B
}
// A trait that creates a Transformer which would encode `From` to `To`,
// involving an intermediate type defined as `Intermediate`.
// Basically it represents a chain of From (-> Intermediate) -> To conversion.
trait DoubleEncoder[From, To] {
type Intermediate
def transformer(using Transformer[From, Intermediate], Transformer[Intermediate, To]): Transformer[From, To]
}
// Now we define a chain of Int -> X -> Y
class X(i: Int)
class Y(x: X)
given Transformer[Int, X] = X.apply _
given Transformer[X, Y] = Y.apply _
// And DoubleEncoder for that chain
given DoubleEncoder[Int, Y] = new DoubleEncoder[Int, Y] {
type Intermediate = X
override def transformer(using t1: Transformer[Int, X], t2: Transformer[X, Y]): Transformer[Int, Y] =
new Transformer { def transform(i: Int): Y = t2.transform(t1.transform(i)) }
}
// With DoubleEncoder defined we can create a transformer of P (-> Intermediate) -> Q out of defined DoubleEncoders
given [P, Q](using de: DoubleEncoder[P, Q])(using Transformer[P, de.Intermediate], Transformer[de.Intermediate, Q]): Transformer[P, Q] =
de.transformer
summon[Transformer[Int, Y]].transform(1) // But then boom.
(Scastie)
The last line fails with quite an obscure message:
Weirdly, if I change one line it starts working as intended:
given DoubleEncoder[Int, Y] = new DoubleEncoder[Int, Y] {
// ...
to
given DoubleEncoder[Int, Y] with {
// ...
What gives? Why didn't it work in the first version? And what difference is there between the first and the second version?
EDIT: Also, even if the type signature of the second version preserves the dependent type information, wouldn't it have been stripped out of the last given
statement? It summoned them with de: DoubleEncoder[P, Q]
not de: DoubleEncoder[Int, Y] { type Intermediate = X }
. Why did it still make a difference?
If you refine the type of the given to DoubleEncoder[Int, Y] { type Intermediate = X }
, like this:
given DoubleEncoder[Int, Y] { type Intermediate = X } = new DoubleEncoder[Int, Y] {
type Intermediate = X
override def transformer(using t1: Transformer[Int, X], t2: Transformer[X, Y]): Transformer[Int, Y] =
new Transformer { def transform(i: Int): Y = t2.transform(t1.transform(i)) }
}
then "works on my machine", at least with 3.3.1:
trait Transformer[A, B] {
def transform(a: A): B
}
// A trait that creates a Transformer which would encode `From` to `To`,
// involving an intermediate type defined as `Intermediate`.
// Basically it represents a chain of From (-> Intermediate) -> To conversion.
trait DoubleEncoder[From, To] {
type Intermediate
def transformer(using Transformer[From, Intermediate], Transformer[Intermediate, To]): Transformer[From, To]
}
// Now we define a chain of Int -> X -> Y
class X(i: Int)
class Y(x: X)
given Transformer[Int, X] = (i: Int) => X(i)
given Transformer[X, Y] = (x: X) => Y(x)
// And DoubleEncoder for that chain
given DoubleEncoder[Int, Y] { type Intermediate = X } = new DoubleEncoder[Int, Y] {
type Intermediate = X
override def transformer(using t1: Transformer[Int, X], t2: Transformer[X, Y]): Transformer[Int, Y] =
new Transformer { def transform(i: Int): Y = t2.transform(t1.transform(i)) }
}
// With DoubleEncoder defined we can create a transformer of P (-> Intermediate) -> Q out of defined DoubleEncoders
given [P, Q](using de: DoubleEncoder[P, Q])(using Transformer[P, de.Intermediate], Transformer[de.Intermediate, Q]): Transformer[P, Q] =
de.transformer
@main def entry(): Unit =
summon[Transformer[Int, Y]].transform(1) // no boom, works fine
That being said, all this "double encoder" with path dependent types looks unnecessarily complex. Here is a more straightforward way of doing the same:
DoubleEncoder
using
-parameter listsde.Xyz
-typestrait Transformer[A, B] {
def transform(a: A): B
}
class X(i: Int)
class Y(x: X)
given Transformer[Int, X] = (i: Int) => X(i)
given Transformer[X, Y] = (x: X) => Y(x)
given [P, I, Q](using pi: Transformer[P, I], iq: Transformer[I, Q]): Transformer[P, Q] with
def transform(p: P): Q = iq.transform(pi.transform(p))
@main def entry(): Unit =
summon[Transformer[Int, Y]].transform(1) // works fine
println("ship it")
Also note that at least in this simplified example, your Transformer
has collapsed into a simple Conversion
:
class X(i: Int)
class Y(x: X)
given Conversion[Int, X] = (i: Int) => X(i)
given Conversion[X, Y] = (x: X) => Y(x)
given chainConversions[X, Y, Z](using xy: Conversion[X, Y], yz: Conversion[Y, Z]): Conversion[X, Z] with
def apply(x: X): Z = yz(xy(x))
@main def entry(): Unit =
summon[Conversion[Int, Y]](1) // also works fine
println("ship it")