Can we allow non-indented cases for non`-match` `case` blocks

There’s a bit of an irregularity here where the first syntax is allowed but the second syntax isn’t:

scala> 1 match
       case 1 => 1
       
val res1: Int = 1
                                                                                                                          
scala> Seq(1).map:
       case 1 => 1
-- Error: ----------------------------------------------------------------------
2 |case 1 => 1
  |^^^^
  |indented definitions expected, eof found

Is there any technical reason we can’t support the second as well?

If not I can write up a SIP and implementation

2 Likes

It might be this easy.

I was eager to use the phrase, “relaxed colon”.

The description of <<< ts >>> needs to be updated for the special behavior of case after match, catch and :.

Edit: I was dubious about the proposal because in a match, the cases look like labels, but in an indented region they look like an anonymous function. But since it is just syntax, maybe I used that explanation to encourage unindented matches because that is my preference.

It’s not obvious that a SIP or a feature is required.

I like the idea of uniformity. Just wondering though, what would it look like if .map is followed by some other call, i.e.:

Seq(1).map:
case 1 => 2
.map:
case 2  => 3
.map: x =>
    somethingElse(x)

– should it looks like that?

I wouldn’t think so.

Do people like this notation enough to want to enable it, rather than disable it for match, though, if we’re after uniformity?

Personally, I find this to be inordinately difficult to parse and highly unpleasant aesthetically even if I could parse it. My tastes don’t match everyone’s, but this strikes me as the kind of thing that might put off a lot of people.

3 Likes

I think it’s a great way to reduce nesting without losing ambiguity. There are plenty of syntactic constructs with multiple keywords on the same nesting level: if/then/else, try-catch-finally, etc..

match/case can be thought of as a generalization of if/else, where instead of one discriminator expression and two branches you have one discriminator expression and N branches. Imagine if the then and else cases need to be nested one more level deeper than the if, that would be awkward and unnecessary! That’s the current situation for match/case

Looking at it from another angle, there are a lot of other languages where pattern matching cases are not indented: FSharp, Haskell, etc. are also indentation-sensitive languages that don’t indent their cases, while Ocaml and Python do. Given Scala already allows non-indented case statements for match, and we can’t remove that for backwards compatibility, the only way to achieve uniformity in Scala is to allow non-indented case statements in other contexts as well.

The analogy with if/else is also how I justify aligned match.

(As an aside, I just saw a comment circa 2015 on the style for the dotty project, whether else should align when there is a brace.

if (cond) {
  body
} else {
  other
}

Although I used the compressed style since days of C, I used aligned style encouraged on Scala 2, and as Odersky endorsed for Scala 3, years before the braces became optional.)

I find the argument from “uniformity” less persuasive, since the syntax is similar but the semantics is disparate.

There are discussions about colon syntax and how to teach it, especially with its current conveniences (special cases, so to speak):

f()
.tap: res =>
  g(res)

and now

f()
.tap:
case s: String => println(s"str $s")

On serial usage, I found an old snippet match-game.scala punning on an old tv show:

def f(x: Any) =
  x match {
  case i: Int =>
  i.toString
  case x => x
  }
  match
  case s: String => s"string $s"
  case x => s"value $x"

or maybe call it “stacked” usage. The question, “Should it look like that?”, is the same as aligned else, a style matter. The syntax ought to be liberal in what it accepts. Semantics, on the other hand, should be “opinionated”. That is, Scala 3 syntax that is more expressive of semantics, such as import x.given, may be deemed opinionated or restrictive.

I’d bet that for the overwhelming majority of people, the speed of parsing and code comprehension with something that indistinct would be worse than adding more levels.

But, anyway, it’s fine–I don’t have to use it, and I do like regularity.

However, I doubt I would willingly work on a codebase that actually used that style until every editor and display mode I might want to use gave additional visual guides.

It’s a good analogy! I sometimes switch to match in order to avoid the lack of visual clarity around if/else (with indented case of course).

We can already have all kinds of fun with conditionals in flat case statements.

foo() match
case e if
e.toLowerCase match
case "eel" => true
case _ => false
=> bar()
case e => baz(e)

It’s only fair to let lambdas join the fun.

1 Like

I have submitted a SIP for this change SIP-XX: Relaxed De-dented Case Syntax by lihaoyi · Pull Request #129 · scala/improvement-proposals · GitHub

2 Likes

Agreed, it looks more confusing and harder to read to me.

If regularity is the goal, I’d be more in favor of requiring indent with match and catch instead. Those are the “bad irregular” ones to me.

4 Likes

I would challenge you to claim my random example is not hard to read:

def f(x: Any) =
  x match {
  case i: Int =>
  i.toString
  case x => x
  }
  match
  case s: String => s"string $s"
  case x => s"value $x"

and then I would challenge you not to disallow braces because they permit code that is hard to read.

Seq(1).map:
case 1 => 2
.map:
case 2 => 3
.map: x =>
somethingElse(x)

I would like to know this as well. Is .map after the first case a continuation of 2 or a chained call to entire .map: case 1 => 2?

I would assume it would behave the same as chained matches when they are dedented, whatever currently happens to chained matches today: it appears the second match closes off the first match block and starts a subsequent

scala> import scala.quoted.*
                                                                                                                          
scala> inline def showTree(inline expr: Any): String = ${ showTreeImpl('expr) }
     | 
     | def showTreeImpl(expr: Expr[Any])(using Quotes): Expr[String] =
     |   import quotes.reflect.*
     |   Expr(expr.asTerm.show(using Printer.TreeStructure))
     | 
                                                                                                        
scala> showTree:
     |   1 match
     |   case 1 => 2
     |   match
     |   case 2 => 3
     | 
Inlined(None, Nil,
    Match(
        Match(
            Literal(IntConstant(1)),
            List(CaseDef(Literal(IntConstant(1)),None, Block(Nil, Literal(IntConstant(2)))))
        ),
        List(
            CaseDef(
                Literal(IntConstant(2)),
                None,
                Block(Nil, Literal(IntConstant(3)))
            )
        )
    )
)