4
votes

I'm working on a DSL for a experimental library I'm building in Scala, and I've run into some vexing peculiarities of Scala's type inference as it pertains to lambda expression arguments that don't seem to be covered in the book Programming In Scala.

In my library, I have a trait, Effect[-T], that is used to represent temporary modifiers that can be applied to an object of type T. I have a object, myEffects, that has a method named += that accepts an argument of type Effect[PlayerCharacter]. Lastly, I have a generic method, when[T], that is used for constructing conditional effects by accepting a conditional expression and another effect as an argument. The signature is as follows:

def when[T](condition : T => Boolean) (effect : Effect[T]) : Effect[T]

When I call the "when" method with the above signature, passing it's result to the += method, it is unable to infer the type of the argument to the lambda expression.

myEffects += when(_.hpLow()) (applyModifierEffect) //<-- Compiler error

If I combine the arguments of "when" into a single parameter list, Scala is able to infer the type of the lambda expression just fine.

def when[T](condition : T => Boolean, effect : Effect[T]) : Effect[T]

/* Snip */

myEffects += when(_.hpLow(), applyModifierEffect) //This works fine!

It also works if I remove the second parameter entirely.

def when[T](condition : T => Boolean) : Effect[T]

/* Snip */

myEffects += when(_.hpLow()) //This works too!

However, for aesthetic reasons, I really want the the arguments to be passed to the "when" method as separate parameter lists.

My understanding from section 16.10 of Programming in Scala is that the compiler first looks at whether the method type is known, and if so it uses that to infer the expected type of it's arguments. In this case, the outermost method call is +=, which accepts an argument of type Effect[PlayerCharacter]. Since the return type of when[T] is Effect[T], and the method to which the result is being passed expects an argument of type Effect[PlayerCharacter], it can infer that T is PlayerCharacter, and therefore the type of the lambda expression passed as the first argument to "when" is PlayerCharacter => Boolean. This seems to be how it works when the arguments are provided in one parameter list, so why does breaking the arguments into two parameter lists break it?

2

2 Answers

2
votes

I'm relatively new to Scala myself, and I don't have a lot of detailed technical knowledge of how the type inference works. So best take this with a grain of salt.

I think the difference is that the compiler is having trouble proving to itself that the two Ts are the same in condition : T => Boolean and effect : Effect[T], in the two-parameter-list version.

I believe when you have multiple parameter lists (because Scala views this as defining a method which returns a function that consumes the next parameter list) the compiler treats the parameter lists one at a time, not all together as in the single parameter list version.

So in this case:

def when[T](condition : T => Boolean, effect : Effect[T]) : Effect[T]

/* Snip */

myEffects += when(_.hpLow(), applyModifierEffect) //This works fine!

The type of applyModifierEffect and the required parameter type of myEffects += can help constrain the parameter type of _.hpLow(); all the Ts must be PlayerCharacter. But in the following:

myEffects += when(_.hpLow()) (applyModifierEffect)

The compiler has to figure out the type of when(_.hpLow()) independently, so that it can check if it's valid to apply it to applyModifierEffect. And on its own, _.hpLow() doesn't provide enough information for the compiler to deduce that this is when[PlayerCharacter](_.hpLow()), so it doesn't know that the return type is a function of type Effect[PlayerCharacter] => Effect[PlayerCharacter], so it doesn't know that it's valid to apply that function in that context. My guess would be that the type inference just doesn't connect the dots and figure out that there is exactly one type that avoids a type error.

And as for the other case that works:

def when[T](condition : T => Boolean) : Effect[T]

/* Snip */

myEffects += when(_.hpLow()) //This works too!

Here the return type of when and its parameter type are more directly connected, without going through the parameter type and the return type of the extra function created by currying. Since myEffects += requires an Effect[PlayerCharacter], the T must be PlayerCharacter, which has a hpLow method, and the compiler is done.

Hopefully someone more knowledgeable can correct me on the details, or point out if I'm barking up the wrong tree altogether!

2
votes

I am a little confused because in my view, none of the versions you say work should, and indeed I cannot make any of them work.

Type inferences works left to right from one parameter list (not parameter) to the next. Typical example is method foldLeft in collections (say Seq[A])

def foldLeft[B] (z: B)(op: (B, A) => B): B

The type of z will makes B known, so op can be written without specifying B (nor A which is known from the start, type parameter of this). If the routine was written as

def foldLeft[B](z: B, op: (B,A) => B): B

or as

def foldLeft[B](op: (B,A) => B)(z: B): B

it would not work, and one would have to make sure op type is clear, or to pass the B explicity when calling foldLeft.

In your case, I think the most pleasant to read equivalent would be to make when a method of Effect, (or make it look like one with an implicit conversion) you would then write

Effects += applyModifierEffect when (_.hpLow())

As you mentionned that Effect is contravariant, when signature is not allowed for a method of Effect (because of the T => Boolean, Function is contravariant in its first type parameter, and the condition appears as a parameter, so in contravariant position, two contravariants makes a covariant), but it can still be done with an implicit

object Effect {
  implicit def withWhen[T](e: Effect[T]) 
    = new {def when(condition: T => Boolean) = ...}
}