Skip to content

Instantly share code, notes, and snippets.

@som-snytt
Created February 3, 2019 09:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save som-snytt/bb8ea6bad417f6169cb1df91f41bea32 to your computer and use it in GitHub Desktop.
Save som-snytt/bb8ea6bad417f6169cb1df91f41bea32 to your computer and use it in GitHub Desktop.
placeholder
There is only one rule for how placeholder syntax is translated to an anonymous function:
The next enclosing Expr, as defined by the syntax summary, is the function body, with the underscores converted to parameters.
This definition is given in section 6.23.2 of the the spec.
An Expr includes any of the control constructs such as if, while, and try, as well as function literal syntax x => f(x).
Function args and tuples (things in parens) are a list of Exprs.
Assignments, and the right-hand side of assignments and definitions are Exprs, as are statements in a block inside curly braces.
Smaller syntactic units, such as selections x.m, applications f(), and infix notation x + y are not Expr.
The underscore itself never triggers binding. (The spec language is that the triggering Expr must "properly contain" the underscore. That is, it must be bigger than the underscore itself.)
For example, because function args are just Exprs,
f(_ + 1)
the enclosing Expr is _ + 1 and this translates to
f(x => x + 1)
But the enclosing Expr for
f(_)
is the entire expression, because _ by itself is not binding:
x => f(x)
The same reasoning distinguishes
if (_: Boolean) 42 else 17
from
if (_ > 0) 42 else 17 // buzzer
where the conditional for an if is an Expr. If we didn't know that already, we might notice that the conditional can itself be an if expression.
Similarly,
throw _: Exception
but not
throw new NullPointerException() initCause _
The "simple" expressions are handy syntax:
List(42).foreach(println(_ + 1)) // the inner arg is an Expr, buzzer
but infix notation permits
List(42).foreach(Console println _ + 1)
where the + operator has higher precedence than println and the arg to foreach is the enclosing Expr, as desired.
This notation looks odd, but is possible just because the application f(42) is a simple expression, syntactically:
scala> List((i: Int) => i * 2).map(_(42))
// List(((i: Int) => i.$times(2))).map(((x$1) => x$1(42)))
res1: List[Int] = List(84)
Because the transform is just syntax, it's easier to reason about than type failures. There are no edge cases, and there is nothing hidden or implicit, only what you have written.
This would qualify as a subtlety, that the generator in a for comprehension is an Expr but the guard is not, so that this is OK
scala> val f: ((List[Int], Int) => Unit) = for (_ <- _ if _ > 1) print('.')
f: (List[Int], Int) => Unit = $$Lambda$2353/489712935@557abb68
scala> f(List(1,2,3), 42)
...
but not
val f: ((List[Int], Int) => Unit) = for (_ <- 42 :: _ if _ > 1) print('.')
with an attempt to augment the input list.
But there is no sense in which a function application is a special case.
scala> def f = List(1,2,3).mkString("[", _: String, "]") + "..."
f: String => String
scala> f("/")
res2: String = [1/2/3]...
The sub-expressions on the right side of f are "simple" expressions, and the parameter is bound at the entire right side, not at the "partial application" of mkString.
Other odd but very regular constructions include:
scala> (_: collection.mutable.Map[Int,Int])(17) = _: Int
// ((x$1: collection.mutable.Map[Int, Int], x$2: Int) => (x$1: collection.mutable.Map[Int, Int]).update(17, (x$2: Int)))
res4: (scala.collection.mutable.Map[Int,Int], Int) => Unit = $$Lambda$2035/1025726005@4cc7e3ad
scala> res4(m, 3)
scala> m
res6: scala.collection.mutable.Map[Int,Int] = HashMap(17 -> 3)
A possible edge case for Scala 2.13 is noted at a question about the interaction of placeholder syntax and named args.
Named args used to be ambiguous with ordinary assignment. A named arg did not count as a "bare underscore":
scala> var i = 0
i: Int = 0
scala> def f(i: Int) = i * 2
f: (i: Int)Int
scala> List(42).map(f(_))
res0: List[Int] = List(84)
scala> List(42).map(f(i = _))
^
error: missing parameter type for expanded function ((<x$1: error>) => i = x$1)
But "assignment syntax" is no longer accepted for arguments:
scala> def g[A](a: A) = ()
g: [A](a: A)Unit
scala> g(i = 42)
^
error: unknown parameter name: i
That means that f(i = _) could be taken as f(_) without ambiguity.
Scala syntax is so complicated!
Other answers about "placeholder syntax" and "partially applied functions" list a bunch of rules and long explanations with waving hands about why one usage works and another doesn't.
Yet everyone still sounds as confused as I am, as though every usage were a special case or, more tellingly, an "edge" case.
It will probably take a generous contributor hours and perhaps days to list them all, but what are the rules for how expressions with embedded underscores are translated to anonymous functions?
I will only accept the answer that lists them all!
Maybe I should put up a bounty or something?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment