05-Pattern-Matching-in-Functions.purs

module Syntax.Basic.PatternMatching where

import Prelude

-- Given a data type like this:
data Fruit
  = Apple
  | Orange
  | Banana
  | Cherry
  | Tomato -- because why not!?

-- Pattern Matching: Basic idea and order of matching
mkString :: Fruit -> String {-
         if the arg is _ = then return _ -}
mkString Apple           = "apple"

{-  else if the arg is _ = then return _ -}
mkString Orange          = "orange"

{-  else if the arg is _ = then return _ -}
mkString Banana          = "banana"

{-  else if the arg is _ = then return _ -}
mkString Cherry          = "cherry"

{-  else if the arg is _ = then return _ -}
mkString Tomato          = "tomato"

-- The above pattern match is "exhaustive" because there are no other
-- Fruit values against which one could match.

-- Pattern Matching: Literal values and catching all values

literalValue :: String -> String
literalValue "a" = "Return this string if arg is 'a'"
literalValue "b" = "Return this string if arg is 'b'"
literalValue "c" = "Return this string if arg is 'c'"
literalValue _   = "ignore input and return this default value"

-- syntax sugar for pattern-matching literal arrays
array :: Array Int -> String
array []           = "an empty array"
array [0]          = "an array with one value that is 0"
array [0, 1]       = "an array with two values, 0 and 1"
array [0, 1, a, b] = "an array with four values, starting with 0 and 1 \
                       \ and binding the third and fouth to names 'a' and 'b'"
array [-1, _ ]     = "an array of two values, '-1' and another value that \
                       \ will not be used in the body of this function."
array _            = "catchall for arrays. This is needed to make this \
                       \ example compile"


-- Pattern Matching: Unwrapping Data Constructors
data A_Type
  = AnInt Int
  | Outer A_Type -- recursive type!
  | Inner Int

f :: A_Type -> String {-
-- Syntax
f patternMatch = bodyToRunIfPatternWasMatched

  where 'patternMatch' is:
    - literal value
    - DataConstructorWithNoArgs
    - (DataConstructor withArgBoundToThisBinding)
    - (DataConstructor "with arg whose value is this literal value")
    - bindingForEntireValue@(literalValue)
    - bindingForEntireValue@(DataConstructorWithNoArgs)
    - bindingForEntireValue@(DataConstructor withArgBoundToThisBinding)
    - bindingForEntireValue@(DataConstructor "with arg whose value is this literal value")

-- Example

f the pattern match     = description of what was matched -}
f (Inner 0)             = "a value of type Inner whose value is 0"
f (Inner int)           = "a value of type Inner, binding its value to 'int' \
                          \name for usage in function body"
f (Outer (Inner int))   = "a value of type Outer, whose Inner value is bound \
                          \to `int` name for usage in function body"
f object@(AnInt 4)      = "a value of type AnInt whose value is '4', \
                          \binding the entire object to the `object` name for \
                          \usage in function body"
f _                     = "ignores input and matches everything; \
                          \acts as a default / catch all case"

-- Pattern Matching: Regular Guards
g :: Int -> Int -> String {-
g x y | condition1  = return this if condition1 is true
      | condition2  = return this if condition2 is true
      | ...         = ...
      | conditionN  = return this if conditionN is true
      | otherwise   = default case-}
g x y | x + y == 0 = "x == -y"
      | x - y == 0 = "x == y"
      | x * y == 0 = "x == 0 || y == 0"
      | otherwise = "some other value"

-- Pattern Matching: Single and Multiple Guards
h :: Int -> Int -> String
h x y | x == 4 && y == 5 = "body"

   -- ... same as...
   -- | x == 4 && y == 6 = "body"`
      | x == 4, y == 6   = "body"
   
   -- | condition1, condition2 = body
      | x == 4, y == 6   = "body"

  {-  ... or when using syntax sugar...
      | condition1
      , condition2 = body -}
      | x == 3
      , y == 2           = "body"

-- It's wise to separate mulitple guards with a blank line for readability.
      | otherwise        = "default"

-- Pattern Matching: Single Pattern Guard
j :: Int -> String {-
j x | returnedValue <- function arg1 arg2 argN = body if match occurs -}
j x | (Box 2) <- toBox x = "Calling toBox x returned a Box with 2 inside of it"
    | (Box y) <- toBox x = concat "The 'y' value was: " (toString y)

-- Pattern Matching: Multiple Pattern Guards
p :: Int -> Int -> String {-
p x y | returnedValue1 <- functionCall1, returnedValue2 <- functionCall2 = body -}
p x y | (Box 2) <- toBox x, (Box 3) <- toBox (x - 1) = "without syntax sugar"

 {-   ... or for easier reading, there is sugar syntax:
p x y | returnedValue1 <- functionCall1
      , returnedValue2 <- functionCall2 = body -}
      | (Box a) <- toBox x
      , (Box b) <- toBox (x * 2) = "with syntax sugar"

      | otherwise = "some other value"

-- Different guards can be mixed:
q :: Int -> Int -> String
q x y | x == 3                   = "3"
      | x == 5, y == 5           = "5"
      | (Box 2) <- toBox x       = "2?"

      | (Box 2) <- toBox x
      , y == 4                   = "curious, no?"

      | (Box a) <- toBox x
      , (Box b) <- toBox (y * 2) = "something?"

      | otherwise                = "catch-all"

-- necessary for this to compile
data Box a = Box a

toBox :: Int -> Box Int
toBox 1 = Box 2
toBox _ = Box 0

concat :: String -> String -> String
concat left right = left <> right

toString :: forall a. Show a => a -> String
toString = show