简体   繁体   中英

Scala conflict of types when extending a class

I have defined an abstract base class like following:

abstract class Base() {
    val somevariables
}

And then, I extend this class like following:

case class Derived (a: SomeOtherClass, i: Int) extends Base {
//Do something with a
} 

Then, I have a method (independent of classes) that is as follows:

 def myMethod (v1: Base, v2: Base, f:(Base, Base) => Int ): Int

And I want to use the above method as myMethod(o1, o2, f1) , where

  1. o1, o2 are objects of type Derived
  2. f1 is as follows def f1(v1: Derived, v2: Derived): Int

Now, this gives me an error because myMethod expects the function f1 to be (Base, Base) => Int , and not (Derived, Derived) => Int . However, if I change the definition of f1 to (Base, Base) => Int , then it gives me an error because internally I want to use some variable from SomeOtherClass , an argument that Base does not have.

If you want to be able to use function f1 where function f2 is expected, f1 must either be of the same type (both input parameters and return value) or a subclass of f2 . Liskov Substitution Principle teaches us that for one function to be a subclass of another, it needs to require less (or same) and provide more (or same).

So if you have a method that as a parameter takes a function of type (Fruit, Fruit) => Fruit , here are types for some valid functions that you can pass to that method:

  • (Fruit, Fruit) => Fruit
  • (Fruit, Fruit) => Apple
  • (Any, Any) => Fruit
  • (Any, Any) => Apple

This relates to covariance/contravariance rule; for example, every one-parameter function in Scala is a trait with two type parameters, Function2[-S, +T] . You can see that it is contravariant in its parameter type and covariant in its return type - requires S or less ("less" because it's more general, so we lose information) and provides T or more ("more" because it's more specific, so we get more information).

This brings us to your problem. If you had things the other way around, trying to fit (Base, Base) => Int in the place where (Derived, Derived) => Int is expected, that would work. Method myMethod obviously expects to be feeding this function with values of type Derived , and a function that takes values of type Base will happily accept those; after all, Derived is a Base . Basically what myMethod is saying is: "I need a function that can handle Derived s", and any function that knows how to work with Base s can also take any of its subclasses, including Derived .

Other people have pointed out that you can set the type of function f 's parameters to a subtype of Base , but at some point you will probably want to use v1 and v2 with that function, and then you will need to revert to downcasting via pattern matching. If you're fine with that, that you can also just pattern match on the function directly, trying to figure out what's its true nature. Either way, pattern matching sucks in this case because you will need to fiddle around myMethod every time a new type is introduced.

Here is how you can solve it more elegantly with type classes:

trait Base[T] {
  def f(t1: T, t2: T): Int
}

case class Shape()
case class Derived()

object Base {

  implicit val BaseDerived = new Base[Derived] {
    def f(s1: Derived, s2: Derived): Int = ??? // some calculation
  }

  implicit val BaseShape = new Base[Shape] {
    def f(s1: Shape, s2: Shape): Int = ??? // some calculation
  }

  // implementations for other types
}

def myMethod[T: Base](v1: T, v2: T): Int = {
  // some logic
  // now let's use f(), without knowing what T is:
  implicitly[Base[T]].f 
  // some other stuff
}

myMethod(Shape(), Shape())

What happens here is that myMethod says: "I need two values of some type T and I need to have an implicit Base[T] available in scope (that's the [T: Base] part, which is a fancy way of saying that you need an implicit parameter of type Base[T] ; that way you would access it by its name, and this way you access it via implicitly ). Then I know I will have f() available which performs the needed logic". And since the logic can have different implementation based on the type, this is a case of ad-hoc polymorphism and type classes are a great way of dealing with that.

What's cool here is that when a new type is introduced that has its own implementation of f , you just need to put this implementation in the Base companion object as an implicit value, so that it's available to myMethod . Method myMethod itself remains unchanged.

You should use type parameters to make sure that the types in myMethod line up correctly.

def myMethod[B <: Base](v1: B, v2: B)(f: (B, B) => Int): Int

Or perhaps a bit more general:

def myMethod[B <: Base, A >: B](v1: B, v2: B)(f: (A, A) => Int): Int

According to my (very simple) tests, this change...

def myMethod[B <: Base](v1: Base, v2: Base, f:(B, B) => Int ): Int = ???

...will allow either of these methods...

def f1(a: Derived, b:Derived): Int = ???
def f2(a: Base, b:Base): Int = ???

...to be accepted as a passed parameter.

myMethod(Derived(x,1), Derived(x,2), f1)
myMethod(Derived(x,1), Derived(x,2), f2)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM