Home > front end >  Pattern match on guard expression
Pattern match on guard expression

Time:11-13

Suppose I have some very simple code:

import qualified Data.Map as Map
import Data.Maybe(fromJust)

keySum :: Map.Map Char Int -> Char -> Char -> Either String Int
keySum m key1 key2
  | val1 == Nothing = Left $ show val1    " not in map"
  | val2 == Nothing = Left $ show val2    " not in map"
  | otherwise       = Right $ val1'   val2'
  where 
    val1 = Map.lookup key1 m
    val2 = Map.lookup key2 m
    val1' = fromJust val1
    val2' = fromJust val2

Certainly, I don't want to use fromJust, and would rather pattern match on the result of Map.lookup here.

But how can I achieve this, without evaluating Map.lookup twice or passing it through some wrapper function?

CodePudding user response:

You can use a pattern guard:

keySum m key1 key2
  | Just val1 <- Map.lookup key1 m
  , Just val2 <- Map.lookup key2 m
               = Right $ val1   val2
  | key1 `Map.notMember` m
               = Left $ show key1    " not in map"
  | otherwise  = Left $ show key2    " not in map"

But perhaps a good old case is actually the cleaner option:

keySum m k₀ k₁ = case (`Map.lookup`m)<$>[k₀,k₁] of
     [Just v₀, Just v₁] -> Right $ v₀   v₁
     [Nothing, _      ] -> Left $ show k₀    " not in map"
     _                  -> Left $ show k₁    " not in map"

That can be simplified further by observing that it's just a standard applicative chain:

keySum m k₀ k₁ = ( ) <$> lku k₀ <*> lku k₁
 where lku k = case Map.lookup m k of
         Just v -> Right v
         Nothing -> Left $ show k    " not in map"

If you're into patterns, this can also be written thus (personally I'm not such a fan of this extension):

{-# LANGUAGE ViewPatterns     #-}

keySum m k₀ k₁ = ( ) <$> lku k₀ <*> lku k₁
 where lku (Map.lookup m -> v) = Right v
       lku k = Left $ show k    " not in map"

And finally, there's no reason to restrict this to only two keys, might as well sequence over a whole arbitrary-length list. Incidentally, there's no reason to pick concrete types.

import Data.Traversable (forM)

keySum :: (Ord a, Show a, Num b) => Map.Map a b -> [a] -> Either String b
keySum m ks = sum <$> forM m `id` \k -> case Map.lookup m k of
         Just v -> Right v
         Nothing -> Left $ show k    " not in map"
  • Related