Layout Rule

Helium
The layout rule makes the braces and semi-colons you often see in other languages superfluous. The layout of your program tells the compiler where definitions and blocks of definitions end. Let us look at an example:

main = let sieve (x:xs) = x : sieve
                            (filter ((/= 0).(`mod` x)) xs)
           allPrimes = sieve [2..]
       in take 100 allPrimes

The definitions for sieve and allPrimes are indented to align vertically. This tells the compiler that these are definitions at the same level. The argument to sieve on the second line is indented more than the definition of sieve, telling the compiler that it is a continuation of the first line. The keyword in is indented less than the definitions and this tells the compiler that the block of definitions has ended. 

The layout rule starts working whenever it sees one of the following keywords: do, let, where, of.  It also works at top-level when you don't have a module header. The first symbol it sees after it is started (the underlined sieve in the example) determines the column with which indentation of subsequent lines is compared. 

The layout rule in short:

  • same indentation as the first in the block means a new definition/statement/alternative
  • more indentation means that this line belongs to the last line
  • less indentation means end of a block of definitions/statements/alternatives

The special case of Let-expressions

The layout rule has a special case for let expressions so that the keyword in also ends the block of definitions. This means you can write:

main = let divisible x y = x `mod` y == 0 in divisible 10 2

In the sieve example we could have used this special case and write "in take 100 allPrimes" behind "sieve [2..]" but that doesn't improve readability.

Some error messages about wrong layout

Since layout has a meaning in Helium you can also make mistakes that have to do with layout. In languages where there is no layout, the layout is purely meant to improve readibility for humans. In the case of Helium, the layout rule takes over the role of braces and semi-colons and making a layout mistake is like forgetting a semi-colon or writing too many closing braces. Let us see what happens:

main = let sieve (x:xs) = x : sieve 
              (filter ((/= 0).(`mod` x)) xs)
         allPrimes = sieve [2..]
       in take 100 allPrimes

In this example allPrimes is indented less than the first in the block (sieve). This means that the block started by let ends here. But when a let block ends there should be the keyword in and not the name allPrimes. And that's exactly what Helium tells you:

(3,10): Syntax error: 
    unexpected variable 'allPrimes'
    expecting keyword 'in'

Now the other way around. Let us indent allPrimes too much:

main = let sieve (x:xs) = x : sieve 
                            (filter ((/= 0).(`mod` x)) xs)
              allPrimes = sieve [2..]
       in take 100 allPrimes

Indenting more means that the line belongs to the previous line, the definition of sieve in this case. Therefore, the compiler looks at the definition of sieve as:

sieve (x:xs) = x : sieve (filter ((/= 0).(`mod` x)) xs) allPrimes = sieve [2..]

The variable allPrimes is okay in this context; it is seen as an extra parameter to sieve. If the compiler would get to type checking this would result in a type error, but the compiler doesn't get to that stage. The next symbol, the equals sign, comes as a total surprise; it can not occur in this context:

(3,25): Syntax error: 
    unexpected '='
    expecting operator, constructor operator, '::', keyword 'where', 
              next in block (based on layout), ';' or 
              end of block (based on layout)

The Helium compiler tells you it doesn't expect the '=' and also tells you all the things it considers acceptable in this context. You see that it would consider a next in block (based on layout) acceptable; in other words: a new definition that is indented the same. It would also be happy with an end of block and some other things but not with the '='.

Tabs are Evil

In short, don't use tab characters in your Helium source files. Your editor and the Helium compiler may interpret tab characters differently. In the Helium compiler a tab takes you to the next tab stop and tab stops are 8 characters apart. If your editor has differently sized tab stops, your program may look okay in your editor, but still the Helium compiler gives layout-related messages.

The solution is to configure your editor to convert existing and new tabs to spaces. This way no tab characters will appear in your source code and the Helium compiler sees the same layout as you do.

Differences with Haskell's Layout Rule

The Haskell layout rule is more powerful than the one in Helium. In Haskell, a block may also be terminated by a symbol that implies that the end of the block is reached. Let us look at an example again:

main = ( case 3 of 4 -> 5
                   3 -> 5, 6 )

The layout block started at 4 is ended at the comma because a comma is unexpected here, but if the block is closed here, it is okay to be there. The value of main is thus the tuple (5, 6). In Helium, to end a block you really have to start on a new line that is indented less. The following syntax error is reported:

(2,26): Syntax error: 
    unexpected ','
    expecting expression, operator, constructor operator, '::', 
              keyword 'where', next in block (based on layout), 
              ';' or end of block (based on layout)

The comma is unexpected and suggestions are made what can be expected as this place. One of those suggestions is an end of block (based on layout) and that's the one we meant:

main = ( case 3 of 4 -> 5
                   3 -> 5
       , 6 )

Or you can use explicit layout.

Explicit Layout

You can circumvent the layout rule and use explicit layout with braces and semi-colons. After the keywords do, let, where, of and at the beginning of a module you can write an opening brace. Definitions / alternatives / statements can then be separated by semi-colons and the block is ended by a closing brace. You can choose explicit layout locally:

main = ( case 3 of { 4 -> 5
                   ; 3 -> 5 }, 6 )

or for your whole program:

{  main = take 100 allPrimes;
sieve (x:xs) = x : sieve(filter ((/= 0).(`mod` x)) xs);
      allPrimes = sieve [2..]
  }

As you can see, inside a context of braces layout doesn't matter anymore.

-- JurriaanHage - 07 Apr 2008