Haskell

A primer

@xpktro - Functional Programming Perú

Original Version:

Por qué deberías aprender programación funcional ya mismo. - Andrés Marzal

Background: Constraint Based Design

    Mario Lavista
    La vida (a) leve
            Para Ulalume
    Y
    se
    oía
    Berg
    Dufai
    Mozart
    Debussy
    Schumann
    Beethoven
    Stravinski
    Lutoslavski:
    orquestarías
    estudiándolos
    brillantísimas
    extraordinarias
    transformaciones
    entremezclándoles
    pluridimensionales
    pseudododecafónicas
    sobreornamentaciones
    hipermeldessonhnianas.

OuLiPo ('60)

Literature based on very aggressive constraints

  • Palindromes
  • Absence of letters
  • Fixed or varying size of words
  • Specific order of rhymes
  • Timespan restrictions (e.g. the time you take when using the bus)

Proposal: OuProgPo

  • No loops (for, while, do, ...)
  • All functions must be declared with only one argument
  • Each function must consist of only one expression
  • No side-effects (don't change anything outside an expression)
  • All functions must produce the same result when given the same parameter
  • Once an identifier is given a value, it can't be changed
  • Data structure operations doesn't alter the original data structure
  • Execution order of expressions doesn't matter

Introducing Functional Programming

  • No loops (for, while, do, ...)
  • All functions must be declared with only one argument
  • Each function must consist of only one expression
  • No side-effects (don't change anything outside an expression)
  • All functions must produce the same result when given the same parameter
  • Once an identifier is given a value, it can't be changed
  • Data structure operations doesn't alter the original data structure
  • Execution order of expressions doesn't matter

Sales Pitch

More and more functional programming features are being included in common programming languages.

  • Lambda functions
  • List comprehension
  • Higher order functions
  • Lazy evaluation
  • Pattern matching
  • Immutability
  • Map/Filter/Reduce

Mathematic Foundation

Functional programming is based on two mathematical principles:

Lambda Calculus

Category Theory

$$ \lambda x.x^{2} $$

$$ (\lambda x.x^{2}) \; 2 = 4 $$

Lambda Calculus

A formal system for representing and operating functions

$$ \lambda x.x^{2} $$

$$ (\lambda x.x^{2}) \; 2 = 4 $$

$$ \lambda (x, y).x^{2}+y^{2} $$

$$ \lambda x. \lambda y.x^{2}+y^{2} $$

$$ \lambda x.(\lambda y.x^{2}+y^{2}) $$

$$ (\lambda x.(\lambda y.x^{2}+y^{2})) \; 4 = \lambda y.16 + y^{2} $$

 

$$ (\lambda y.16+y^{2}) \; 3 = 25 $$

$$ (\lambda x.\lambda y.x^{2}+y^{2}) \; 4 \; 5= 25 $$

 

Pure functional features in Haskell

Haskell is the most usable language that includes pure functional features:

  • Referential transparency
  • No side effects, syntax support for monads
  • Lazy evaluation
  • Currying, first class functions
  • Static type system and type inference
  • Tail call optimization
  • Algebraic data types
  • Pattern matching
  • Immutable data structures

Practical Approach: FizzBuzz

A classic programming exercise: write a program which prints the numbers from 1 to 100. When a multiple of 3 is reached, print "fizz!" instead, and for multiples of 5 print "buzz!"; for multiples of 5 and 3 print "fizzbuzz!".

one!
two!
fizz!
four!
buzz!
fizz!
seven!
eight!
fizz!
buzz!
eleven!
fizz!
thirteen!
fourteen!
fizzbuzz!
sixteen!
seventeen!
fizz!
nineteen!
buzz!
fizz!
twenty two!

Practical Approach: FizzBuzz

fizzbuzz :: Int -> String
fizzbuzz n = undefined

"undefined" is a value that derives from every type

fizzbuzz :: Int -> String
fizzbuzz n = "one!"

main = print (fizzbuzz 1)

"main" is a special function that is called when your program is executed (the entry point)

Practical Approach: FizzBuzz

fizzbuzz n = if n == 1 then "one!" else "two!"

main = print (fizzbuzz 2)

"if" is an expression, it must return a value. We can also define our own if function:

ifThenElse :: Bool -> a -> a -> a
ifThenElse cond thenVal elseVal =
  case cond of
    True -> thenVal
    False -> elseVal

fizzbuzz n = ifThenElse (n==1) ("one"++"!") ("two"++"!")

Practical Approach: FizzBuzz

fizzbuzz :: Int -> String
fizzbuzz n | n == 1 = "one!"
fizzbuzz n | n /= 1 = "two!"

Refactoring time! introducing guards:

Being that the second condition will always succeed for n != 1, we can write it like this:

fizzbuzz n | n == 1 = "one!"
           | True = "two!"

-- Or: 

fizzbuzz n | n == 1 = "one!"
           | otherwise = "two!"

Practical Approach: FizzBuzz

fizzbuzz :: Int -> String
fizzbuzz 1 = "one!"
fizzbuzz n = "two!"

When guards are of the form "n == something", they can be written in a cleaner way:

And if a parameter is not important in a guard, we can replace it with _

fizzbuzz :: Int -> String
fizzbuzz 1 = "one!"
fizzbuzz _ = "two!"

Practical Approach: FizzBuzz

lessThan20 :: Int -> String
lessThan20 n
  | n > 0 && n < 20 =
    let answers = words ("one two three four five six seven eight nine ten " ++
                         "eleven twelve thirteen fourteen fifteen sixteen " ++
                         "seventeen eighteen nineteen")
    in answers !! (n-1)

In english, numbers that are less than 20 have proper names, let's make a function for representing those:

words :: String -> [String]

Practical Approach: FizzBuzz

tens :: Int -> String
tens n
  | n >= 2 && n <= 9 =
    answers !! (n-2)
  where
    answers = words "twenty thirty forty fifty sixty seventy eighty ninety"

We also need a function for getting names for tens:

"let in" and "where" are interchangeable and used depending on code legibility

Practical Approach: FizzBuzz

number :: Int -> String
number n
  | 1 <= n && n < 20           = lessThan20 n
  | n `mod` 10 == 0 && n < 100 = tens (n `div` 10)
  | n < 100                    = tens (n `div` 10) ++ " " ++ lessThan20 (n `mod` 10)
  | n == 100                   = "one hundred"

Combining our two functions into one:

Guards become an instant useful and elegant construct for grouping a complex set of conditions

Practical Approach: FizzBuzz

main = putStr (unlines (map number [1..30]))

Checking what we have so far by printing it:

unlines :: [String] -> String
map :: (a -> b) -> [a] -> [b]

-- For example:
map (\x -> x * 2) [1,2,3]
-- [2,4,6]

"unlines" joins a string using the newline character

"map" takes a function and applies it to every element of a list

Practical Approach: FizzBuzz

fizzbuzz :: Int -> String
fizzbuzz n
  | n `mod` 3 == 0 && n `mod` 5 == 0 = "fizzbuzz!"
  | n `mod` 3 == 0                   = "fizz!"
  | n `mod` 5 == 0                   = "buzz!"
  | otherwise                        = number n ++ "!"

main = putStr (unlines (map fizzbuzz [1..100]))

Final refactor, showing fizz! and buzz! and fizzbuzz!

one!
two!
fizz!
four!
buzz!
fizz!
seven!
eight!
fizz!
buzz!
eleven!
fizz!
thirteen!
fourteen!
fizzbuzz!
sixteen!
seventeen!
fizz!
nineteen!
buzz!
fizz!
twenty two!
twenty three!
fizz!
buzz!
twenty six!
fizz!
twenty eight!
twenty nine!
fizzbuzz!
thirty one!
thirty two!
fizz!
thirty four!
buzz!
fizz!
thirty seven!
thirty eight!
fizz!
buzz!
forty one!
fizz!
forty three!
forty four!
fizzbuzz!
forty six!
forty seven!
fizz!
forty nine!
buzz!
fizz!
fifty two!
fifty three!
fizz!
buzz!
fifty six!
fizz!
fifty eight!
fifty nine!
fizzbuzz!
sixty one!
sixty two!
fizz!
sixty four!
buzz!
fizz!
sixty seven!
sixty eight!
fizz!
buzz!
seventy one!
fizz!
seventy three!
seventy four!
fizzbuzz!
seventy six!
seventy seven!
fizz!
seventy nine!
buzz!
fizz!
eighty two!
eighty three!
fizz!
buzz!
eighty six!
fizz!
eighty eight!
eighty nine!
fizzbuzz!
ninety one!
ninety two!
fizz!
ninety four!
buzz!
fizz!
ninety seven!
ninety eight!
fizz!
buzz!

Practical Approach: Bowling Scoring

For this demonstration, we'll be scoring a bowling game:

Frame Pins Kind Bonus Score Total
1 1, 4 Open 1+4=5 0+5=5
2 4, 5 Open 4+5=9 5+9=14
3 6, 4 Spare 5 6+4+5=15 14+15=29
4 5, 5 Spare 10 5+5+10=20 29+20=49
5 10 Strike 0, 1 10+0+1=11 49+11=60
6 0, 1 Open 0+1=1 60+1=61
7 7, 3 Spare 6 7+3+6=16 61+16=77
8 6, 4 Spare 10 6+4+10=20 77+20=97
9 10 Strike 2, 8 10+2+8=20 97+20=117
10 2, 8 Spare 6 2+8+6=16 117+16=133

Practical Approach: Bowling Scoring

We'll receive a list of knocked-down pins for every throw: 

[1, 4, 4, 5, 6, 4, 5, 5, 10, 0, 1, 7, 3, 6, 4, 10, 2, 8, 6]

We'll create a Frame data type to represent a frame and scoring these throws

toFrames :: [Int] -> [Frame]
score :: [Frame] -> Int

Practical Approach: Bowling Scoring

An Algebraic Data Type is defined with all it's possible values, each separated with a | character

data Frame = Open Int Int
  | Spare Int Int
  | Strike Int Int
  
toFrames :: [Int] -> [Frame]
toFrames pins = undefined
data Frame = Open   { pins1 :: Int
                    , pins2 :: Int
                    }
           | Spare  { pins1 :: Int
                    , bonus1 :: Int
                    }
           | Strike { bonus1 :: Int
                    , bonus2 :: Int
                    }

Practical Approach: Bowling Scoring

A class is a set of behaviors (functions) that any data type can implement (be a instance of)

class Eq a where
  (==) :: a -> a -> Bool
  x == y = not (x /= y)

  (/=) :: a -> a -> Bool
  x /= y = not (x == y)
instance Eq Frame where
  Open x y == Open x' y'   = x == x' && y == y'
  Spare x y == Open x' y'  = x == x' && y == y'
  Strike x y == Open x' y' = x == x' && y == y'
  _ == _                   = False

Practical Approach: Bowling Scoring

Haskell compiler can actually generate code for some common class derivations

data Frame = Open Int Int
           | Spare Int Int
           | Strike Int Int
           deriving (Eq, Show)
instance Show Frame where
  show (Open x y)   = "Open " ++ show x ++ " " ++ show y
  show (Spare x y)  = "Spare " ++ show x ++ " " ++ show y
  show (Strike x y) = "Strike " ++ show x ++ " " ++ show y

Practical Approach: Bowling Scoring

We match the argument of our function with the form (first : second : rest)

toFrames :: [Int] -> [Frame]
toFrames (x:y:ys) = Open x y : toFrames ys
toFrames []       = []

":" is the constructor operator for lists:

(:) :: a -> [a] -> [a]
-- [1, 2, 3] == 1:2:3:[]

Practical Approach: Bowling Scoring

Let's consider the concept of Spare

toFrames (x:y:z:ys)
  | x+y == 10   = Spare x z : toFrames (z:ys)
  | otherwise   = Open x y : toFrames (z:ys)
toFrames [x, y] = [Open x y]
toFrames []     = []

Here we're using guards again and add a new definition for 2 element lists.

Practical Approach: Bowling Scoring

When a Spare is made at the final frame, an additional throw must be made, so we need to track the current frame number.

toFrames pins = go 1 pins
  where
    go 10 [x, y]    = [Open x y]
    go 10 [x, y, z]
      | x + y == 10 = [Spare x z]
    go n (x:y:z:ys)
      | x + y == 10 = Spare x z : go (n+1) (z:ys)
      | otherwise   = Open x y  : go (n+1) (z:ys)

Practical Approach: Bowling Scoring

Finally, the Strike type is considered:

toFrames :: [Int] -> [Frame]
toFrames pins = go 1 pins
  where
    go 10 [x, y]    = [Open x y]
    go 10 [x, y, z]
      | x     == 10 = [Strike y z]
      | x + y == 10 = [Spare x z]
    go n (x:y:z:ys)
      | x     == 10 = Strike y z : go (n+1) (y:z:ys)
      | x + y == 10 = Spare x z  : go (n+1) (z:ys)
      | otherwise   = Open x y   : go (n+1) (z:ys)

Practical Approach: Bowling Scoring

Something is missing, what is we run out of numbers of if a part of them is inconsistent?

toFrames :: [Int] -> Maybe [Frame]

"Maybe" is a type used to represent 'optional' results:

data Maybe a = Just a
             | Nothing

Practical Approach: Bowling Scoring

Making our function return a "Maybe" value:

toFrames :: [Int] -> Maybe [Frame]
toFrames pins = go 1 pins
  where
    go 10 [x, y]
      | x + y < 10 = Just [Open x y]
      | otherwise  = Nothing
    go 10 [x, y, z]
      | x == 10     = Just [Strike y z]
      | x + y == 10 = Just [Spare x z]
      | otherwise   = Nothing
    go n (x:y:z:ys)
      | x == 10     = fmap (Strike y z :) (go (n+1) (y:z:ys))
      | x + y == 10 = fmap (Spare x z  :) (go (n+1) (z:ys))
      | x + y < 10  = fmap (Open x y   :) (go (n+1) (z:ys))
      | otherwise   = Nothing
    go _ _ = Nothing

Practical Approach: Bowling Scoring

Finally, our scoring function is made:

frameScore :: Frame -> Int
frameScore (Open x y)   = x + y
frameScore (Spare _ y)  = 10 + y
frameScore (Strike x y) = 10 + x + y

score :: [Frame] -> Int
score frames = sum (map frameScore frames)

main = print (score frames)
  where 
    Just frames = toFrames throws
    throws      = [1, 4, 4, 5, 6, 4, 5, 5, 10, 0, 1, 7, 3, 6, 4, 10, 2, 8, 6]

-- > 133

What now?

Many things were left behind for you to research:

  • Monads / IO
  • Function composition
  • Functors
  • $
  • List comprehensions
  • Lambdas

?

Haskell: A primer

By Moisés Gabriel Cachay Tello

Haskell: A primer

An introduction to Haskell, based on "Por qué deberías aprender programación funcional ya mismo", a talk from Andrés Marzal.

  • 842