Created
February 3, 2019 09:31
-
-
Save som-snytt/bb8ea6bad417f6169cb1df91f41bea32 to your computer and use it in GitHub Desktop.
placeholder
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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