Search code examples
scalatypesimplicitphantom-typesunion-types

resolve implicit for "A with B" based on user defined implicits for "A" and "B"


In an effort to make domain object states explicit through phantom type parameters instead of separate domain types, I came up with a model such as this:

sealed trait State

sealed trait Verified extends State

sealed trait Contactable extends State

class Name[+S <: State](val name: String)

class User[S <: State] {
  def state(implicit n: Name[S]): String = n.name
}

implicit def verifiedName: Name[Verified] = new Name[Verified](name = "verified")
implicit def contactableName: Name[Contactable] = new Name[Contactable](name = "contactable")
implicit def nameAB[A <: State, B <: State](implicit s1: Name[A], s2: Name[B]): Name[A with B] = 
    new Name[A with B](name = s1.name concat " & " concat s2.name)

//client code
new User[Verified].state //Check!
new User[Contactable].state //Check!
new User[Contactable with Verified].state //Epic Fail!
//Error:(20, 38) diverging implicit expansion for type
// A$A516.this.Name[A$A516.this.Contactable with A$A516.this.Verified]
//starting with method nameAB in class A$A516
//new User[Contactable with Verified].state;}

What I want to achieve in general is automatic composition of behavior for commutative operations. In this case I need the state method of User with state Contactable with Verified to be "contactable & verified" or maybe even "verified & contactable". Scala's type system is all too much for me atm. So I can't figure out the problem. If solvable how can I do it? if not what's the reason?


Solution

  • Personally I think you are conflating notions of implicit resolution with phantom types. Deriving implicits for compound types from their simpler variants is a fairly well documented procedure, either you encode the record type as HList and derive as HList or you resort to implicit macros, which in effect does the same thing without the shapeless sugar.

    Phantom types are about statically(at compile time) "mutating" the type argument on a class to encode some state in compile time, and it's generally not mixed with implicits like you suggest. What I mean is this:

    trait State
    trait Contacted extends State
    trait Verifiable extends State
    
    
    class Builder[C <: State, V <: State] {
      // In here I statically alter the signature of `Builder`
      // telling the compiler a call to `addContact`
      // will alter the type arg to the value of `Contacted`
      def addContact: Builder[Contacted, V]
      def verify(input: Whatever)(
        implicit ev: C =:= Contacted
      ): Builder[Contacted, Verifiable] = ???
    }
    

    In the above scenario, the compiler would now prevent you from calling verifiable before you addContact. And any later user of this method, if say you were building a framework, would get a compile time error if they tried to do something deemed invalid.

    So the trick is not to try to unify the types per say, although you could, it's likely adding more downstream complexity than you think. With phantom types, you generally have "one per state".