Scala - Optional Braces



Scala 3 has rules on indentation and also has braces { … } as optional. There are some codes which can give warnings if they are not indented properly. Braces { … } are optional in Scala 3. Braces do not affect the meaning of a well-indented program in Scala 3. You can use `-no-indent` to turn off indentation mode.

Indentation Rules

There are two rules for indentation. If the program is not well-indented then there will be a warning by the compiler.

1. In a section enclosed by braces, no statement can begin before the first statement on a new line after the opening brace. This rule is used to find missing closing braces and can prevent errors.

For example,

if condition {
    print("This is indented correctly")
  print("This is indented too far to the left")  # Error: IndentationError

2. When indentation is off or in Scala 2 mode, and a sub-part ends with a newline, the next statement should start with an indentation less than the sub-part to avoid missing an opening brace.

For example,

if condition
  print("This is indented correctly")
  print("This is also indented correctly")  // error: missing `{`

These rules provide flexibility in indentation. You are not restricting expression indentation or demanding exact alignment within a block. These rules help to identify errors with missing braces. This can be challenging to spot in large programs.

Optional Braces

Compiler adds <indent> and <outdent> tokens at line breaks. These tokens work like { and }. Algorithm uses stack (IW) to track previous indentation widths. It starts with one element having zero width and current width is from the top of the stack. 

These are two rules:

1. `<indent>` is added at a line break if

  • Source can start indentation region at the current position and
  • First token on the next line has greater indentation width.

Indentation starts:

  • After an extension's leading parameters.
  • After 'with' in an instance.
  • After a ": at end of line" token.
  • After certain tokens like '=', '=>', 'for', etc.

If <indent> is added, the next line token width is pushed onto IW.

2. `<outdent>` is added at a line break if

  • Next line's token width is less than the current width.
  • Previous line's last token is not 'then', 'else', 'do', etc.
  • Next line first token is not a leading infix operator.

If `<outdent>` is added, top of stack IW element is removed. If the next line token width is still less than the new current width, then more <outdent> tokens can be added.

`<outdent>` is added if the next token after <indent> and statement sequence closes the indentation, like 'then', 'else', 'do', etc.

<outdent> is also inserted before a comma after an <indent> if the indented part is within parentheses.

An error occurs if the token after an <outdent> does not match the previous line indentation. For example, the following will be rejected.

if condition1 then
   result1
 else   // error: `else` does not align correctly
   result2

Indentation tokens are used where newline separates statements: top-level, within braces {...}, not inside parentheses (...), patterns, and types.

Optional Braces around Template bodies

In Scala grammar, class, trait, and object definitions are in 'template body' enclosed in braces. These braces can be optional according to rules.

If a template body can start and there is a ':' at the end of line and followed by indented statement, ':' is changed to ': at end of line.' The grammar can have optional ': at end of line' before a template body. Similar rules apply to enum bodies and local packages with nested definitions.

For example, these construct are valid:

trait Animal:
   def sound: String

class Dog extends Animal:
   def sound = "Woof"

object Cat:
   def sound = "Meow"

enum Food:
   case Pizza, Burger, Salad

new Animal:
   def sound = "Moo"

package fruits:
   def apple = "Red"

package vegetables:
   def carrot = "Orange"

In all cases, ': at end of line' can be replaced by braces without changing the meaning.

The syntax changes to allow this are as follows:

TraitTemplate   ::=  ExtendingTraits [colonEol] [TemplateContents]
CaseClassDef    ::=  case id ClassParams ExtendingTraits [colonEol] CaseClassBody
ObjectDefinition ::=  'object' Identifier [nl | colonEol] '{' Members '}'
MethodDefinition ::=  'def' MethodName ParameterClauses [colonEol] '=' MethodBody

`colonEol` means ': at end of line,' as explained earlier. The lexer now reports ': at end of line' if it is valid next token for the parser.

Spaces Vs Tabs

Indentation uses spaces and/or tabs. Indentation widths are sorted by their prefixes. For example, '2 tabs, then 4 spaces' is shorter than '2 tabs, then 5 spaces'. But '2 tabs, then 4 spaces' cannot be compared with '6 tabs' and '4 spaces, then 2 tabs'. An error occurs if the indentation of a line cannot be compared to the width of the current area. To avoid errors, it is best not to mix spaces and tabs in the same file.

Indentation and Braces

Indentation can be mixed with braces {...}, brackets [...], and parentheses (...). Here are the rules:

1. For braces {...}, the assumed indentation is from the first new line's token after the opening brace.

2. For brackets [...] or parentheses (...):

  • If the opening symbol is at the line's end, use the token after it.
  • Otherwise, use the enclosing region's indentation.

3. When a closing brace }, bracket ], or parenthesis ) is found, <outdent> tokens are added to close all open nested indentation regions.

For example,

{
   val a = calculate(
      a: Double, b =>
         a * (
            b + 2
         ) +
         (a +
         a)
   )
}

Special Treatment of Case Clauses

`match` expressions and `catch` clauses have refined indentation rules:

  • An indentation region starts after a 'match' or 'catch' when a 'case' follows at the current indentation width.
  • It ends at the first token at the same width that's not 'case' or any token with a smaller width.

These rules permit match expressions with cases that are not indented. For example,

grade match
case "A" => print("Excellent")
case "B" => print("Good")
case "C" => print("Satisfactory")
case "D" => print("Needs improvement")
case "F" => print("Fail")

println(".")

The End Marker

Indentation-based syntax is useful, but it is challenging to know when a large region ends. Braces have the same issue. Scala 3 introduces an optional end marker to address this.

For example,

def complicatedFunction(...) =
   ...
   if someCondition then
      ...
   else
      ... // a lengthy block of code
   end if
   ... // additional code
end complicatedFunction

End marker has 'end' and specifier (e.g., 'if', 'while'). Specifiers include keywords and identifiers. End markers can be used in statement sequences and the specifier should match the preceding statement.

Advertisements