SIP-74 - Allow Partial Function Literals to be defined with Parentheses

Language

By: Li Haoyi

History

Date Version
Aug 22nd 2025 Initial Draft

Summary

This proposal is to allow parens (...) to be used instead of curly braces {...} when defining partial functions which have only one case:

Seq((1, 2), (3, 4)).collect(case (a, b) if b > 2 => a) // 3

Currently this syntax is disallowed:

scala> Seq((1, 2), (3, 4)).collect(case (a, b) if b > 2 => a)
-- [E018] Syntax Error: --------------------------------------------------------
1 |Seq((1, 2), (3, 4)).collect(case (a, b) if b > 2 => a)
  |                            ^^^^
  |                            expression expected but case found
  |
  | longer explanation available when compiling with `-explain`

Partial functions with multiple case blocks should also be allowed to use parentheses :

Seq((1, 2), (3, 4)).collect(
  case (a, b) if b > 2 => a
  case _ => ???
)

Multi-line case blocks should work with parens as well, just like multi-line function literals without case already work:

Seq((1, 2), (3, 4)).collect(
  case (a, b) =>
    println(b)
    a
)

// This already works today
Seq((1, 2), (3, 4)).collect(
  (a, b) =>
    println(b)  
    a
)

Partial function literals should also be allowed to be defined without parentheses for single-line scenarios such as:

val partial: PartialFunction[(Int, Int), Int] = case (a, b) if b > 2 => a

This delimiter-less syntax is similar to what is already allowed today in catch blocks:

try ???
catch case e: Exception => ???

Out of Scope

For now, we do not allow parentheses to be used in match statements:

(1, 2) match (
  case (a, b) if b > 2 => a
  case _ => ???
)

And we do not allow parentheses for multi-case single-line partial functions:

Seq((1, 2), (3, 4)).collect{ case (a, b) if b > 2 => a case (a, b) if a > 2 => b }
Seq((1, 2), (3, 4)).collect(case (a, b) if b > 2 => a case (a, b) if a > 2 => b)
Seq((1, 2), (3, 4)).collect(
  case (a, b) if b > 2 => a
  case (a, b) if a > 2 => b
)

These could be added in future, but for now we left them out as they are edge cases that are not core to this proposal

Motivation

With Scala 3’s Optional Braces, and SIP-44’s Fewer Braces, single-line partial functions are one of the only remaining places where curly braces are mandatory in Scala syntax.

// No way to write this without curlies
Seq((1, 2), (3, 4)).collect{ case (a, b) if b > 2 => a } 

Needing to swap between parens and curlies also adds friction of converting a function to a partial function, which happens frequently:

Seq((1, 2), (3, 4)).map((a, b) => a) // OK
Seq((1, 2), (3, 4)).collect(case (a, b) if b > 2 => a) // BAD
Seq((1, 2), (3, 4)).collect{ case (a, b) if b > 2 => a } // OK

This also currently causes visual messiness in method call chains where some single-line methods use parens and others use curlies:

Seq((1, 2), (3, 4), (5, 6))
  .filter(_._1 < 5) // PARENS
  .collect{ case (a, b) if b > 2 => a } // CURLIES
  .reduce(_ + _) // PARENS

In the syntax of other expressions, curly braces are only necessary to define “blocks” with multiple statements: if-else, try-catch-finally, for-yield, do-while, method calls like foo() etc. all allow you to replace curly braces with parentheses or elide them altogether using indentation. This is unlike other languages like Java that mandate curly braces in these syntactic constructs. Furthermore, in most expressions, Optional Braces means you do not have to write the curlies if you do not want to.

This proposal brings partial functions in-line with the rest of Scala syntax, with the curly braces only being mandatory for multi-statement blocks, and made optional with Scala 3’s Optional Braces. With this proposal, curly braces are only for opening multi-statement blocks, always in places that can be replaced by indentation-based blocks if the user wants to do so. They are no longer also incidentally tied to partial-function syntax as they were before.

With this change, all the snippets below are now valid: we can see how the syntax of (), {}, or .collect: followed by indentation is now fully orthogonal to the partial function case expression within them, resulting in a much more regular syntax than before when parentheses were prohibited but the other syntaxes worked.

Seq((1, 2), (3, 4)).collect(case (a, b) if b > 2 => a)
Seq((1, 2), (3, 4)).collect { case (a, b) if b > 2 => a }
Seq((1, 2), (3, 4)).collect: 
  case (a, b) if b > 2 => a

Seq((1, 2), (3, 4)).collect( 
  case (a, b) if b > 2 => a
  case (a, b) if a > 2 => b
)

Seq((1, 2), (3, 4)).collect {
  case (a, b) if b > 2 => a
  case (a, b) if a > 2 => b
}

Seq((1, 2), (3, 4)).collect: 
  case (a, b) if b > 2 => a
  case (a, b) if a > 2 => b

Seq((1, 2), (3, 4)).collect(
  case (a, b) =>
    println(b)
    a
)

Seq((1, 2), (3, 4)).map((a, b) => a)
Seq((1, 2), (3, 4)).map { (a, b) => a }
Seq((1, 2), (3, 4)).map:
  (a, b) => a
Seq((1, 2), (3, 4)).map:
  (a, b) =>
    println(b)
    a

Seq((1, 2), (3, 4)).collect(
  case (a, b) if b > 2 =>
    println(b)
    a
)