haskellindentation

Indentation of a nested function appears to require at least 5 spaces in haskell


I am trying to learn haskell through the advent of code (I know, it's late in the season), but I am encountering indentation issues I do not understand, despite checking what I believed to be the relevant sections on haskell.org or wikibooks.

Specifically, while indenting my inner auxiliary function, haskell will send a parse error if I indent by 4 or less spaces (not counting the 4 spaces of indentation of the main block), but will work if indenting by at least 5 additional spaces. So I need to indent my let ... in by 9 spaces at least. However indenting the main block by only 4 spaces and not 5 seems to work fine, and I do not understand the difference.

So this works :

solvePart1 :: String -> IO ()
solvePart1 input = do
    let parsedInput = map (map read . words) . lines $ input :: [[Int]]
    let validateReport report =
         let differences = zipWith (-) report (tail report)
         in all (\d -> d >= 1 && d <= 3) differences || all (\d -> d <= -1 && d >= -3) differences
    let areReportsValid = map validateReport parsedInput
    let validReportsCount = sum $ map fromEnum areReportsValid

    putStrLn $ "Day 2, Part 1: " ++ show validReportsCount

but this does not :

solvePart1 :: String -> IO ()
solvePart1 input = do
    let parsedInput = map (map read . words) . lines $ input :: [[Int]]
    let validateReport report =
        let differences = zipWith (-) report (tail report)
        in all (\d -> d >= 1 && d <= 3) differences || all (\d -> d <= -1 && d >= -3) differences
    let areReportsValid = map validateReport parsedInput
    let validReportsCount = sum $ map fromEnum areReportsValid

    putStrLn $ "Day 2, Part 1: " ++ show validReportsCount

The issue is not related to the do vs let ... in syntax, as rewriting the inner function as a do block does not change the issue : 5 spaces are still required.

(I am using vscode as an editor and building with stack in case it is relevant)


Solution

  • To some extent, it is the same as:

    solvePart1 input = do
     let parsedInput = map (map read . words) . lines $ input :: [[Int]]
     -- …
    

    versus:

    solvePart1 input = do
    let parsedInput = map (map read . words) . lines $ input :: [[Int]]
    -- …
    

    The item needs to be one column to the right of the start of the identifier that defines the scope. So the let itself does not count, indeed, for example the same does not work either:

    let     x y =
         let tmp = y
         in tmp
    in x 5
    

    It works if the let is one column more to the right than x:

    let     x y =
             let tmp = y
             in tmp
    in x 5
    

    That being said, typically you don't use a lot of lets in a do block, but work with a function without a do block, like:

    validateReport :: [Int] -> Bool
    validateReport report = all (\d -> d >= 1 && d <= 3) differences || all (\d -> d <= -1 && d >= -3) differences
      where
        differences = zipWith (-) report (tail report)
    
    solve :: [[Int]] -> Int
    solve parsedInput = sum (map fromEnum (map validateReport parsedInput))

    and then solve it with:

    solvePart1 :: String -> IO ()
    solvePart1 input = do
      let parsedInput = map (map read . words) . lines $ input :: [[Int]]
      putStrLn $ "Day 2, Part 1: " ++ show (solve parsedInput)