Learning Haskell: Functional Patterns

posted 2 years ago

Arguments and Parameters

As you already know if you've read my other article on currying, Haskell may appear to have multiple parameters but in reality all functions take one argument and return one result.

A function in Haskell is a first-class value; all Haskell values can be arguments to functions.

You name parameters to function by declaring them between the name of the function and the equals sign, separating the name from both the function name and the equals sign with white space.

Anonymous Functions

Anonymous means "without a name".

We can construct functions without giving them a name.

Here is an example of a named function vs an anonymous function:

double :: Integer -> Integer -- named
double x = x * 2

(\x -> x * 2) :: Integer -> Integer -- anonymous

Lambda Syntax Utility

You most often use this syntax when you're passing a function in as an argument to a higher-order function and that's the only place in your program where that particular function will be used.

If you're never going to call it, then it doesn't need to be given a name.

Pattern Matching

Pattern matching is a method of matching values against patterns and binding variables to successful matches.

Patterns can include things as diverse as undefined variables, numeric literals, and list syntax.

Pattern matching matches on any and all data constructors.

We can pattern match on numbers for example:

isItOne :: Integer -> Bool
isItOne 1 = True
isItOne _ = False

The underscore represents the default pattern if nothing else explicitly defined matches.

The order of pattern matches matters. Always put your default pattern at the end. Always order from most specific to least specific.

You can check your pattern matches at compile time to see if your pattern matches every case:

Prelude> :set -Wall
Prelude> :{
Prelude| let isItOne :: Integer -> Bool
Prelude|     isItOne 1 = True
Prelude| :}

<interactive>:8:5: warning: [-Wincomplete-patterns]
    Pattern match(es) are non-exhaustive
    In an equation for ‘isItOne’:
        Patterns not matched: p where p is not one of {1}

Pattern Matching Against Data Constructors

We can unpack and expose the contents of our data using pattern matching.

newtype is a special case of a data declaration; it is different from data in that it only permits one constructor and only one field.

Let's use it to focus on how pattern matching can be used to expose the contents of data and specify behavior based on that data:

module Users where

newtype Username
  = Username String

newtype AccountNumber
  = AccountNumber Integer

newtype Password
  = Password String

data User
  = UnregisteredUser
  | RegisteredUser Username AccountNumber Password

We can use pattern matching to accomplish two things.

User is a sum with two constructors: UnregisteredUser and RegisteredUser. We use pattern matching to cause the function to act differently depending on which value is passed.

With the RegisteredUser, we see that it is a product of three newtypes: Username, AccountNumber, and Password.

RegisteredUser is a function that constructs a User out of three arguments: Username, AccountNumber, and Password. This is why it's called a "data constructor".

Case Expressions

Case expressions are similar to if-then-else.

You can use case expressions with any datatype that has visible data constructors.

Consider Bool:

data Bool = False | True
--   [1]     [2]    [3]
  1. Type constructor
  2. Data constructor for False
  3. Data constructor for True

Let's write a case expression, matching on the data constructors of Bool:

f x =
  case x + 1 == 1 of
    True -> "Cool"
    False -> "Uncool"
Prelude> f 0
"Cool"

Higher-order Functions

Higher-order functions are functions that accept functions as arguments.

When we want to express a function argument within a function type, we must use parentheses to nest it.

Remember, that functions are values, and thus, can be used as arguments.

flip :: (a -> b -> c) -> b -> a -> c -- HOF
flip f x y = f y x

Guards

Guard syntax allows us to write compact functions that allow for two or more possible outcomes depending on the truth of the condition.

Let's rewrite a function to use guards:

myAbs :: Integer -> Integer -- return the absolute value of an Integer
myAbs i = if i < 0 then (-x) else x

myAbs :: Integer -> Integer
myAbs i -- guards
  | x < 0     = (-x)
  | otherwise = x

Each guard has its own equals sign.

| denotes a new guard case.

otherwise is another name for True, used here as a fallback case in case x < 0 is False.

Function Composition

Function composition is a type of higher-order function that allows us to combine functions such that the result of applying one function gets passed to the next function as an argument.

(.) is the function composition operator

(.) :: (b -> c) -> (a -> b) -> a -> c
--       [1]         [2]      [3]  [4]
  1. a function from b to c, passed as an argument
  2. a function from a to b
  3. value of type a
  4. value of type c

The basic syntax of function composition looks like this:

(f . g) x = f (g x)

This composition operator, takes two functions, named f and g. The f corresponds to (b -> c) and the g corresponds to (a -> b).

An example:

negate . sum $ [1, 2, 3, 4, 5]
-- -15

Pointfree Style

Pointfree style refers to a style of composing functions without specifying their arguments.

For example:

f . g = \x -> f (g x)

f . g . h = \x -> f (g (h x))

We can rewrite the example from earlier:

Prelude> let f = negate . sum
Prelude> f [1, 2, 3, 4, 5]
-15

It is important to remember that functions in composition are applied from right to left.

Sources

Case expressions and pattern matching - https://www.haskell.org/tutorial/patterns.html

The implementation of functional programming languages - http://research.microsoft.com/en-us/um/people/simonpj/papers/slpj-book-1987/index.htm

Point free program calculation - http://www4.di.uminho.pt/~mac/Publications/phd.pdf

Haskell Book - https://haskellbook.com/