Correct indentation rules using guards

599 Views Asked by At

I've looked at questions regarding indentation, which were of no help. My indentation also looks correct but according to the compiler it isnt. What is the correct indentation and what are the rules?

readFile filename = do 
                    inputFile <- openFile filename ReadMode
                    readLines inputFile
                    hClose inputFile

readLines inputFile = 
        do endof <- hIsEOF inputFile 
            | endof = return() 
            | otherwise = do 
                          inpStr <- hGetLine inputFile
                          print inpStr
                          readLines inputFile

Using all spaces and no tabs. Error: "parse error on input '|' | endof = return() "

2

There are 2 best solutions below

4
Will Ness On

You could restructure your code for this, like

readLines :: Handle -> IO ()
readLines inputFile  =  g =<< hIsEOF inputFile
    where                  -- hIsEOF :: Handle -> IO Bool
      g endof
            | endof = return () 
            | otherwise = do 
                          inpStr <- hGetLine inputFile
                          print inpStr
                          readLines inputFile

The guards, | ..., belong to function definitions or case expressions. They can't appear in do block by themselves.

g =<< hIsEOF inputFile is a shorter way to write

readLines inputFile  =  do {  endof <- hIsEOF inputFile
                           ;  g endof
                           }
    where
      g endof
            | endof = .....

But a simpler option is just to use if ... then ... else ... in the do block in the first place:

readLines inputFile = 
        do { endof <- hIsEOF inputFile 
           ; if endof 
                then return() 
                else do { inpStr <- hGetLine inputFile
                        ; print inpStr
                        ; readLines inputFile
                        }}

Yet another is using LambdaCase to inline the g definition:

readLines :: Handle -> IO ()
readLines inputFile  =  hIsEOF inputFile >>=
    (\ case { True -> return () 
            ; _    -> do 
                          inpStr <- hGetLine inputFile
                          print inpStr
                          readLines inputFile })

And case clauses can have guards (though here we didn't need them).

0
Jon Purdy On

As Will Ness explains in his answer, indentation is not your issue here—the problem is that you’re trying to use a guard (| …) after a statement in a do block, but guards can only ever appear between patterns and bodies in (1) function equations:

function param1 param2
  | guard1 = body1
  | guard2 = body2
  …

And (2) case expressions:

case expr of
  pattern1
    | guard1 -> body1
    | guard2 -> body2
  pattern2
    | guard3 -> body3
  …

So you probably want an if expression instead. As for the rules of indentation, your code is indented correctly, but you don’t need as much whitespace as you’re using: the basic rules are:

  • Certain keywords such as do, let, where, and of begin layout blocks

  • Within those blocks, everything must be indented past the first column of the first line in the block

  • If an expression wraps across multiple lines, the lines following the first must be indented

So a rule of thumb that always works is to simply add a newline and indent by some number of spaces (e.g. 2 or 4) after every such keyword:

readFile filename = do -- newline+indent to begin block
  inputFile <- openFile filename ReadMode
  readLines inputFile
  hClose inputFile

readLines inputFile = do -- newline+indent to begin block
  endof <- hIsEOF inputFile
  if endof -- indent to continue if expression
    then return () 
    else do -- newline+indent to begin block
      inpStr <- hGetLine inputFile
      print inpStr
      readLines inputFile

The alternative style is to start a block on the same line as the layout keyword; then everything needs to have the same alignment as that line:

readFile filename = do inputFile <- openFile filename ReadMode
                       readLines inputFile
                       hClose inputFile

readLines inputFile = do endof <- hIsEOF inputFile
                         if endof
                           then return () 
                           else do inpStr <- hGetLine inputFile
                                   print inpStr
                                   readLines inputFile

I prefer to avoid this style because it leads to deep indentation, which also depends on the width of the code before it.

Both of these styles are desugared to the following code with explicit delimiters, which you can also write yourself to get a better feel for how layout works:

readFile filename = do {
  inputFile <- openFile filename ReadMode;
  readLines inputFile;
  hClose inputFile;
};

readLines inputFile = do {
  endof <- hIsEOF inputFile; 
  if endof
    then return () 
    else do {
      inpStr <- hGetLine inputFile;
      print inpStr;
      readLines inputFile;
    };
};

One thing that often trips people up is that let also introduces a layout block for defining multiple bindings in the same block. So if you write a let statement or letin… expression with a long definition, then you need to either write it aligned:

let example1 = some very long definition
      that we need to wrap across lines
    example2 = another binding for illustration
--  ^ everything must be aligned past this column
in example1 . example2

Or use the same newline+indent style as for everything else:

let
  example1 = some very long definition
    that we need to wrap across lines
  example2 = another binding for illustration
in example1 . example2

Finally, as a convenience, if x then return () else y can be written unless x y or when (not x) y using unless or when from Control.Monad:

import Control.Monad (unless)

…
  endof <- hIsEOF inputFile
  unless endof $ do
    inpStr <- hGetLine inputFile
    print inpStr
    readLines inputFile

Furthermore, you may see the $ omitted before do (and other block keywords like case and \) in code that enables the BlockArguments extension:

{-# LANGUAGE BlockArguments #-}
import Control.Monad (unless)

…
  endof <- hIsEOF inputFile
  unless endof do -- no need for $
    inpStr <- hGetLine inputFile
    print inpStr
    readLines inputFile