Search code examples
scalatype-parametertype-constraintspath-dependent-type

Constraining an operation by matching a type parameter to an argument's path-dependent type


I would like to exploit Scala's type system to constrain operations in a system where there are versioned references to some values. This is all happening in some transactional context Ctx which has a version type V attached to it. Now there is a Factory to create reference variables. They get created with a creation version attached them (type parameter V1), corresponding to the version of the context in which the factory was called.

Now imagine that some code tries to access that reference in a later version, that is using a different Ctx. What I want to achieve is that it is prohibited to call access on that Ref in any version (Ctx's V type field) that doesn't match the creation version, but that you are allowed to resolve the reference by some substitution mechanism that returns a new view of the Ref which can be accessed in the current version. (it's ok if substitute is called with an invalid context, e.g. one that is older than the Ref's V1 -- in that case a runtime exception could be thrown)

Here is my attempt:

trait Version

trait Ctx {
  type V <: Version
}

object Ref {
  implicit def access[C <: Ctx, R, T](r: R)(implicit c: C, view: R => Ref[C#V, T]): T =
    view(r).access(c)

  implicit def substitute[C <: Ctx, T](r: Ref[_ <: Version, T])
                                      (implicit c: C): Ref[C#V, T] = r.substitute(c)
}
trait Ref[V1 <: Version, T] {
  def access(implicit c: { type V = V1 }): T // ???
  def substitute[C <: Ctx](implicit c: C): Ref[C#V, T]
}

trait Factory {
  def makeRef[C <: Ctx, T](init: T)(implicit c: C): Ref[C#V, T]
}

And the problem is to define class method access in a way that the whole thing compiles, i.e. the compound object's access should compile, but at the same time that I cannot call this class method access with any Ctx, only with one whose version matches the reference's version.

Preferably without structural typing or anything that imposes performance issues.


Solution

  • FYI, and to close the question, here is another idea that I like because the client code is fairly clutter free:

    trait System[A <: Access[_]] {
      def in[T](v: Version)(fun: A => T): T
    }
    
    trait Access[Repr] {
      def version: Version
      def meld[R[_]](v: Version)(fun: Repr => Ref[_, R]): R[this.type]
    }
    
    trait Version
    
    trait Ref[A, Repr[_]] {
      def sub[B](b: B): Repr[B]
    }
    
    object MyRef {
      def apply[A <: MyAccess](implicit a: A): MyRef[A] = new Impl[A](a)
    
      private class Impl[A](a: A) extends MyRef[A] {
        def sub[B](b: B) = new Impl[B](b)
        def schnuppi(implicit ev: A <:< MyAccess) = a.gagaism
      }
    }
    trait MyRef[A] extends Ref[A, MyRef] {
      // this is how we get MyAccess specific functionality
      // in here without getting trapped in more type parameters
      // in all the traits
      def schnuppi(implicit ev: A <:< MyAccess): Int
    }
    
    trait MyAccess extends Access[MyAccess] {
      var head: MyRef[this.type]
      var tail: MyRef[this.type]
      def gagaism: Int
    }
    
    def test(sys: System[MyAccess], v0: Version, v1: Version): Unit = {
      val v2 = sys.in(v0) { a => a.tail = a.meld(v1)(_.head); a.version }
      val a3 = sys.in(v2) { a => a }
      val (v4, a4) = sys.in(v1) { a =>
        a.head = a.head
        println(a.head.schnuppi) // yes!
        (a.version, a)
      }
      // a3.head = a4.head // forbidden
    }