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

Posted a new Pre-SIP. Please take a look if interested!

9 Likes

Seems nice !

Do we need to forbid multiple cases between parentheses ?
It seems like it would be more regular without this restriction

1 Like

Good question! I don’t really have a strong opinion either way. Perhaps we can see what other people think?

If case statements are blocks, this is irregular.

I always thought of them as blocks before: partial functions are a match statement (block) without needing the match because it’s unnecessary.

So I’m mildly negative to the idea. Feels like extra friction to make things slightly less regular. I don’t care very much, but it feels like a small step in the direction of reduced consistency.

If we do make the change, this should probably work too:

val x: PartialFunction[Any, Int] = case y: String => y.length
5 Likes

I agree with @Sporarum , all of these should work if we want regularity:

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

It would be a net negative I think, compared to the current braces requirement, if no-braces work only for single line lambdas. In that case I’d rather stick with curly braces as it is now.

I do agree with this also! :smiley: I also think of it that way. (Because I learned it that way from the Programming in Scala book.) However:

  1. I always found it weird / inconsistent to “replace” match with braces (in my mind)… conceptually, a non-exhaustive match can also be thought of as a partial function (I initially expected the compiler to decide “partial or not” based on exhaustivity check, alas it does not work that way…). Some cases without an accompanying match always felt “dangling”. (However I do admit that omitting the awkward (a, b) => (a, b) match ... is indeed nice :smiley: )

  2. A code block { ... } does not feel like the same thing as “what follows a match”. The braces around the cases felt like formality / ceremony, and not really an improvement over the match.

  3. The idea of a partial function can be viewed either way; especially since PartialFunction[A, B] is a subtype of A => B in Scala. So “every partial function is a function”, then it makes sense to pass them just like (total) functions.

1 Like

I don’t view it as “replacing the match with braces”. Match statements are blocks, that’s all. You can introduce the block with match, or you can introduce it because the return type is a partial function (which is true for catch as well). match means that you want a complete match (unless you annotate otherwise).

What this proposal does is essentially introduce a non-block match. We do have those: the destructuring of vals in assignments. But this uses the block syntax, except not in a block.

If there was any compelling reason for this, sure. But the reason is just "now we can use ( ) instead of { }, which seems pretty meh to me.

1 Like

I have updated the proposal in response to everyone’s feedback:

  1. Allow multi-case partial functions to use parentheses
  2. Allow partial functions without parentheses or braces as expressions
  3. Allow match statements to use parentheses as well if there is only one expression per branch
2 Likes

I think the extension makes sense, but the restrictions aren’t quite right.

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

ought to work, because

  case (a, b) if b > 2 =>
    println(b)  
    a

is an expression because

    println(b)
    a

is a block subsumed by the case ... => statement. It should be completely equivalent to

  case (a, b) if b > 2 => { println(b); a }

Because you can

xs.map(s => { println("Hello from a block inside an expression!"); s })

it hardly makes any sense to forbid the same construct just because case is floating around.

Relatedly, I am not sure that

xs.collect(
  case "eel" => "long"
  case x if x.length > 3 => x
)

really makes sense unless we are able to

xs.collect(case "eel" => "long" case x if x.length > 3 => x)

because inside parens the separate lines are not distinct statements.

I don’t think parens should magically get semicolon inference.

Now, it turns out that right now you don’t need semicolons between cases, so that’s completely consistent with how it works now. You can in fact

x match { case "eel" => "long" case x if x.length > 3 => x }

But if you wanted to limit the one-line paren-case to one case statement, then you’d better do it regardless of whether it’s on multiple lines.

Anyway, if we’re going to do this, please let’s not have special rules for how block syntax works in this case. Everything just works as it does now, except that if you expect a PartialFunction, you can put a case expression in without needing a block.

1 Like

@Ichoran that’s a good point. This already works today with normal function literals:

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

So no reason it shouldn’t work with partial functions

2 Likes

Sure, but in Scala 3 we don’t need braces for the match blocks… I guess the case acts as a “guard” for the parsing. For a purely Scala 3 person who learned it that way (me, I think that’s rare here maybe?) they do feel like replacing the match with braces.

It’s also a bit hard to explain, to students for example, why only a partial function requires this kind of code block with braces and not other functions, whereas we can use code blocks without braces everywhere else, even inside vals, in Scala 3.

I wouldn’t have made sense of it myself, if I had not worked through “Programming in Scala 5th Edition”. It states:

Case sequences as partial functions
A sequence of cases (i.e., alternatives) in curly braces can be used anywhere a function literal can be used.

Here is a simple example:

val withDefault: Option[Int] => Int =
  case Some(x) => x
  case None => 0


One other generalization is worth noting: a sequence of cases gives you a partial function.

The book is already inconsistent with itself; it said “inside curly braces” but the example does not use or require it! :smiley: (I guess the indentation takes care of it?) There are also some unfortunate bugs in Scastie that newcomers encounter.

I see some questions over on the Users forum from time to time from people confused about this (they haven’t worked through the book). Many people seem to be using {} in Scala 3 out of habit from other languages, or their IDEs do it for them, not really understanding code block or partial function concepts fully. I guess it’s not too big of a deal…

A bit off-topic: we might ask: are curly-braced code blocks even needed, or appropriate, in Scala 3, or just being dragged for compatibility reasons only?

Fair point… but that’s kind of the point of the SIP isn’t it :smiley:

I’m fine with the current status or the SIP, as long as the result ends up being consistent and regular. For the above reasons I think there is benefit in removing the curly brace restrictions.

As someone who still mostly uses the so called “old syntax” (braces), I also find this as something I would like to see, esp. for the single case case. In almost all cases opening brace is followed by a newline. Partial functions one-liners are the only exception and because of that they do not look very nice even in the code using braces.

1 Like

The “sequence of cases” syntax is not a “partial function literal”, it is a “pattern-matching anonymous function”. The pedantic distinction matters because only one of the following is a partial function:

  List(42).map:
    case i: Int => i + 1
  List(42).collect:
    case i: Int => i + 1

Function literals yield a function or partial function as required.

I think the use case for “cases in parens” is supported because you get indentation after =>. This is what Haoyi just called “normal” function literals, but which one may call “caseless”.

  List(42).map(i =>
    val j = i + 1
    j + 26
  )

This syntax was the basis for arguing against “colon lambda”:

  List(42).map: i =>
    val j = i + 1
    j + 26

Parsing “sequence of cases” in argument parens should be easy. I see that after colon, a subsequent case may be further indented but not dedented, so I’m not sure whether indentation would work the same in parens (whatever the rules are).

  List(42).collect:
    case i: Int if i < 10 => i + 1
      case j: Int if j > 10 => j + 2

Will people still use “parameter untupling” if they can write a case in parens?

In “old” syntax, a function application takes either parens or a block expression, which is why Rex says “it’s just a block”. The paren syntax looks less weird compared to:

  List(42).collect:
    if b then
      case i: Int if i < 10 => i + 1
    else
      case j: Int if j > 10 => j + 2

I would expect all the one-liners to work, as mentioned on another thread, as well as the multi-case case.

  List(42).map: i => i + 1

  List(42).map: case i: Int => i + 1

  List(42).map(
    case i: Int => i + 1
  )

  List(42).map(i => i + 1)

  List(42).map(case i: Int => i + 1)

They took away much-beloved parenless syntax, so it seems a natural progression to add parens and then remove them again:

  List(42).map { i: Int => i + 1 } // error now
  List(42).map { case i: Int => i + 1 }
  List(42).map(case i: Int => i + 1)
  List(42).map: case i: Int => i + 1

Syntax for one-line catch (which takes an expression, try 42 catch println(_)) calls it a “case expression” or ExprCaseClause, so I think that is the right lingo.

When the option to omit braces in favor of significant whitespace was pushed on the community, the promise was that the curly-braced code blocks would remain standard in the language.

A subset of users find the indentation style anywhere from unpleasant to borderline unreadable, and at least a plurality of tooling fails when asked to do basic editing operations (like copy/paste) on indentation-sensitive code.

That being said, given the rate of change in Scala 3, I wouldn’t be shocked one style was completely removed in favor of the other in the next few years. I don’t know if sunken-costs will nudge things in favor of the complete elimination of block delimiters, or if they’ll walk the change back. Given the comments about the lack of maintainers available to work on bugs, I don’t expect that maintaining what are effectively 2 languages will be sustainable in the long term.

2 Likes

Oooohhh… didn’t know that!

Isn’t it just that partial functions are subtypes of functions ?
C.f. PartialFunction

So both really are partial function literals, one is just widened to a regular function
(And possibly the compiler optimizes that widening, but this wouldn’t change that it is a partial function literal)

No, a partial function is synthesized only when a PartialFunction is expected.

So both syntaxes (arrow or cases) are for whatever type is required.

2 Likes