Oli
Oli

Reputation: 1142

Scala: Polymorphism in argument type of higher order function

I have stumbled across something in Scala that really confuses me. It seems that argument types for higher order functions are not polymorphic. Below is a demonstration of what I mean:

class A

class B extends A

def call(f: A => A):A = f(new A)

def A2A(a: A): A = new A
def A2B(a: A): B = new B
def B2A(b: B): A = new A
def B2B(b: B): B = new B

call(A2A)  // Works
call(A2B)  // Works
call(B2A)  // Error!
call(B2B)  // Error!

The error message is the same for both calls:

<console>:12: error: type mismatch;
found   : B => A
required: A => A
          call(new B, B2A)

<console>:12: error: type mismatch;
found   : B => B
required: A => A
          call(new B, B2B)

A2A and A2B work, so it's not the return type that's causing the problem, it must be the argument type. Is this something to do with type erasure?

I don't think there's anything logically wrong with what I'm trying to do (maybe there is?) so I suspect it's a quirk with the type system?

EDIT

Thanks for the answers! It turns out this is my faulty logic after all. The problem is with the definition of call. It expects a function that takes an A (or subtype) as an argument.

B2A and B2B both take a B (or subtype) and are therefore not compatible with call's definition.

I solved this problem by introducing a trait that A extends from called ALike and changed the definition of call so that it takes any argument that has ALike as a supertype.

trait ALike
class A extends ALike
class B extends A

def call[T <: ALike](t: T, f: T => A): A = f(t)

def A2A(a: A): A = a
def A2B(a: A): B = new B
def B2A(b: B): A = new A
def B2B(b: B): B = b

call(new A, A2A)
call(new A, A2B)
call(new B, B2A)
call(new B, B2B)

Upvotes: 1

Views: 467

Answers (2)

Ren
Ren

Reputation: 3455

call(f) is expecting a function that can take in an A (or any subtype) and return an A (or any subtype). The reason 1 and 2 work is that your function definition can take A or any subtypes of A, and in 1, returns A (satisfying the condition of being A) or in 2, returns B (satisfying the condition of being a subtype of A).

The reason the latter two don't work is that you're passing in a function which can only take a B and return A or B. To see why this is a problem, assume you've got class C extends A. B2A won't be able to handle C, which means that call(f), by definition, won't be able to handle the input C, despite being a subtype of A. This is a compile error due to the definition of call.

Upvotes: 3

Peter Neyens
Peter Neyens

Reputation: 9820

The arguments of a Scala Function are contravariant. Which in the case of A => A means that you can create a function which accepts a super type of A but not a function which accepts a sub type like B.

You could define a function which takes Any:

def Any2A(any: Any) = new A 
def Any2B(any: Any) = new B

call(Any2A) // A = A@b5bddc9
call(Any2B) // A = B@62c1c65f

Function1[-T1, +R]

The argument of Function1 is contravariant and the result is covariant, which means that in a function of type A => A:

  • You can return a B when an A is the result type (covariant) : A2B
  • You can not accept a B when an A is the type of the parameter (B2A and B2B), but you can pass a super type of A like Any (Any2A and Any2B).

Some explanation about the contravariance (and covariance) of Function can be found in Programming in Scala, 1ed

Upvotes: 3

Related Questions