Thursday, April 07, 2022

[xcruhlyr] first-class pattern guards in Haskell

patterns are not first class objects in Haskell.  they cannot be assigned to variables nor passed around.

in the function f1 below, the patterns Apple and Banana are hardcoded.

data Fruit = Apple | Banana | Orange ;

f1 :: Fruit -> String;
f1 Apple = "got first choice fruit";
f1 Banana = "got second choice fruit";
f1 _ = "did not get what we want";

however, unlike patterns, pattern guards can be first-class objects.  using them, we can accomplish anything a first-class pattern could do.  in the example below, we pass patterns as boolean predicates to f2 and call them in the pattern guards (to the right of the vertical bar).  applep, bananap, and orangep are patterns turned into boolean functions.

f2 :: (Fruit -> Bool) -> (Fruit -> Bool) -> Fruit -> String;
f2 pattern1 pattern2 fruit
| pattern1 fruit = "got first choice fruit" -- note: no semicolon here
| pattern2 fruit = "got second choice fruit";
f2 _ _ _ = "did not get what we want";

applep :: Fruit -> Bool;
applep Apple = True;
applep _ = False;

bananap :: Fruit -> Bool;
bananap Banana = True;
bananap _ = False;

orangep :: Fruit -> Bool;
orangep Orange = True;
orangep _ = False;

examplef2 :: Fruit -> IO();
examplef2 fruit = do {
putStrLn $ f2 applep bananap fruit;
putStrLn $ f2 orangep applep fruit;

we can also do pattern matching, encapsulating extraction of a value from a pattern into a (first-class) function returning Maybe.  below, the function superhero calls its supplied pattern and attempts to match against Just.  if the pattern returns Nothing, the guard fails, and we fall through to "muggle".

data Health = Mind Int | Body Int;

superhero :: (Health -> Maybe Int) -> Health -> String;
superhero pattern health | Just power <- pattern health = if power > 9000
  then "is superhero"
  else "not strong enough";
superhero _ _ = "muggle";

getmind :: Health -> Maybe Int;
getmind (Mind i) = Just i;
getmind _ = Nothing;

getbody :: Health -> Maybe Int;
getbody (Body i) = Just i;
getbody _ = Nothing;

powermeter :: Health -> IO();
powermeter health = do {
putStrLn $ superhero getmind health;
putStrLn $ superhero getbody health;

in general, because what we pass is a function, we can build up a pattern guard in the myriad of ways we can build a function in a functional programming language.  however, I have not explored this very far.

a pattern guard can have several components, separated by commas.  a comma acts like the boolean AND operator.  each component can be a boolean expression, a pattern match (its boolean value is whether the match succeeds), or an assignment of a local variable with "let" (always evaluates to True).

note well: pattern matching in a guard is different from pattern matching in a let in a guard.  unlike the definition above, the following always succeeds, never falling through to "muggle".  if pattern returns Nothing, then a run-time error "Non-exhaustive patterns" occurs.

superhero pattern health | let { Just power = pattern health } = if power ...

No comments :