Chap 4 | Syntax in Functions

learnyouahaskell.com

Pattern matching

With pattern matching, you can check if a data conforms some specification and deconstruct the data. A function can have several bodies for different patterns. The below function checks if its parameter is included in [1, 3]. If the parameter is 1, 2, or 3, it returns "One", "Two", or "Three" respectively. If not, it returns "Not in [1, 3]".

showInt :: Integral a => a -> String
showInt 1 = "One"
showInt 2 = "Two"
showInt 3 = "Three"
showInt x = "Not in [1, 3]"

The concept of pattern matching is pretty like the switch statement in C-like languages. An argument is checked against the pattern from top to bottom. If the argument conforms to a pattern, the function body corresponding to that pattern is used and following patterns and function bodies are all ignored just like switch statements with break keywords. If the argument doesn't conform to the first 3 specific patterns, it falls to the last pattern and is bound to x.

We can define factorial function like below. Here, had we interchange the two patterns, the factorial 0 = 1 pattern would be never used and this recursive function never stops.

factorial :: Integral a => a -> a
factorial 0 = 1
factorial n = n * factorial (pred n)

If we don't place the all-catch, general pattern at the end, the function may fail. An argument that doesn't conform to any specific patterns isn't caught throughout such a function. If a function without "all-catcher" is called with an argument that doesn't correspond to any patterns in that function, it doesn't know what to do with it and raises a runtime error.

Patterns are also used with tuples. We can define a function that takes 2 2D vectors (pairs) and adds them up.

addVectors :: Num a => (a, a) -> (a, a) -> (a, a)
addVectors (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)

ghci> addVectors (1,0) (0,1)
(1,1)

We can define our original fst function as below.

myFst :: (a, b) -> a
myFst (a, _) = a

myFst takes a tuple that comprises of any type of values and returns the first one of the pair. As we don't care about the type of the tuple members, we wrote the type declaration as (a, b) -> a, which means it takes a tuple of any type and returns a value whose type is the same as the first value of the tuple. Since we don't care about the second member of the tuple, the pattern says (a, _). _ in a pattern means that the value that is bound to that is not used in the body there.

As for lists, we can extract items or sublists with :.

myHead :: [a] -> a
myHead [] = error "Can't call myHead on an empty list."
myHead (x:_) = x

ghci> myHead "hello"
'h'
ghci> myHead [1,2,3,4]
1

--

myTail :: [a] -> [a]
myTail [] = error "Can't call myTail on an empty list."
myTail (_:x) = x

ghci> myTail "hello"
"ello"
ghci> myTail [1,2,3,4]
[2,3,4]

Notice that we have to surround patterns with parentheses.

We can write a function that takes a list of any type of typeclass Show and tells something about it.

tell :: Show a => [a] -> String
tell [] = "The list is empty"
tell (x:[]) = "The list has one element: " ++ show x
tell (x:y:[]) = "The list has two elements: " ++ show x ++ " and " ++ show y
tell (x:y:_) = "This list is long. The first two elements are: " ++ show x ++ " and " ++ show y

As [1,2,3] is just syntactic sugar for 1:2:3:[], the second and third patterns can also be written as below.

tell [x] = "The list has one element: " ++ show x
tell [x,y] = "The list has two elements: " ++ show x ++ " and " ++ show y

Be careful we cannot replace (x:y:_) with [x,y,_] or something like that.

There is also a thing called as patterns. With that, we can split a function's parameters into some components like above while keeping a reference to the whole parameters. For example, by prefixing a pattern with all@ like below, you can get a reference all that points to the whole parameters (whole string in this example).

capital :: String -> String
capital "" = "Empty string"
capital all@(x:_) = "The first letter of '" ++ all ++ "' is '" ++ [x] ++ "'"

Guards, guards!

While patterns are conditional branching in a function's signature, guards are that in a function body. Patterns make sure that parameters obey some form and deconstruct them. On the other hand, guards execute tests for the deconstructed values and then, you can do something according to that results.

bmiTell :: RealFloat a => a -> a -> String
bmiTell weight height
    | weight / height ^ 2 <= 18.5 = "You're underweight"
    | weight / height ^ 2 <= 25.0 = "You're normal"
    | weight / height ^ 2 <= 30.0 = "You're fat"
    | otherwise                   = "You're fucking awesome"

ghci> bmiTell 60 1.7
"You're normal"

The function bmiTell takes 2 RealFloat numbers, calculates a BMI, and then, tell something based on the data. Conditions are written between | and = like above. Like switch statements in C-like languages, conditions are evaluated from top to bottom and if one is evaluated as True, expressions following = are returned from the function. The last guard is often otherwise, which is simply evaluated as True and catches everything.

Guards check specific conditions of parameters while patterns check if parameters meet a function's signature. If all guards are evaluated as False, the operation falls to the next pattern. If no suitable function body is found throughout all patterns and all guards, an error is thrown.

Where!?

The previous code is not DRY. We can solve this problem with where.

bmiTell :: RealFloat a => a -> a -> String
bmiTell weight height
    | bmi <= 18.5 = "You're underweight"
    | bmi <= 25.0 = "You're normal"
    | bmi <= 30.0 = "You're fat"
    | otherwise   = "You're fucking awesome"
    where bmi = weight / height ^ 2

Variables defined inside where clause are valid across all guards but not shared among patterns. You can define as many variables as you like.

bmiTell :: RealFloat a => a -> a -> String
bmiTell weight height
    | bmi <= skinny = "You're underweight"
    | bmi <= normal = "You're normal"
    | bmi <= fat    = "You're fat"
    | otherwise     = "You're fucking awesome"
    where bmi    = weight / height ^ 2
          skinny = 18.5
          normal = 25.0
          fat    = 30.0

You also can bind some components of values as we have done with patterns.

initials :: String -> String -> String
initials firstname lastname = [f] ++ ". " ++ [l] ++ "."
    where (f:_) = firstname
          (l:_) = lastname

We could do the same thing as above with function parameters pattern matching and many times that is a better way (this is just a demo). Functions can also be defined in where clause.

calcBmis :: RealFloat a => [(a, a)] -> [a]
calcBmis xs = [bmi w h | (w, h) <- xs]
    where bmi weight height = weight / height ^ 2

ghci> calcBmis [(50, 1.7), (60, 1.7), (70, 1.7)]
[17.301038062283737,20.761245674740486,24.221453287197235]

where can be nested. It's a common idiom to define a function with helper functions, which also have several helper functions too.

Let it be

Let bindings are expressions with in which you can bind values to variables. Let bindings are used like:

let var1 = val1
    var2 = val2
in  var1 + var2

In the example above, we assume that val1 and val2 are the same type which can be added. You can define functions in the let part:

ghci> [let square x = x * x in (square 5, square 3, square 2)]
[(25,9,4)]

Let can also be put inside list comprehensions.

calcBmis :: RealFloat a => [(a, a)] -> [a]
calcBmis xs = [bmi | (w, h) <- xs, let bmi = w / h ^ 2]

Case expressions

case expression of pattern -> result
                   pattern -> result
                   pattern -> result
                   ...

Pattern matching on parameters in function definitions are just syntactic sugar for case expressions. The two codes below are equivalent and interchangeable.

myHead :: [a] -> a
myHead [] = error "Can't call myHead on an empty list."
myHead (x:_) = x
myHead :: [a] -> a
myHead xs = case xs of [] -> error "Can't call myHead on an empty list."
                       (x:_) -> x