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.

3 Answers 3

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
Sign up to request clarification or add additional context in comments.

1 Comment

The second is a bad idea, it'll break type inference.
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.

10 Comments

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?
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.
@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.
@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.
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.
|
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)

4 Comments

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.
Can you make parameters v1 and v2 type B as well?
Ah, therein lies the wisdom of providing MCV code examples. Working with your original code and description wasn't enough to provide a complete answer for your situation.
I think I fixed it by replacing one of the occurrences of an object o of type Derived by o.asInstanceOf[B].

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.