Scala loses track of related types when concatenating (projecting) type members

90 views Asked by At

I am walking around a problem, and I found a new strange problem with type projections and abstract types. Say I have a system which spawns transactions, and there is a peer system to which I want to bridge. The following looks good to me:

trait Txn[S <: Sys[S]] {
  def peer: S#Peer#Tx
  def newID(): S#ID
  def newVar[A](id: S#ID, init: A): S#Var[A]
}
trait Sys[S <: Sys[S]] {
  type Tx <: Txn[S]
  type Peer <: Sys[Peer] // parallel system
  type Var[A]
  type ID
}

And I can use the direct system:

def directWorks[S <: Sys[S]](implicit tx: S#Tx): Unit = {
  val id = tx.newID()
  val v  = tx.newVar(id, 0)
}

But somehow the peer method of the transaction is flawed, as the following shows:

def indirectDoesnt[S <: Sys[S]](implicit tx: S#Tx): Unit = {
  val p   = tx.peer
  val id  = p.newID()
  val v   = p.newVar(id, 0) // what the **** - id is not compatible??
}

error: type mismatch;
 found   : id.type (with underlying type S#Peer#ID)
 required: _30129.Peer#ID where val _30129: S
          val v   = p.newVar(id, 0)
                             ^

I wanted to be clever and work around it:

def clever[S <: Sys[S]](implicit tx: S#Tx): Unit = {
  def directWorks[S <: Sys[S]](implicit tx: S#Tx): Unit = {
    val id = tx.newID()
    val v  = tx.newVar(id, 0)
  }
  directWorks(tx.peer)
}

...but that fails, too, giving more clues about what's wrong:

error: inferred type arguments [S#Peer] do not conform to method
       directWorks's type parameter bounds [S <: Sys[S]]
          directWorks(tx.peer)
          ^

It all suggests that either def peer: S#Peer#Tx introduces a problem, or (more likely?) that type Peer <: Sys[Peer] is problematic when not used as type parameter but type member.

1

There are 1 answers

1
0__ On

Now here is an almost acceptable solution. It is based on the idea to "fix" the representation types in Sys; in a concrete system S, we will always have S#S#Tx == S#Tx etc. My understanding of this solution is that it moves the responsibility for variance from the use site to the system itself.

Since this involves some ceremony, I'd be still very grateful about additional answers regarding removal or reduction of this ceremony; as well as explanations regarding why this works and omitting the call to fix does not work.

trait VarLike[ -Tx, A ] { def update( value: A )( implicit tx: Tx ) : Unit }

trait Sys[ S <: Sys[ S ]] {
   type Tx       <: Txn[ S ]
   type Var[ A ] <: VarLike[ S#Tx, A ]
   type ID
   type Peer     <: Sys[ Peer ]

   // 'pop' the representation type ?!
   def fix[ A ]( v: S#Peer#Var[ A ]) : Peer#Var[ A ]
   def peer( tx: S#Tx ) : Peer#Tx
}

trait Txn[ S <: Sys[ S ]] {
   def newID() : S#ID
   def newVar[ A ]( id: S#ID, init: A ) : S#Var[ A ]
   def system: S
}

Now two example systems. First:

class InMemTx( val system: InMem ) extends Txn[ InMem ] {
   def newID() {}
   def newVar[ A ]( id: InMem#ID, init: A ) : InMem#Var[ A ] =
      new VarLike[ InMemTx, A ] {
         def update( v: A )( implicit tx: InMemTx ) {}
      }
}
class InMem extends Sys[ InMem ] {
   type Tx       = InMemTx
   type Var[ A ] = VarLike[ Tx, A ]
   type ID       = Unit
   type Peer     = InMem  // reflect back to ourself

   def fix[ A ]( v: Var[ A ]) : Var[ A ] = v
   def peer( tx: Tx ) : Tx = tx
}

Second:

class DurableTx( val system: Durable, val peer: InMem#Tx ) extends Txn[ Durable ] {
   def newID() = 33
   def newVar[ A ]( id: Durable#ID, init: A ) : Durable#Var[ A ] =
      new VarLike[ DurableTx, A ] {
        def update( v: A )( implicit tx: DurableTx ) {}
      }
}
class Durable extends Sys[ Durable ] {
   type Tx       = DurableTx
   type Var[ A ] = VarLike[ Tx, A ]
   type ID       = Int
   type Peer     = InMem

   def fix[ A ]( v: InMem#Var[ A ]) : InMem#Var[ A ] = v
   def peer( tx: Tx ) : InMem#Tx = tx.peer
}

And verfication on the use site:

// let's make sure we can use the system as intended
trait TestTrait[ S <: Sys[ S ]] {
   def v : S#Peer#Var[ Int ]

   def test( implicit tx: S#Tx ) {
      val s          = tx.system
      implicit val p = s.peer( tx )
      val vf         = s.fix( v ) // not cool...
      vf()            = 1
   }
}

// see if we can actually create variables
class TestImpl[ S <: Sys[ S ]]( implicit tx: S#Tx ) extends TestTrait[ S ] {
   val v = {
      val s          = tx.system
      implicit val p = s.peer( tx )
      val id         = p.newID()
      p.newVar( id, 0 )
   }
}