From bd05b694f9c26bfce38ddea71a6f10e20533c5af Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 21 Sep 2025 18:31:06 +0200 Subject: [PATCH 1/8] SIP-XX: Allow single-line lambda after `:` --- content/singleLineLambdas.md | 107 +++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 content/singleLineLambdas.md diff --git a/content/singleLineLambdas.md b/content/singleLineLambdas.md new file mode 100644 index 00000000..c96e3855 --- /dev/null +++ b/content/singleLineLambdas.md @@ -0,0 +1,107 @@ +--- +layout: sip +permalink: /sips/into.html +stage: design +presip-thread: https://contributors.scala-lang.org/t/pre-sip-allow-fully-implicit-conversions-in-scala-3-with-into/7105 +title: Allow single-line lambdas after `:` +--- + +**By: Martin Odersky** + +## History + +| Date | Version | +|---------------|--------------------| +| Sep 21, 2025 | Initial Draft | + +## Summary + +This proposal is to allow a lambda expression following a `:` on the same line. +Currently, we need a newline and indent after the arrow, e.g. +```scala +xs.map: x => + x + 1 +``` +We propose to also allow to write the lambda on a single line: +```scala +xs.map: x => x + 1 +``` + +The lambda extends in this case to the end of the line. + +## History +This feature has been demanded repeatedly since the colon-lambda syntax was introduced as part of [SIP 44](https://docs.scala-lang.org/sips/fewer-braces.html), for instance see a [recent thread in Scala Users](https://users.scala-lang.org/t/why-were-single-line-lambdas-removed/11980/6). The original SIP 44 did not include it, because the concern at the time was the feature as a whole would look too much like type ascription and single line lambdas after colon would make that worse. But the experience since SIP 44 shipped has shown that the concerns about confusion with type ascriptions were largely overblown. So we now come back to the issue in a separate SIP. + +## Motivation + +The new behavior is more general and more intuitive. We can now state that a `:` means application if it is followed by an indented block or by a lambda. + +The new behavior also makes refactoring easier. One often splits or combines lines when some code part changes in length. We can now do this for lambda arguments without having to switch between parentheses and `:`. + +## Other Examples + +The syntax works for all kinds of function literals. They can start with one or more parameters, or with type parameters, or they can be partial functions starting +with `case`. + +```scala +Seq((1, 2), (3, 4)).map: (a, b) => a + b + +Seq((1, 2), (3, 4)).map: (a: Int, b: Int) => a + b + +Seq((1, 2), (3, 4)).collect: case (a, b) if b > 2 = a + +(1, true).map: [T] => (x: T) => List(x) +``` + +## Detailed Spec + +A `:` means application if its is followed by one of the following: + + 1. a line end and an indented block, + 2. a parameter section, followed by `=>`, a line end and an indented block, + 3. a parameter section, followed by `=>` and an expression on a single line, + 4. a case clause, representing a single-case partial function. + +(1) and (2) is the status quo, (3) and (4) are new. + +**Restriction:** (3) and (4) do not apply in code that is immediately enclosed in parentheses (without being more closely enclosed in braces or indentation). This is to avoid an ambiguity with type ascription. For instance, +```scala +( + x: Int => Int +) +``` +still means type ascription, no interpretation as function application is attempted. + +## Compatibility + +Because of the restriction mentioned above, the new scheme is fully compatible with +existing code. This is because type ascriptions with function types are currently only allowed when they are enclosed in parentheses. + +The scheme is also already compatible with _SIP-XX - No-Curly Partial Functions and Matches_ since it allows case clauses after `:`, so single case clauses can appear syntactically in all contexts where lambdas can appear. In fact, one could envisage to merge the two SIPs into one. + +## Implementation + +An [implementation](https://github.com/scala/scala3/pull/23821) of the new rules supports this SIP as well as _SIP-XX - No-Curly Partial Functions and Matches_. The new behavior is enabled by a language import `language.experimental.relaxedLambdas`. + +The implementation is quite straightforward. It does require a rich model of interaction between lexer and parser, but that model is already in place to support other constructs. The model is as follows: + +In the Scala compiler, the lexer produces a stream of tokens that the parser consumes. The lexer can be seen as a pushdown automaton that maintains a stack of regions that record the environment of the current lexeme: whether it is enclosed in parentheses, brackets or braces, whether it is an indented block, or whether it is in the pattern of a case clause. There is a backchannel of information from parser to scanner where the parser can push a region on the stack. + +With the new scheme we need to enter a "single-line-lambda" region after a `:`, provided the `:` is followed by something that looks like a parameter section and a `=>`. Testing this condition can involve unlimited lookahead when a pair of matching parentheses enclosing a parameter section needs to be identified. If the test is positive, the parser instructs the lexer to create a new region representing a single line lambda. The region ends at the end of the line. + +## Syntax Changes + +``` +ColonArgument ::= colon [LambdaStart] + indent (CaseClauses | Block) outdent + | colon LambdaStart expr ENDlambda + | colon ExprCaseClause +LambdaStart ::= FunParams (‘=>’ | ‘?=>’) + | TypTypeParamClause ‘=>’``` +``` +The second and third alternatives of `ColonArgument` are new, the rest is as before. + +Notes: + + - Lexer inserts ENDlambda at the next EOL, before producing a NEWLINE. + - The case does not apply if the directly enclosing region is bounded by parentheses `(` ... `)`. \ No newline at end of file From 777db114ae41a8b70428ef68e69b43cea04012e5 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 21 Sep 2025 18:36:04 +0200 Subject: [PATCH 2/8] Six pre-sip thread URL --- content/singleLineLambdas.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/singleLineLambdas.md b/content/singleLineLambdas.md index c96e3855..ad432ab3 100644 --- a/content/singleLineLambdas.md +++ b/content/singleLineLambdas.md @@ -2,7 +2,7 @@ layout: sip permalink: /sips/into.html stage: design -presip-thread: https://contributors.scala-lang.org/t/pre-sip-allow-fully-implicit-conversions-in-scala-3-with-into/7105 +presip-thread: https://contributors.scala-lang.org/t/pre-sip-allow-single-line-lambdas-after/7258 title: Allow single-line lambdas after `:` --- From ee2f8b105abdb609c3891f06b0c5cebdca810d8c Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 25 Sep 2025 14:08:12 +0200 Subject: [PATCH 3/8] Update content/singleLineLambdas.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michał Pałka --- content/singleLineLambdas.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/singleLineLambdas.md b/content/singleLineLambdas.md index ad432ab3..986f6c8f 100644 --- a/content/singleLineLambdas.md +++ b/content/singleLineLambdas.md @@ -87,7 +87,7 @@ The implementation is quite straightforward. It does require a rich model of int In the Scala compiler, the lexer produces a stream of tokens that the parser consumes. The lexer can be seen as a pushdown automaton that maintains a stack of regions that record the environment of the current lexeme: whether it is enclosed in parentheses, brackets or braces, whether it is an indented block, or whether it is in the pattern of a case clause. There is a backchannel of information from parser to scanner where the parser can push a region on the stack. -With the new scheme we need to enter a "single-line-lambda" region after a `:`, provided the `:` is followed by something that looks like a parameter section and a `=>`. Testing this condition can involve unlimited lookahead when a pair of matching parentheses enclosing a parameter section needs to be identified. If the test is positive, the parser instructs the lexer to create a new region representing a single line lambda. The region ends at the end of the line. +With the new scheme we need to enter a "single-line-lambda" region after a `:`, provided the `:` is followed by something that looks like a parameter section and a `=>` or a `?=>`. Testing this condition can involve unlimited lookahead when a pair of matching parentheses enclosing a parameter section needs to be identified. If the test is positive, the parser instructs the lexer to create a new region representing a single line lambda. The region ends at the end of the line. ## Syntax Changes From f6562552bcb37995c00fb154195084a8375c3744 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 25 Sep 2025 22:24:31 +0200 Subject: [PATCH 4/8] Admit curried parameter sections in multi-line lambdas --- content/singleLineLambdas.md | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/content/singleLineLambdas.md b/content/singleLineLambdas.md index 986f6c8f..6a083f91 100644 --- a/content/singleLineLambdas.md +++ b/content/singleLineLambdas.md @@ -53,13 +53,21 @@ Seq((1, 2), (3, 4)).collect: case (a, b) if b > 2 = a (1, true).map: [T] => (x: T) => List(x) ``` +The syntax does not work for function values that do not contain a `=>` or `?=>`. For instance the following are illegal. + +```scala +Seq((1, 2), (3, 4)).map: _ + _ // error + +Seq(1, 2, 3).map: plus1 // error +``` + ## Detailed Spec A `:` means application if its is followed by one of the following: 1. a line end and an indented block, - 2. a parameter section, followed by `=>`, a line end and an indented block, - 3. a parameter section, followed by `=>` and an expression on a single line, + 2. a parameter section, followed by `=>` or `=>?`, a line end and an indented block, + 3. a parameter section, followed by `=>` or `=>?` and an expression on a single line, 4. a case clause, representing a single-case partial function. (1) and (2) is the status quo, (3) and (4) are new. @@ -72,6 +80,23 @@ A `:` means application if its is followed by one of the following: ``` still means type ascription, no interpretation as function application is attempted. +## Curried Multi-Line Lambdas + +Previously, we admitted only a single parameter section and an arrow before +an indented block. We now also admit multiple such sections. So the following +is now legal: + +```scala +def fun(f: Int => Int => Int): Int = f(1)(2) + +fun: (x: Int) => y => + x + y +``` + +In the detailed spec above, point (2) is modified as follows: + +2. one or more parameter sections, each followed by `=>` or `=>?`, and finally a line end and an indented block. + ## Compatibility Because of the restriction mentioned above, the new scheme is fully compatible with @@ -92,14 +117,17 @@ With the new scheme we need to enter a "single-line-lambda" region after a `:`, ## Syntax Changes ``` -ColonArgument ::= colon [LambdaStart] +ColonArgument ::= colon {LambdaStart} indent (CaseClauses | Block) outdent | colon LambdaStart expr ENDlambda | colon ExprCaseClause LambdaStart ::= FunParams (‘=>’ | ‘?=>’) | TypTypeParamClause ‘=>’``` ``` -The second and third alternatives of `ColonArgument` are new, the rest is as before. +Changes wrt existing syntax: + + - In the first clause of `ColonArgument`, `[LambdaStart]` is now `{LambdaStart}`. + - The second and third alternatives of `ColonArgument` are new. Notes: From 3f7705641959564f45179a26ea78ca568e3e74be Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 26 Sep 2025 09:21:32 +0200 Subject: [PATCH 5/8] Apply suggestion from @prolativ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michał Pałka --- content/singleLineLambdas.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/singleLineLambdas.md b/content/singleLineLambdas.md index 6a083f91..7f2f5bff 100644 --- a/content/singleLineLambdas.md +++ b/content/singleLineLambdas.md @@ -66,7 +66,7 @@ Seq(1, 2, 3).map: plus1 // error A `:` means application if its is followed by one of the following: 1. a line end and an indented block, - 2. a parameter section, followed by `=>` or `=>?`, a line end and an indented block, + 2. a parameter section, followed by `=>` or `?=>`, a line end and an indented block, 3. a parameter section, followed by `=>` or `=>?` and an expression on a single line, 4. a case clause, representing a single-case partial function. From 2e6bb897b81f2160b337ed3f7c34e0feab92a1bd Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 26 Sep 2025 09:21:49 +0200 Subject: [PATCH 6/8] Apply suggestion from @prolativ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michał Pałka --- content/singleLineLambdas.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/singleLineLambdas.md b/content/singleLineLambdas.md index 7f2f5bff..d617e096 100644 --- a/content/singleLineLambdas.md +++ b/content/singleLineLambdas.md @@ -95,7 +95,7 @@ fun: (x: Int) => y => In the detailed spec above, point (2) is modified as follows: -2. one or more parameter sections, each followed by `=>` or `=>?`, and finally a line end and an indented block. +2. one or more parameter sections, each followed by `=>` or `?=>`, and finally a line end and an indented block. ## Compatibility From fc60d3f4c2b4728e168f8bd4cc35956072ee71ef Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 26 Sep 2025 09:21:55 +0200 Subject: [PATCH 7/8] Apply suggestion from @prolativ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michał Pałka --- content/singleLineLambdas.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/singleLineLambdas.md b/content/singleLineLambdas.md index d617e096..14b59800 100644 --- a/content/singleLineLambdas.md +++ b/content/singleLineLambdas.md @@ -67,7 +67,7 @@ A `:` means application if its is followed by one of the following: 1. a line end and an indented block, 2. a parameter section, followed by `=>` or `?=>`, a line end and an indented block, - 3. a parameter section, followed by `=>` or `=>?` and an expression on a single line, + 3. a parameter section, followed by `=>` or `?=>` and an expression on a single line, 4. a case clause, representing a single-case partial function. (1) and (2) is the status quo, (3) and (4) are new. From 021d1077a175082eacfd0b332eae310ad5698fa5 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 26 Sep 2025 09:51:08 +0200 Subject: [PATCH 8/8] Discussion of nested single-line lambdas --- content/singleLineLambdas.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/content/singleLineLambdas.md b/content/singleLineLambdas.md index 14b59800..5230824f 100644 --- a/content/singleLineLambdas.md +++ b/content/singleLineLambdas.md @@ -10,9 +10,10 @@ title: Allow single-line lambdas after `:` ## History -| Date | Version | -|---------------|--------------------| -| Sep 21, 2025 | Initial Draft | +| Date | Version | +|---------------|--------------------------------| +| Sep 21, 2025 | Initial Draft | +| Sep 25, 2025 | Curried Multi-Line Lambdas | ## Summary @@ -61,6 +62,11 @@ Seq((1, 2), (3, 4)).map: _ + _ // error Seq(1, 2, 3).map: plus1 // error ``` +Single-line lambdas can be nested, as in: +```scala + xs.map: x => x.toString + xs.dropWhile: y => y > 0 +``` + ## Detailed Spec A `:` means application if its is followed by one of the following: @@ -95,7 +101,7 @@ fun: (x: Int) => y => In the detailed spec above, point (2) is modified as follows: -2. one or more parameter sections, each followed by `=>` or `?=>`, and finally a line end and an indented block. +2. _one or more_ parameter sections, _each_ followed by `=>` or `?=>`, and finally a line end and an indented block. ## Compatibility @@ -114,6 +120,10 @@ In the Scala compiler, the lexer produces a stream of tokens that the parser con With the new scheme we need to enter a "single-line-lambda" region after a `:`, provided the `:` is followed by something that looks like a parameter section and a `=>` or a `?=>`. Testing this condition can involve unlimited lookahead when a pair of matching parentheses enclosing a parameter section needs to be identified. If the test is positive, the parser instructs the lexer to create a new region representing a single line lambda. The region ends at the end of the line. +## Alternatives + +Nested single-line lambdas could be disallowed if it is felt that they lead to unreadable code. This could be easily enforced by saying that single-line lambdas are not recognized in regions bounded by parentheses _or_ in regions representing single-line lambdas. + ## Syntax Changes ```