scalaimplicitdependent-typescala-3

Why are `given ... with { ... }` and `given ... = new { ... }` working differently?


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: error

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 {
 // ...

succeed

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?


Solution

  • 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:

    trait 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")