How to do a where clause across pattern matches

Viewed 80

I can get a where clause to work across pattern matches a case statement:

updateHung :: Text -> Maybe Char -> Int
updateHung word got =
    let x = case got of
            Just l
                | elem l . unpack $ word -> pass
                | otherwise -> strike
            Nothing -> pass
            where
                strike = 1
                pass = 0
    in x

But when I try to do the same thing using a multipart function it doesn't work:

updateHung :: Text -> Maybe Char -> Int
updateHung word (Just l)
    | elem l . unpack $ word = pass
    | otherwise = strike
updateHung word Nothing = pass
    where
        strike = 1
        pass = 0

Is there some way to get this to work?

2 Answers

In the working version, your where clause is indented misleadingly. You've indented it as though it were attached to the case statement, but in fact it is attached to the definition of x, and would be more clearly indented as

updateHung :: Text -> Maybe Char -> Int
updateHung word got =
    let x = case got of
          Just l
              | elem l . unpack $ word -> pass
              | otherwise -> strike
          Nothing -> pass
          where
            strike = 1
            pass = 0
    in x

A where clause is always scoped to a single pattern, across all the guards of that pattern. This is very important, because that allows it to use variables that were introduced by the pattern. For example, it might be useful for you to use l in the definition of pass, but that would make no sense if you could somehow have it scoped to the entire case statement.

If you want variables in scope for all of your patterns, you must bind those variables before starting to pattern match. Define a single equation for your function, and define variables in it, either with let or with where, and then do the rest of your logic in a case on a tuple of all your parameters, or just the ones you care about:

updateHung :: a -> Maybe Char -> Int
updateHung word got =
  let strike = 1
      pass = 0
  in case got of
     Just l
       | elem l . unpack $ word -> pass
       | otherwise -> strike
     Nothing -> pass

or

updateHung :: Text -> Maybe Char -> Int
updateHung word got =
  case got of
     Just l
       | elem l . unpack $ word -> pass
       | otherwise -> strike
     Nothing -> pass
  where strike = 1
        pass = 0

@amalloy’s answer explains how and why where clauses are scoped to a single equation, not a whole definition, and offers an alternative using case. But just to answer your narrower question, if you want to keep using the equational style, but also have shared local definitions in a where clause, you can use a local “worker” function scoped to the same where block as your auxiliary definitions, so they’re all in scope together:

updateHung = go
  where
    go word (Just l)
      | elem l $ unpack word = pass
      | otherwise = strike
    go word Nothing = pass

    strike = 1
    pass = 0

Then, as a simplification, you can lift the word parameter to updateHung, because it’s always matched the same way in go:

updateHung word = go
  where
    go (Just l)
      | elem l $ unpack word = pass
      | otherwise = strike
    go Nothing = pass

    strike = 1
    pass = 0

When the local definition is recursive and the outer definition just sets up its initial parameters, this is often referred to as the worker–wrapper transform[ation], which can be beneficial to performance in two ways:

  1. Sharing parameters like word that remain the same throughout the inner computation, rather than repeatedly passing them as arguments to recursive calls

  2. Allowing the outer definition to be inlined to expose more optimisations (since GHC will generally not inline recursive functions).

Related