Guards vs. if-then-else vs. cases in Haskell

HaskellIf StatementCase

Haskell Problem Overview


I have three functions that find the nth element of a list:

nthElement :: [a] -> Int -> Maybe a 
nthElement [] a = Nothing
nthElement (x:xs) a | a <= 0 = Nothing
                    | a == 1 = Just x
                    | a > 1 = nthElement xs (a-1)

nthElementIf :: [a] -> Int -> Maybe a
nthElementIf [] a = Nothing
nthElementIf (x:xs) a = if a <= 1
                        then if a <= 0 
                             then Nothing
                             else Just x -- a == 1
                        else nthElementIf xs (a-1)                           

nthElementCases :: [a] -> Int -> Maybe a
nthElementCases [] a = Nothing
nthElementCases (x:xs) a = case a <= 0 of
                             True -> Nothing
                             False -> case a == 1 of
                                        True -> Just x
                                        False -> nthElementCases xs (a-1)

In my opinion, the first function is the best implementation because it is the most concise. But is there anything about the other two implementations that would make them preferable? And by extension, how would you choose between using guards, if-then-else statements, and cases?

Haskell Solutions


Solution 1 - Haskell

From a technical standpoint, all three versions are equivalent.

That being said, my rule of thumb for styles is that if you can read it as if it was English (read | as "when", | otherwise as "otherwise" and = as "is" or "be"), you're probably doing something right.

if..then..else is for when you have one binary condition, or one single decision you need to make. Nested if..then..else-expressions are very uncommon in Haskell, and guards should almost always be used instead.

let absOfN =
  if n < 0 -- Single binary expression
  then -n
  else  n

Every if..then..else expression can be replaced by a guard if it is at the top level of a function, and this should generally be preferred, since you can add more cases more easily then:

abs n
  | n < 0     = -n
  | otherwise =  n

case..of is for when you have multiple code paths, and every code path is guided by the structure of a value, i.e. via pattern matching. You very seldom match on True and False.

case mapping of
  Constant v -> const v
  Function f -> map f

Guards complement case..of expressions, meaning that if you need to make complicated decisions depending on a value, first make decisions depending on the structure of your input, and then make decisions on the values in the structure.

handle  ExitSuccess = return ()
handle (ExitFailure code)
  | code < 0  = putStrLn . ("internal error " ++) . show . abs $ code
  | otherwise = putStrLn . ("user error " ++)     . show       $ code

BTW. As a style tip, always make a newline after a = or before a | if the stuff after the =/| is too long for one line, or uses more lines for some other reason:

-- NO!
nthElement (x:xs) a | a <= 0 = Nothing
                    | a == 1 = Just x
                    | a > 1 = nthElement xs (a-1)

-- Much more compact! Look at those spaces we didn't waste!
nthElement (x:xs) a
  | a <= 0    = Nothing
  | a == 1    = Just x
  | otherwise = nthElement xs (a-1)

Solution 2 - Haskell

I know this is question about style for explicitly recursive functions, but I would suggest that the best style is finding a way to reuse existing recursive functions instead.

nthElement xs n = guard (n > 0) >> listToMaybe (drop (n-1) xs)

Solution 3 - Haskell

This is just a matter of ordering but I think its very readable and has the same structure as guards.

nthElement :: [a] -> Int -> Maybe a 
nthElement [] a = Nothing
nthElement (x:xs) a = if a  < 1 then Nothing else
                      if a == 1 then Just x
                      else nthElement xs (a-1)

The last else doesn't need and if since there is no other possibilities, also functions should have "last resort case" in case you missed anything.

Solution 4 - Haskell

While all three implementations produce correct results, GHC (as of year 2021) complains that pattern matches are non-exhaustive – which is true insofar as possible patterns are hidden behind guards/if/case. Consider this implementation, which is both more conscise than the three of them, plus avoids a non-exhaustive patterns warning:

nthElement :: [a] -> Int -> Maybe a
nthElement (x:_) 1  = Just x
nthElement (_:xs) i = nthElement xs (i - 1)
nthElement _ _      = Nothing  -- index is out of bounds

The last pattern matches everything and therefore needs to be below a possibly successful match from the first two patterns.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionnucleartideView Question on Stackoverflow
Solution 1 - HaskelldflemstrView Answer on Stackoverflow
Solution 2 - HaskellDaniel WagnerView Answer on Stackoverflow
Solution 3 - HaskellCristian GarciaView Answer on Stackoverflow
Solution 4 - HaskellFritz FegerView Answer on Stackoverflow