Execution of where clause in Haskell

Viewed 124

I got the following piece of code which I know that works but I am completely new to Haskell and got 2 questions about the where clause.

f3 :: [[Int]] -> [Int] -> [Int]
f3 [] status = status --- Base Case
f3 ([p1,p2]:tail) status
 | status !! (p1-1) == 0  = f3 tail status  --- Case 1
 | status !! (p2-1) == 1  = f3 tail newStatus1 --- Case 2
 | otherwise = f3 tail newStatus2 --- Case 3
  where newStatus1 = set status p1 0                    --- Line 7
        newStatus2 = set newStatus2Temp p1 1            --- Line 8
        newStatus2Temp = set status p2 0                --- Line 9

So basically the f3 predicate has 2 arguments :

  • A list of Integer lists like so : [[1,2],[2,3],[3,2]]
  • An Integer list

It's output is the final updated second argument.

As you see besides the base case I got 2 cases (2) and (3) where the status/[Int] argument via a standard set predicate.

Question 1) :

  • Say case 2 is true. Does Haskell execute lines 8 and 9 ?
  • Say case 3 is true. Does Haskell execute line 7?

Question 2) :

  • Can a guard have it's own where?
  • Is there a better way to actually do this?
1 Answers

As a result of lazy evaluation, the code in each of lines 7-9 is only run if the value of the corresponding binding is evaluated/used in the course of evaluation of code for the case that matches. So:

  • If case 1 is true, none of lines 7-9 are run.
  • If case 1 is false but case 2 is true, then evaluation of newStatus runs line 7, but lines 8-9 are not run.
  • If cases 1 and 2 are false but case 3 is true, then evaluation of newStatus2 runs line 8 which evaluates newStatus2Temp causing line 9 to run. Line 7 is not run.

The where clauses themselves can only be attached to entire pattern bindings (e.g., the whole f3 ([p1,p2]:tail) status | ... | ... = ... expression), not individual guards, so a guard can't have its own where clause. You could either repeat the pattern for each guard:

f3 :: [[Int]] -> [Int] -> [Int]
f3 [] status = status
f3 ([p1,p2]:tail) status | status !! (p1-1) == 0  = f3 tail status
f3 ([p1,p2]:tail) status | status !! (p2-1) == 1  = f3 tail newStatus1
  where newStatus1 = set status p1 0
f3 ([p1,p2]:tail) status | otherwise              = f3 tail newStatus2
  where newStatus2 = set newStatus2Temp p1 1
        newStatus2Temp = set status p2 0

or use let ... in ... blocks:

f3 :: [[Int]] -> [Int] -> [Int]
f3 [] status = status
f3 ([p1,p2]:tail) status
 | status !! (p1-1) == 0  = f3 tail status
 | status !! (p2-1) == 1
  = let newStatus1 = set status p1 0
    in f3 tail newStatus1
 | otherwise
  = let newStatus2 = set newStatus2Temp p1 1
        newStatus2Temp = set status p2 0
    in f3 tail newStatus2

I don't think there's anything wrong with your where-clause version, and it's not unusual to write Haskell code where only a subset of the bindings in the where-clause are used (or even valid/meaningful) for each case. With such small helpers, this specific example might be more clearly written without any helpers though:

f3 :: [[Int]] -> [Int] -> [Int]
f3 [] status = status
f3 ([p1,p2]:tail) status
 | status !! (p1-1) == 0  = f3 tail $ status
 | status !! (p2-1) == 1  = f3 tail $ set status p1 0
 | otherwise              = f3 tail $ set (set status p2 0) p1 1

With GHC and -O2, all four of these (your original code and these three variants) compile to identical low-level code, so use whichever you think is clearest.

Related