Lecture 2 — 2020-01-29

Higher-order functions

This lecture is written in literate Haskell; you can download the raw source.

We spent the class discussion list functions, using anonymous functions (lambdas, written \x1 x2 x3 ... -> e), with a focus on folds. Notes on graphs are at the bottom.

Before I go into detail about folds, you should know how to turn on warnings in GHC:

{-# OPTIONS_GHC -Wall -fno-warn-unused-imports #-}

I turn off the unused imports warning, because it’s annoying and not really relevant for us. For this file, we’ll also turn off warnings about name shadowing.

{-# OPTIONS_GHC -fno-warn-name-shadowing #-}

In general, though, it’s better to leave that warning on.

module Lec02 where

import Prelude hiding (map, foldr, foldl) -- hide some things we'll define ourselves

import Data.Map (Map)
import qualified Data.Map as Map
import Data.Set (Set)
import qualified Data.Set as Set

Maps

We started by observing some patterns:

addOne :: [Int] -> [Int]
addOne [] = []
addOne (x:xs) = x+1 : addOne xs

double :: [Int] -> [Int]
double [] = []
double (x:xs) = 2*x : double xs

negateAll :: [Bool] -> [Bool]
negateAll [] = []
negateAll (b:bs) = not b : negateAll bs

And abstracting out a general framework:

map :: (a -> b) -> [a] -> [b]
map _ [] = []
map f (x:xs) = f x : map f xs

The map function is a higher-order function, because it’s a function that takes another function as its input. We can understand its behavior diagrammatically like so:

   :             :
  / \           / \
 1   :        f 1  :
    / \    =>     / \
   2   :        f 2  :
      / \           / \
     3  []        f 3  []

Higher-order functions are extremely powerful: they let us express whole patterns of computation in a single fell swoop. Once we’ve written map, we don’t need to ever write that kind of recursion manually again. Not only do we avoid silly errors in writing map-like functions, we also save time.

Folds

There’s some nice material on Wikipedia about folds.

There are two kinds of folds: rightwards folds and leftwards folds. They correspond to direct and accumulating recursion, respectively. Since direct is easier to understand, we looked at foldr first.

The folds we’ll define—foldr and foldl are already in the Prelude…

so we can hide them for the rest of this file.

We’ll need a few more imports below.

foldr

Here’s a rightward fold, a higher-order function that seems a bit opaque at first blush.

foldr :: (a -> b -> b) -> b -> [a] -> b
foldr _ b [] = b
foldr f b (a:as) = f a (foldr f b as)

We can understandfoldr using the following equation:

foldr f b [a1,...,an] = f a1 (f a2 (... (f an b) ...))

We can also understand foldr as replacing a list’s ‘spine’ with function applications, like so:

   :             f
  / \           / \
 1   :         1   f
    / \    =>     / \
   2   :         2   f
      / \           / \
     3  []         3   b

We defined a few different functions using foldr.

head :: [a] -> a
head = foldr (\a _ -> a) undefined 

product :: [Int] -> Int
product = foldr (*) 1

and, or :: [Bool] -> Bool
and = foldr (&&) True
or  = foldr (||) False

In general, a function defined like:

g [] = v
g (x:xs) = f x (g xs)

can be implemented using foldr as:

g = foldr f v

foldl

Leftward folds are like rightward folds but using a different pattern:

   :                f
  / \              / \
 1   :            f   3
    / \    =>    / \
   2   :        f   2
      / \      / \
     3  []    b   1

The code for a leftward fold looks more like the accumulator passing style functions we’ve already written.

foldl :: (b -> a -> b) -> b -> [a] -> b
foldl _ b [] = b
foldl f b (a:as) = foldl f (f b a) as

Correspondingly, foldl satisfies the following equation:

foldl f b [a1,...,an] = f (... (f (f b a1) a2) ...) an

We can use foldl to define the list reversal:

rev :: [a] -> [a]
rev = foldl (\acc x -> x:acc) []

We can also use it to easily define last:

last = foldl (\a b->b) undefined 

Finally, we can mechanically translate recursive accumulating functions defined like:

g xs = g' xs b

g' [] acc = acc
g' (x:xs) acc = g' xs (f x acc)

into leftward folds like:

g' = foldl f b

Graphs

We didn’t get a chance to apply our newfound prowess with maps and folds to working with maps and sets.

Haskell’s maps and set data types (in Data.Map and Data.Set, respectively) are opaque: we don’t get to know what constructors they have, so we have no choice but to use maps, folds, and other higher-order functions to work with them.

type Node = String
type Graph = Map Node (Set Node)

Note how we (a) simulataneously assign types to a number of values while (b) simultaneously defining those values.

a,b,c,d,e :: Node
[a,b,c,d,e] = ["a","b","c","d","e"]

You can also use tuples, as in:

f,g :: Node
(f,g) = ("f","g")

Next, we can define two graphs.

g1,g2 :: Graph
g1 = Map.fromList [(a, Set.fromList [b,c]),
                   (b, Set.fromList [a,d]),
                   (c, Set.fromList [a,d]),
                   (d, Set.fromList [b,c])]
g2 = Map.fromList [(a, Set.fromList [b,c]),
                   (b, Set.fromList [a,d]),
                   (c, Set.fromList [a,d]),
                   (d, Set.fromList [])]

These correspond to the following directed graphs:

a ↔ b
↕   ↕    g1
c ↔ d

a ↔ b
↕   ↓    g2
c → d

We can easily calculate the out-degree of each node:

outDegrees :: Graph -> Map Node Int
outDegrees = Map.map Set.size

And we can just as easily check that all nodes have out-degree one or more:

allDegreeOneOrMore :: Graph -> Bool
allDegreeOneOrMore = Map.foldr ((&&) . (>=1)) True . outDegrees

We can also write a function that takes a node and a graph and makes a new graph where every node has out-degree of at least one.

makeDegreeOne :: Node -> Graph -> Graph
makeDegreeOne n g = Map.insert n (Set.singleton n) g'
  where g' = Map.map connect g
        connect tgts =
          if Set.null tgts
          then Set.singleton n
          else tgts