Unexpected type inference failure

val temps = List(32, 21, 40)

temps.find(_ * 1.8 + 32.0 > 90.0)
temps.find((_ * 1.8) + 32.0 > 90.0)

Type inference works with the first expression but fails with the second. Any deep reason why? My naive intuition is that a mechanism that works for the first should also work for the other.

The underscore shortcut binds to the innermost (parens) nesting level - in the second example it assumes _ * 1.8 to be an anonymous function.

temps.find(x => (x * 1.8) + 32.0 > 90.0)

This is still not entirely clear to me:

  • x * 1.8 + 32.0 < 90.0 and (x * 1.8) + 32.0 < 90.0 are (presumably) parsed into the same thing. Not so with an underscore instead of a variable name?

  • strings.find(_.length > 3) works fine, which means _.length in that expression does not have type String => Int. So, what’s innermost exactly?

(_ * 1.8) + 32.0 < 90.0

would be equivalent to

(y => y * 1.8) + 32.0 < 90.0

The first thing the compiler stumbles over is that it cannot infer a type for _ (i.e. y), but obviously there’s more issues with this expression (and its use as an argument to #find()).

Yes, it’s of type Int, as the full expression is parsed as strings.find(x => x.length > 3).

Um… Here’s what looks like the relevant part of the lang spec.

Frankly, I’ve never really drilled deeper into the subject - to me it basically is parens nesting, and I rarely write expressions (in particular involving underscores) complicated enough to trigger any additional edge cases of this naive understanding.

Having drilled, I can say unhelpfully that it is defined in terms of syntax productions.

I think the rule of thumb about parens is unhelpful. (Because it is often repeated but has settled nothing in the popular imagination.)

The underscore expands at the enclosing expression, as opposed to “simple expressions” like infix ops x * y and selections x.y.

scala> List(true).map(!_)
val res0: List[Boolean] = List(false)

scala> List(true).exists(if (_) false else true)
val res1: Boolean = false

Everything is an expression, such as the condition:

scala> List(true).exists(if (!_) false else true)
                              ^
       error: missing parameter type for expanded function ((<x$1: error>) => x$1.unary_$bang)

where the enclosing expession is the condition and not the if.

Similarly

scala> List(42).map((_, 27))
val res3: List[(Int, Int)] = List((42,27))

scala> List(42).map((_ + 1, 27))
                     ^
       error: missing parameter type for expanded function ((<x$1: error>) => x$1.$plus(1))

because the elements in parens are expressions. (The infix op is a simple expression, but in a comma-separated list in parens, it’s an expression.)

I think the notion of “simple expressions” is helpful just to remember why common idioms work, like

strings.find(_.length > 3)

or

scala> Option(42).map(_ + 1 > 50)
val res6: Option[Boolean] = Some(false)

scala> List((i: Int) => i+1).map(_(42))
val res7: List[Int] = List(43)

Everything is an expression, but some things are simple expressions that don’t keep the underscore from bubbling up.

2 Likes

Thanks for the reference. I agree with @sangamon that complex expressions with underscores are hard to read (I’m working on examples of what not to do for a lecture), but I’d like to understand the limits better myself. I got stuck in a demo because I could convert my temperatures from Celsius to Fahrenheit but not the other way around:

temps.map(_ * 1.8 + 32)   // OK
temps.map((_ - 32) / 1.8) // doesn't work

and I’m unable to explain why.

A separate (maybe related) issue is confusing error messages. This expression:

val f: (Int, Int) => Int = Integer.compare(_ + 1, _)

produces this error in Scala 2:

missing parameter type for expanded function ((<x$1: error>) => x$1.$plus(1))

but this in Scala 3:

Wrong number of parameters, expected: 2

Neither message is particularly helpful.

Expr as opposed to simple expressions or Expr1 in the syntax summary of the spec.

In compare(_ + 1, _), the difference is that one underscore is “properly enclosed” by Expr, while the other occupies the Expr itself. That is somewhat subtle, but explains the common question, why doesn’t xs.map(_) work?

I think my explanation is a good approximation. It inverts the usual question, which is “Where does the underscore expand to?” (and which the spec answers), by saying some expressions do not induce expansion (like infix expressions and selections) and everything else does.

If your question is about pedagogy, then the challenge is more like boolean expressions: add parens until the meaning is clear. In this case, add parens and explicit parameters if needed for clarity.

There is an old scala 2 ticket (or two) to improve the error message.

I remember the idea to synthesize a quick fix and try it, perhaps reporting it as, “did you mean?”

The quick fix would be driven by an enclosing expression for which the expected type is a function.

The other issue is that the user wants to see the expansion in context. IDEs should show expansions in hovers, if they don’t already.

I like //print<tab> comment in REPL:

scala> val f: (Int, Int) => Int = Integer.compare(_ + 1, _) // print

private[this] val `f `: scala.Function2[scala.Int, scala.Int, scala.Int] = ((x$2: Int) => java.lang.Integer.compare(((x$1) => x$1.+(1)), x$2))
1 Like

I like this formulation. The Staircase book explains it as:

[Y]ou can use underscores as placeholders for one or more parameters, so long as each parameter appears only one time within the function literal.[…] You can think of the underscore as a “blank” in the expression that needs to be “filled in.” This blank will be filled in with an argument to the function each time the function is invoked.

In my mind, this suggests something like a lambda abstraction, going from f(x) (with a single occurrence of x) to lambda x. f(x), which would be completely general, no matter where x occurs. I’m glad I got a chance to clarify that this is not what the specs say.

That I don’t quite see. Adding clarifying parentheses to filter(_ * 1.8 + 32 > 90) would be what? filter((_ * 1.8 + 32 > 90))? And other parentheses, e.g., to clarify operator precedence, get in the way since filter((_ * 1.8) + 32 > 90) doesn’t work. And even if I’m willing to make an expression ugly with parentheses and type ascriptions, there’s still no way to use partial application for the reverse transformation: filter((_ - 32) / 1.8 > 32.2), is there?

I don’t use the underscore much myself. I don’t mind the occasional (_ > 0), but even temp => temp > 90 is better than _ > 90 at reminding the reader that we’re talking about temperatures. Still, I want students to be aware of the mechanism, as they might encounter variations of it in other languages, e.g.:

temps.filter { (it - 32) / 1.8 > 32.2 }

works fine in Kotlin.

readings.filter((temp: Celsius) => temp > 100) would be fully explicit.

Note that Scala 3 no longer accepts

List(42).filter { i: Int => i > 40 }

which was arguably an edge case in the syntax. But the change suggests that, for style and clarity, start with canonical forms, for which placeholder syntax is a mere convenience.

It’s certainly worth telling students that if you expect it to work like Kotlin, you’re out of luck.

In case anyone wonders if our prayers are heard in heaven.