Enforcing type equality bound on implicit type parameter


#1

Hello,

I want to have the compiler select and use a checking function automatically. To do this I tried using implicits but have an issue. For some context assume we have a type that will be used to select the correct checking function:

 sealed trait AlgorithmType
  object AlgorithmType {
    sealed trait Unknown extends AlgorithmType
    sealed trait Classification extends AlgorithmType
    sealed trait BinaryClassification extends Classification
    sealed trait MultiClassification extends Classification
    sealed trait OneClass extends Classification
    sealed trait Regression extends AlgorithmType
  }

Now I define a type that expresses a checking function “tagged” with a phantiom type:

  type PhantomFunk[T <: AlgorithmType] = (Double) => Either[ADWError, Double]

where ADWError is the helper

case class ADWError(msg: String)

To test the “selection” I created the following test:

object Test1 {

  type PhantomFunk[T <: AlgorithmType] = (Double) => Either[ADWError, Double]

  implicit def check: PhantomFunk[MultiClassification] = (v : Double) => Right(v)
  implicit def check0: PhantomFunk[BinaryClassification] = (v : Double) => Right(v)
  implicit def check2: PhantomFunk[Regression] = (v : Double) => Right(v)

  class Do[A <: AlgorithmType]() {
    def exec0[T >: A <: A](v: Double)(implicit ev: PhantomFunk[T]): Either[ADWError, Double] = ev(v)
  }

  val t2: Do[BinaryClassification] = new Do[BinaryClassification]()
  val r4: Either[ADWError, Double] = t2.exec0(100.0)
  println(r4)
}

When I compile this I get the error>:

ambiguous implicit values:
[error]  both method check in object Test1 of type => Test1.PhantomFunk[AlgorithmType.MultiClassification]
[error]  and method check0 in object Test1 of type => Test1.PhantomFunk[AlgorithmType.BinaryClassification]
[error]  match expected type Test1.PhantomFunk[AlgorithmType.BinaryClassification]
[error]   val r4: Either[ADWError, Double] = t2.exec0(100.0)

So my question is: how do I impose that the type A should be AlgorithmType.BinaryClassification so that check0 is used? I have tried several variations including just using the classes A directly and the =:= implicit constraint. Is their any other way to make this simpler?

TIA


#2

PhantomFunk is just a type alias. So PhantomFunk[MultiClassification] and PhantomFunk[BinaryClassification] are exactly the same type because the type argument is not used in the right hand side of the type alias.


#3

Makes sense. But it is being used as a phantom type, so I assume it would still be used in type inference. I have also confirmed that if I simply use:

def exec[T <: A](v: Double)(implicit ev: PhantomFunk[T]): Either[ADWError, Double] = ev(v)

then it works. So I am still intrigued that making the restriction stronger does not yield the same solution.

Thanks.


#4

The fact that that seems to work is a lot stranger to me than the others not working.
Also when you take your working exec method and you pass its type parameter explicitly (i.e. t2.exec[BinaryClassification](100.0)) it suddenly stops working again…

The strangely working case also seems to be “fixed” in dotty in that it no longer works.


#5

As @Jasper-M said, the fact that your type is only an alias likely has something to do with the error. Beyond that, the fact that loosening the type restriction makes the error go away might be a type inference bug. In any case, implicit function values aren’t really encouraged style in Scala, nor do they make sense semantically if you think about it–what does it mean in your domain to have implicit values of type Double => Either[ADWError, Double]? Could someone come to your codebase later and incidentally start using that type with some other meaning? Type aliasing doesn’t allow the compiler to enforce your domain. The idiomatic way is to use a typeclass:

/** Typeclass. */
class PhantomFunk[A <: AlgorithmType](
  private val f: Double => Either[ADWError, Double])
  extends AnyVal {
  def apply(double: Double): Either[ADWError, Double] = f(double)
}

/** Instances. */
object PhantomFunk {
  implicit val multiClassification: PhantomFunk[MultiClassification] =
    new PhantomFunk(Right.apply)

  implicit val binaryClassification: PhantomFunk[BinaryClassification] =
    new PhantomFunk(Right.apply)

  implicit val regression: PhantomFunk[Regression] =
    new PhantomFunk(Right.apply)
}


class Do[A <: AlgorithmType](implicit ev: PhantomFunk[A]) {
  def exec0(v: Double): Either[ADWError, Double] = ev(v)
}

def run() = {
  val t2: Do[BinaryClassification] = new Do[BinaryClassification]
  val r4: Either[ADWError, Double] = t2.exec0(100.0)

  println(r4)
}

#6

These are some nice refinements over your previous thread; you’re definitely moving in the right direction.

My guess is that you’re trying to avoid the case class layer I suggested in your previous thread. This is pretty close, but as @Jasper-M says, concrete type members like PhantomFunk don’t hold onto phantom parameters that way. More formally, given any types T, U <: AlgorithmType, PhantomFunk[T] = PhantomFunk[U], because

1. PhantomFunk[T] = Double => Either[ADWError, Double] // type alias
2. PhantomFunk[U] = Double => Either[ADWError, Double] // type alias
3. Double => Either[ADWError, Double] = PhantomFunk[U] // equivalence relation symmetry
4. PhantomFunk[T] = PhantomFunk[U] // 2+3 transitivity (from equivalence relation)

And you can get the compiler to prove this, too, implicitly[PhantomFunk[T] =:= PhantomFunk[U]], and it compiles.

So if you want a phantom parameter on a type alias, you have to hide the fact that they’re equal from the compiler. You can use @julianmichael’s approach to newtypes for this.

sealed abstract class PhantomFunkModule {
  type PhantomFunk[T <: AlgorithmType]
  // add more member declarations here for conversion
}

object PhantomFunkModule {
  val Module: PhantomFunkModule = new PhantomFunkModule {
    type PhantomFunk[T <: AlgorithmType] = Double => Either[ADWError, Double]
    // add the member definitions here; they should be trivial
  }
}

Now the compiler knows about the type alias equality only inside the new PhantomFunkModule body; you can prove this with implicitly as above: the implicitly will compile inside that body, but fail outside it. So that way the phantom is preserved, because the compiler can’t just throw it away anymore; it has existential type, and is no longer a type alias.

You might be interested in developments to make this more convenient, like the pre-SIP or @alexknvl’s newts.

If you don’t want to do this, @yawaramin’s example is perfectly fine too; you can even add case to class without causing trouble.


#7

Yep. I can confirm this. On 2.12.2 this breaks. Back to the drawing board. :disappointed:


#8

You are correct in regards to the (lack of) interpretation of the implicit function. I should “name” it to something like validateDependentVariable. Thanks for the “solution”. This is basically the same technique suggested by @S11001001 in a previous thread. Frankly I did not realize I was basically attempting to do the same thing here. BTW appreciate you pointing out the use of AnyVal.


#9

Once again a very complete answer. Appreciate it. I would like to say I did not realize I was doing something similar in this and the thread you reference. The context is different: in this case I want to “select” the “validation function” based on the phantom type - for a given AlgorithmType make sure the dependent variable is either an integer or a real (even though the value is always expressed as a Double). In the previous case I wanted to check if a value of a given type was set to its default value. In that case I “match” by the values type.

In regards to the solution I will stick to use case shown by you and @yawaramin.

As for the suggestion you make above I am going through your article and @julianmichael’s post.

Thanks once again to everyone who answered.