1

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.

taninamdar
  • 137
  • 1
  • 8

3 Answers3

3

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 Deriveds", and any function that knows how to work with Bases 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.

slouc
  • 9,508
  • 3
  • 16
  • 41
  • I see. That's inconvenient, because the function f1 that I want to pass while calling myMethod, is tied with the Derived class. If I have another class Derived2 that extends Base, I want to use some other f2 that depends on Derived2. But myMethod is supposed to be a catch-all method that can handle all sub-classes of Base, provided appropriate function is provided. How can one model such a situation? – taninamdar Apr 28 '17 at 20:07
  • Well, like I said, you can always say that `myMethod` works with values (and functions) of type `Base`, and then have an internal pattern matching which invokes appropriate logic based on the actual type that has been received (Derived1, Derived2, Derived99). But this entails changing the signature of f1 to be working with Base, not Derived. – slouc Apr 28 '17 at 20:09
  • @taninamdar In other words, you want `myMethod` to be generic. Something like `def myMethod[B <: Base](v1: B, v2: B)(f:(B, B) => Int ): Int` probably. – Jasper-M Apr 28 '17 at 20:11
  • @slouc I see. But what if I don't know all the ways by which Base can be extended? Example, Base = Shape, and f1 returns difference between areas of two Shapes, which is then used by myMethod. f1 would look different if the Shapes are circles, rectangles etc, but I want its definition to be left general enough, so that if someone defines a Triangle class and provides appropriate f1, then myMethod should still work in that case. – taninamdar Apr 28 '17 at 20:12
  • If you can change the signature of `myMethod`, this is easily achievable with type classes. I can update my answer if this is the case. – slouc Apr 28 '17 at 20:20
  • I can change it, provided that the change doesn't make it specific for one particular `Derived` class. – taninamdar Apr 28 '17 at 20:21
  • For the typeclass solution, I think it even works when you add a new class `Foo` that you add the implicit instance to the `Foo` companion object. So you don't have to touch any existing code. – Jasper-M Apr 28 '17 at 20:51
  • Yes, that's true. But if you have access to `Base`, it's nice to keep them in one place. But yes, you're right. – slouc Apr 28 '17 at 20:52
  • Thank you so much for this answer, it was really helpful. I'm accepting the other answer because it was more concise and local solution. – taninamdar Apr 28 '17 at 22:58
  • No problem, as long as you got what you needed. I would definitely use type classes in your case though. It's a known FP pattern borrowed from Haskell and a much more flexible solution (not to mention elegant) than using `asInstanceOf`. If you're OK with manual type casting then the whole question becomes trivial anyway. :) – slouc Apr 29 '17 at 07:58
3

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
Jasper-M
  • 14,966
  • 2
  • 26
  • 37
1

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)
jwvh
  • 50,871
  • 7
  • 38
  • 64
  • This fixes *that* problem, but it creates another problem. Inside `myMethod`, when I call `f1(v1, v2)` (`v1, v2` are `Derived`), it gives me the following error: `found: v1.type (With underlying type Base)`, `Required: B`. – taninamdar Apr 28 '17 at 20:20
  • Can you make parameters `v1` and `v2` type `B` as well? – jwvh Apr 28 '17 at 20:30
  • 1
    Ah, therein lies the wisdom of providing [MCV code](http://stackoverflow.com/help/mcve) examples. Working with your original code and description wasn't enough to provide a complete answer for your situation. – jwvh Apr 28 '17 at 20:38
  • I think I fixed it by replacing one of the occurrences of an object `o` of type `Derived` by `o.asInstanceOf[B]`. – taninamdar Apr 28 '17 at 22:05