Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scala Puzzle: enforcing that two function arguments are of the same type AND both are a subtype of a given class

How can I enforce that trickyMethod's arguments are the same at compile time but at the same time also have the common super type Fruit ?

So in other words, tricky.trickyMethod(new Banana,new Apple) should not compile.

I am sure there must be a simple solution but I just spent 1 hour searching for the answer and still have no idea :(

I tried implicit evidence with <:< but I could not get it to work.

class Fruit
class Apple extends Fruit
class Banana extends Fruit

class TrickyClass[T<:Fruit]{
  def trickyMethod(p1:T,p2:T)= println("I am tricky to solve!")
}

object TypeInferenceQuestion extends App{
  val tricky=new TrickyClass[Fruit]()
  tricky.trickyMethod(new Apple,new Apple) //this should be OK
  tricky.trickyMethod(new Banana,new Banana) //this should be OK
  tricky.trickyMethod(new Banana,new Apple) //this should NOT compile

}

EDIT :

Thank you for the answers !

Follow up (more general) question:

This second example is a more general case of the first example.

class Fruit

class Apple extends Fruit
class Banana extends Fruit

class TrickyClass[T]{
  def trickyMethod[S<:T](p1:S,p2:S)= println("I am tricky to solve!")
}

object TypeInferenceQuestion extends App{
  val tricky=new TrickyClass[Fruit]()
  tricky.trickyMethod(new Apple,new Apple) //this should be OK
  tricky.trickyMethod(new Banana,new Banana) //this should be OK
  tricky.trickyMethod(new Banana,new Apple) //this should NOT compile

}
like image 773
jhegedus Avatar asked Mar 13 '14 15:03

jhegedus


4 Answers

You could do :

class Tricky[T] {
  def trickyMethod[S1<:T,S2<:T](s1:S1,s2:S2)(implicit ev: S1=:=S2) = println()
}


scala> val t = new Tricky[Seq[Int]]
t: Tricky[Seq[Int]] = Tricky@2e585191

scala> t.trickyMethod(List(1),List(1))
//OK

scala> t.trickyMethod(List(1),Seq(1))
<console>:10: error: Cannot prove that List[Int] =:= Seq[Int].
              t.trickyMethod(List(1),Seq(1))
like image 60
Marth Avatar answered Nov 13 '22 02:11

Marth


You can do this with implicits like so. Keep in mind that you will always be able to

tricky.trickyMethod((new Banana): Fruit, (new Apple): Fruit)

since disallowing that would break the subtyping relationship. (If really necessary, you could use Miles Sabin's encoding of "not this type" (see comment #38) to reject Fruit.)

Anyway, one way to achieve what you want is as follows:

class SamePair[T,U] {}
object SamePair extends SamePair[Nothing,Nothing] {
  def as[A] = this.asInstanceOf[SamePair[A,A]]
}
implicit def weAreTheSame[A] = SamePair.as[A]

Now we have an implicit that will give us a dummy object that verifies that two types are the same.

So now we change TrickyClass to

class TrickyClass[T <: Fruit] {
  def trickyMethod[A <: T, B <: T](p1: A, p2: B)(implicit same: SamePair[A,B]) = println("!")
}

And it does what you want:

scala>  val tricky=new TrickyClass[Fruit]()
tricky: TrickyClass[Fruit] = TrickyClass@1483ce25

scala>   tricky.trickyMethod(new Apple,new Apple) //this should be OK
!

scala>   tricky.trickyMethod(new Banana,new Banana) //this should be OK
!

scala>   tricky.trickyMethod(new Banana,new Apple) //this should NOT compile
<console>:16: error: could not find implicit value for parameter same: SamePair[Banana,Apple]
                tricky.trickyMethod(new Banana,new Apple) //this should NOT compile
like image 34
Rex Kerr Avatar answered Nov 13 '22 00:11

Rex Kerr


Try something like this:

abstract class Fruit[T <: Fruit[T]] {
  def trick(that: T)
}

class Apple extends Fruit[Apple] {
  override def trick(that: Apple) = { println("Apple") }
}

class Banana extends Fruit[Banana] {
  override def trick(that: Banana) = { println("Banana") }
}

class TrickyClass{
  def trickyMethod[T<:Fruit[T]](p1:T,p2:T)= p1.trick(p2)
}

object TypeInferenceQuestion {
  val tricky=new TrickyClass()
  tricky.trickyMethod(new Apple,new Apple) //this should be OK
  tricky.trickyMethod(new Banana,new Banana) //this should be OK
  tricky.trickyMethod(new Banana,new Apple) //this should NOT compile
}

it gives:

error: inferred type arguments [Fruit[_ >: Apple with Banana <: Fruit[_ >: Apple with Banana <: ScalaObject]]] do not conform to method trickyMethod's type parameter bounds [T <: Fruit[T]]
         tricky.trickyMethod(new Banana,new Apple) //this should NOT compile

On the last one.

like image 21
Peter Avatar answered Nov 13 '22 00:11

Peter


You could use implicits, although you will have to define one for each Fruit subclass:

class Fruit
class Apple extends Fruit
class Banana extends Fruit

trait FruitEvidence[T <: Fruit]

class TrickyClass{
  def trickyMethod[T <: Fruit](p1:T,p2:T)(implicit e: FruitEvidence[T]) = println("I am tricky to solve!")
}

object FruitImplicits {
    implicit val BananaEvidence = new FruitEvidence[Banana] { }
    implicit val AppleEvidence = new FruitEvidence[Apple] { }
}

object TypeInferenceQuestion extends App{
      import FruitImplicits._
      val tricky=new TrickyClass()
      tricky.trickyMethod(new Apple,new Apple) //this should be OK
      tricky.trickyMethod(new Banana,new Banana) //this should be OK
      tricky.trickyMethod(new Banana,new Apple) //this should NOT compile
}
like image 31
Lee Avatar answered Nov 13 '22 00:11

Lee