1

I currently have something like this:

case class Bear(a: String, b: String) {
   val can: Can[T] = ?? 
   def drink[T](str: String) = can.open(str)
}

I need to modify this to be used for only 4 types A,B,C and D. For example given an instance of Bear we should only be able to call bearinstance.drink[A]("abc"), bearinstance.drink[B]("abc"), bearinstance.drink[C]("abc") and bearinstance.drink[D]("abc"). Any other type should not be allowed.

Now the question is how do I rewrite this method for specific types?

Another issue is with the can, assuming I manage to rewrite drink to be used with only types 'A', 'B', 'C' and 'D', I will have to create can for all the four types as member variables. How do I make generic method to dynamically select the can based on the type? One option is to implicitly declare can outside the class, but it requires class parameters to be declared.

Any leads will be appreciated.

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
Bugman
  • 191
  • 6

2 Answers2

2

The fact that you need to do this means you really should refactor your code.

But anyways...

Try using implicit parameters:

case class Bear(a: String, b: String) {
   val can: Can[T] = ???
   def drink[T](str: String)(implicit ev: CanDrink[T]) = can.open(str)
}

Then make a trait CanDrink with implicit instances:

trait CanDrink[T]
implicit object ACanDrink extends CanDrink[A]
implicit object BCanDrink extends CanDrink[B]
//And so on

And now you can call it like this:

bearinstance.drink[A]("abc") 
//Gets implicit object ACanDrink

bearinstance.drink[X]("abc") 
//Doesn't work because no implicit parameter given of type CanDrink[X]

In Dotty, you could try changing the definition of drink using union types, as suggested by Dmytro Mitin:

def drink(x: A | B | C | D)(str: String) = ???
def drink[T](str: String)(using T <:< (A | B | C | D)) = ???

If you need it to be determined dynamically, use ClassTag.

def drink[T](str: String)(implicit ev: ClassTag[T]) = ev match {
  case classOf[A] => ???
  ...
}
user
  • 7,435
  • 3
  • 14
  • 44
  • I did think of creating implicit objects but I need class parameters to initialize them. – Bugman May 04 '20 at 19:28
  • @Bugman I've edited my answer now to dynamically determine it – user May 04 '20 at 19:33
  • 2
    @user `A | B | C | D` is a type, you can't use it like `def drink[A | B | C | D](str: String) = ???` (at least in Dotty 0.25), you can use it like `def drink(x: A | B | C | D)(str: String) = ???` or `def drink[T](str: String)(using T <:< (A | B | C | D)) = ???` – Dmytro Mitin May 04 '20 at 19:42
  • I've modified my answer now – user May 04 '20 at 19:54
0

If you're actually overriding generic method you must implement it for all possible types of type parameters (otherwise you violate the contract of class):

trait BearLike {
  def drink[T](str: String)
}

case class Bear(a: String, b: String) extends BearLike {
  override def drink[T](str: String) = ??? // for all T
}

or

case class Bear(a: String, b: String) {
  def drinkABCD[T](str: String)(implicit can: Can[T]) = can.open(str) // only for A, ..., D
}

or

case class Bear(a: String, b: String) extends BearLike {
  override def drink[T](str: String): Unit = sys.error("Bear is not actually a BearLike")
  def drinkABCD[T](str: String)(implicit can: Can[T]) = can.open(str) // only for A, ..., D
}

provided there are Can[A], ..., Can[D]

trait Can[T] {
  def open(str: String)
}
object Can {
  implicit val a: Can[A] = ???
  implicit val b: Can[B] = ???
  implicit val c: Can[C] = ???
  implicit val d: Can[D] = ???
}

If you can modify the contract then you can add this restriction (that the method works only for A, ..., D) to the contract

trait BearLike {
  def drink[T](str: String)(implicit can: Can[T])
}

case class Bear(a: String, b: String) extends BearLike {
  override def drink[T](str: String)(implicit can: Can[T]) = can.open(str)
}

Sometimes it's not easy to combine FP (type classes) with OOP (inheritance). Normally if you start to work with type classes (Can) you should prefer type classes further on

trait BearLike[B] {
  def drink(str: String)
}

case class Bear[T](a: String, b: String) 
object Bear {
  implicit def bearIsBearLike[T](implicit can: Can[T]): BearLike[Bear[T]] = new BearLike[Bear[T]] {
    override def drink(str: String): Unit = can.open(str)
  }
}

How to define "type disjunction" (union types)?

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66