Skip to content

Instantly share code, notes, and snippets.

@LOZORD
Last active April 9, 2016 23:44
Show Gist options
  • Save LOZORD/9d23396e4206a5b1cb02 to your computer and use it in GitHub Desktop.
Save LOZORD/9d23396e4206a5b1cb02 to your computer and use it in GitHub Desktop.
Introduction to Functional Programming with Haskell

Introduction to Functional Programming with Haskell

By Leo Rudberg, for the UPL Video Lecture Series

Preceding slideshow

Setup

First, let's make sure that you have ghc and ghci installed:

$ ghc --version && ghci
# ghci should boot up...

If that doesn't work, you need to download the Haskell platform.

Your REPL should be ready to roll. Let's do the classic "Hello world":

Prelude> putStrLn "Hello world!"

Math and Boolean

Enter the following into your ghci REPL and observe the results

1 + 2
1 - 2
(*) 2 3 -- operators are just functions (see below)
(/ 16) 2 -- partial application
7 `mod` 4
(mod 4) 5
True || False
False && True
(not ((> 4) 3)) && (3 >= 3)

Strings, Lists, and Tuples

'c' -- a char
['s', 't', 'r', 'i', 'n', 'g'] == "string"

["this is a list", "of", "THINGS!"]
[] -- an empty list
-- NOTE: all elements of a list should be the same type

-- lists can be ranges
[1..5]
['a'..'z']
-- they can also be infinite!
[1..]

-- to get an element from a list
3 == [1..5] !! 2

-- tuples are fixed-sizes groupings
("like", "pairs")
('x', 'y', 'z')
(1, 3, ["foo", "bar"], True, ("UPL", 2))
(1, 2) -- not the same as [1, 2]

Lambdas

-- basic syntax
let doubler = (\ n -> n * 2)
let adder   = (\ a b -> a + b) -- two arguments

Lambdas are anonymous functions, nothing special! If you've used scripting languages like Ruby, Python, or JavaScript, you've probably used a lambda, but called it a block or closure. They can be used as arguments to other functions like map.

Basic Syntax

-- two dashes is a comment

{-
  multi
  line
  comment
-}

if 4 < 5 then ":)" else ":("
if 5 >= 6 then ":(" else if 3 < 2 then ">:(" else "8|"
(4 + (2 * 3)) == 4 + $ 2 * 3 -- only works in ghc (NOT ghci)

mod 3 4 == 3 `mod` 4 -- infix operators

-- FUNCTION DECLARATIONS
-- the following are equivalent
let add x y = x + y -- in ghci
add x y = (+) x y -- in ghc
add = (+)
add = (\ x y -> x + y) -- a lambda expression (see below)

(add 4) 5 == add 4 5 -- no parentheses or commas!

Partial Application

All functions in Haskell are functions of only one argument.

When you call a function with "all arguments", it returns a value. If you call a function with less than all of its parameters, then you get a curried function.

So, the following are equivalent:

let tripler n = n * 3
-- equivalent to
let tripler = (3 *)
-- equivalent to
let tripler = (*) 3

A useful example

take 2 (map tripler [1..]) == [3, 6]

Let's look at the left hand side:

(map tripler [1..])

This maps all positive integers (1 to Infinity) using tripler. In other words, for every element n in the infinite list [1..], tripler is applied to it to get 3 * n. Thus (map tripler [1..]) gives us [3 * 1, 3 * 2, 3 * 3, ...].

We can work with infinite lists in Haskell because it is lazy -- it only gives you what you need when you need it, and never earlier! Read more about Haskell's laziness here.

take 2 (map tripler [1..])

The take 2 part of this retrieves the first two elements from this infinite list. In this case, we get [3, 6].

ghci syntax

See this article for more on ghci syntax.

Starter Haskell code (for ghc)

Note: do is a special sugar syntax over monads. Think of it as a way of forcing Haskell to do IO actions in a specific order. Remember that performing I/O in your functions will make them impure, and impurity makes your life less easy. In Haskell, you want to minimize I/O: your main function should, optimally, be the only function performing I/O. All other functions should be running pure, mathematical computation over that unwrapped ("un-I/O'd") input.

Basic Single I/O

{-  the thing directly below these comments is a type signature
    it helps make your code clearer for compilers and humans alike!
    this type signature says:
    "foo is a function that takes a String and returns an Integer"
-}
foo :: String -> Integer
foo s = (length s) * 1000

-- main is a function that takes no (code) arguments and performs IO
main :: IO ()
main =
  do
    let result = foo "do something"
    putStrLn (show result) -- show is like "toString"
    return ()

Basic Arg-to-Ouput

import System.Environment (getArgs)

-- three string arguments, one integer return value
bar :: String -> String -> String -> Integer
bar a1 a2 an = (length (a1 ++ a2)) * (length $ maximum [a1, a2, an])

baz :: [String] -> Integer
baz args = 100 - (count args)

main :: IO ()
main =
  do
    args <- getArgs -- "<-" means "perform impure function, and fetch result"
    let [arg1, arg2, argN] = args -- yay pattern matching! here, we're forcing 3 args
    let combinedResult = (bar arg1 arg2 argN) * (baz args)
    putStrLn (show combinedResult)
    return ()

Basic Interactive Loop

quux :: String -> Bool
quux str = 'q' `elem` str

loop = do
  putStrLn "Input:"
  input <- getLine
  let result = quux input
  putStrLn (show result)
  loop

main = do
  loop

During the talk, Mike Tolly created this great explanation of Haskell types:

-- Haskell datatypes are made up of "constructors",
-- different ways to construct or make that datatype.

-- Here is a type with 2 constructors:
data Bool = False | True
-- (this definition is built-in, you don't need to put it in your code.)
-- This definition makes
-- 1. a type, called Bool
-- 2. a value, False, of type Bool, written: False :: Bool
-- 3. a value, True, of type Bool, written: True :: Bool

-- Constructors are just values (or functions), with some extra features.
-- Both type names and constructors start with capital letters.
-- (Normal variables and "type variables" (shown later) start with lowercase letters.)

-- "Pattern matching" is like a super-powered "switch" statement.
-- Each line of the match specifies *constructors* to match on.

-- Let's define the logical AND function for Bool.
myand :: Bool -> Bool -> Bool -- takes 2 Bools as args and returns a Bool
myand x y = case x of -- pattern match on the "x" argument
  True  -> y     -- If x is True, then the result is y.
  False -> False -- If x is False, then the result is False.

-- Pattern matching can be written in two styles.
-- The first style with the "case" keyword is above.
-- Here's the second way, which lets you match on all arguments together.

myand2 :: Bool -> Bool -> Bool
myand2 True  y = y
-- In the above line, True is a pattern for the first argument,
-- and y is a pattern for the second. Both patterns must match
-- for the line to be chosen.
-- The "y" is a pattern that matches no matter what, and *binds*
-- that second argument to the name "y".
myand2 False _ = False
-- Now, False is the pattern for the first argument, and the underscore
-- is the pattern for the second. The underscore is like the "y"
-- pattern, but it doesn't bind the value -- it ignores it.

-- The power of pattern matching is that the compiler
-- can check that you handled all constructors of the value you match on.
-- Here's a definition that will cause a warning:
brokenfn :: Bool -> Int
brokenfn True = 5
-- We didn't handle the False case, so supplying False will
-- throw an exception. Make sure you pass -Wall to ghc or ghci
-- to see the warnings.

-- Now, here's another type that's not just a list of values.
data IntOrNull = AnInt Int | Null
-- Here, we define an IntOrNull type, with 2 constructors:
-- 1. AnInt is a *function* that takes an Int, and returns an IntOrNull.
--    (Int is a built-in type.)
--    The type of AnInt can be written: AnInt :: Int -> IntOrNull
-- 2. Null is a value, just like True or False, but of type IntOrNull.

-- Here's how to match on it.
theIntOrZero :: IntOrNull -> Int
theIntOrZero (AnInt i) = i -- This matches the AnInt constructor,
                           -- and binds "i" to the Int within.
                           -- That binding is then active only on the
                           -- right-hand side of the match line.
theIntOrZero Null      = 0

-- A more general version of this is already built-in to Haskell:
data Maybe a = Just a | Nothing
-- The "a" here is a "type variable". This is very much like the
-- generics system in Java/C#/etc.
-- Maybe can be called a "type function" -- it is parametrized
-- by another type. So "Maybe" is not a type, but "Maybe Int" is.
-- For the type "Maybe Int", the "Just" constructor takes an Int.
-- So Just has this type:         Just    :: a -> Maybe a
-- Nothing is still just a value: Nothing :: Maybe a

-- Here's a function that already exists in the "Data.Maybe" module.
-- You can put "import Data.Maybe" at the top of your file to get it.
fromMaybe :: a -> Maybe a -> a
-- This type means: for some (any) type, which we'll call "a",
-- this function can take an "a", a "Maybe a", and produce an "a".
-- So, for example, you can give it Int and Maybe Int and get Int.
fromMaybe _ (Just x) = x -- Mind the parens!
-- If our "Maybe a" has a real "a" in it, return that.
fromMaybe x Nothing  = x
-- Otherwise, return the first argument as a default.

main :: IO ()
main = print (myand2 False True)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment