Map / Filter / Fold etc.
(aka higher-order functions)
// map on a list
func map<T, U>(l: List<T>, transform: T -> U) -> List<U>
val addOne = { x -> x + 1 }
val nums = [1, 2, 3]
val incr = nums.map(addOne)
println(incr) // [2, 3, 4]
// OR
val incr = nums.map{ x -> x + 1 }
println(incr) // [2, 3, 4]
Map applies a function to each element while maintaining the "shape" of the data
Mapping over a list gives you back a list
// map on a list
func map<T, U>(l: List<T>, transform: T -> U) -> List<U> {
var newList = []
for (t in l) {
newList.append( transform(t) )
}
return newList
}
// map on an option
func map<T, U>(v: T?, transform: T -> U) -> U? {
if let v_ = v {
return transform(v_)
} else {
return nil
}
}
// map on anything (for any M that holds things)
func map<T, U>(x: M<T>, transform: T -> U) -> M<U>
Map works on many shapes
// filter on a list
func filter<T>(l: List<T>, predicate: T -> Bool) -> List<T>
val nums = [1,2,3,4]
val isEven = { x -> x % 2 == 0 }
val evens = nums.filter(isEven)
println(evens) // [2, 4]
// OR
val evens = nums.filter{ x -> x % 2 == 0 }
println(evens) // [2, 4]
Filter applies a predicate to the elements and keeps those for which it is true
Shape of the data is maintained
// filter on a list
func filter<T>(l: List<T>, predicate: T -> Bool) -> List<T> {
var newList = []
for (t in l) {
if ( predicate(t) ) {
newList.append(t)
}
}
return newList
}
// filter on anything (for any M that holds things)
func filter<T>(x: M<T>, predicate: T -> Bool) -> M<T>
Filter applies a predicate to the elements and keeps those for which it is true
Shape of the data is maintained
// left-associative fold
// foldLeft([1,2,3], z, *) = ((z * 1) * 2) * 3)
func foldLeft<T, U>(l: List<T>, z: U, op: (U, T) -> U) -> U
// right-associative fold
// foldRight([1,2,3], z, *) = ((z * 3) * 2) * 1)
func foldRight<T, U>(l: List<T>, z: U, op: (U, T) -> U) -> U
// no base-case fold
// only use associative operators order is unspecified
// reduce([1,2,3], *) = 1 * 2 * 3
func reduce<T, U where T: U>(l: List<T>, op: (U, T) -> U) -> U
println( [1,2,3].reduce{ x, y -> x + y } ) //6
println( ['q', 'r', 's'].foldLeft("") { b, x -> b.append(x) } ) // "qrs"
Fold applies the operator op to each element to combine it into one
The 3 folds: foldLeft, foldRight, and reduce
// foldLeft/foldRight can be implemented elegantly recursively
// foldLeft iteratively shown here
func foldLeft<T, U>(l: List<T>, z: U, op: (U, T) -> U) -> U {
var acc = z
for (t in l) {
acc = op(acc, t)
}
return acc
}
// reduce
// reduce starts with some element of the list, so all Ts must be Us
func reduce<T, U where T: U>(l: List<T>, op: (U, T) -> U) -> U {
var acc = l[0] // this will crash if the list is empty!
for (t in l) {
acc = op(acc, t)
}
return acc
}
Fold applies the operator op to each element to combine it into one
func zip<T, U>(a: M<T>, b: M<U>) -> M<(T,U)>
// here M = List
[1,2,3].zip(["a", "b", "c"]) // [(1, "a"), (2, "b"), (3, "c")]
Zip turns a product of containers into a container of products maintaining shape
func flatten<T>(l: M<M<T>>) -> M<T>
// here M = List
[[1,2], [3,4]].flatten() // [1,2,3,4]
Flatten turns a container of a container of elements into a single container of elements
func flatMap<T, U>(a: M<T>, f: T -> M<U>) -> M<U>
// here M = List
[1,2,3].flatMap{ x -> [x, 0] } // [1, 0, 2, 0, 3, 0]
// here M = option, using bind
3.bind{ x -> x + 1 } // 4
nil.bind{ x -> x + 1 } // nil
FlatMap applies a wrapping transformation to each element and then flattens the result
Bind on options is used everywhere in our code (iOS and Android)
NOTE: if-let is basically option bind, but a statement (it doesn't return a value)
All software is just moving data around into/within different shapes
- by someone
Let's move it better
*by shape we mean how the data is contained (simple examples: List, Map, Set)
the operators are:
These programs are equivalent
Also, where applicable, the operators remember the shape of the data you gave
for-loops don't -- if you need to rebuild a list, you have to do it yourself
// concise
return l.filter{ it.startsWith('f') }
.map{ len(it) }
.reduce(+)
// verbose
var sum = 0
for (s in l) {
if (s.startsWith('f')) {
sum += len(s)
}
}
return sum
// how many letters are there in total
// in only the words that start with f
This means that implementers of the operators are free to do whatever is best for our domain
For most of the operators, no where do we specify how we iterate
None of the definitions of these operators pertain to the what the shape of our data is either
This means we can reuse the same operators for ANY shapes of data and they always mean the same thing!
Typed means the computer will yell at you when it thinks you made a mistake (see last week's slides)
Pure (no side-effects) means the computer will catch you (see last week's slides -- remember side-effects subvert the type system)
Typeclasses are ways to speak about sets of Ms that satisfy some constraints.
A Monad is one of the most "famous" typeclasses (you see it thrown around a lot on the internet)
// there exists a way to wrap something
func pure<T>(x: T) -> M<T>
// for example
func pureList<T>(x: T) -> List<T> { [x] }
// there exists flatmap
func flatMap<T>(l: M<T>, transform: T -> M<U>) -> M<U>
And assuming pure and flatMap are implemented properly
(lookup Monad Laws for a formal definition of "properly")
Lists, Options, even Functions are Monads
Futures are Monads (the type for a computation which may not be complete -- see more next week)
The sequence function works for any monad
// turn a bunch of monads of T into a monad of a bunch of Ts
// for example:
// option: List<T?> -> List<T>? aka if one thing null, it's all null
func sequence(monads: List<M<T>>) -> M<List<T>> {
return monads.foldLeft([].pure()) { b, m ->
b.flatMap{ _ -> m }
}
}
Understanding typeclasses allows for a higher-level of discourse for code.
You can say more with fewer words.
If you tell me that your function works with Monads, I know it can work on Options, Lists, and Futures and any other weird Monadic structure I later invent
// pythonic list comprehension
val result = [x + 1 for x in [1,2,3,4] if x % 2 == 0]
println(result) // [3, 5]
// decomposed into maps and filters
val result = [1,2,3,4].filter{ x % 2 == 0 }.map{ x -> x + 1 }
println(result) // [3, 5]
a list comprehension is syntactic sugar for a series of maps and filters
val add: (Int, Int) -> Int = { x, y -> x + y }
val curriedAdd: Int -> Int -> Int = curry(add)
val addThree: Int -> Int = add(3)
val eight: Int = addThree(5)
println(eight) // 8
Currying turns a product of parameters into a series of partially applied functions one parameter at a time.
The curry function looks like this:
// curry a function with two parameters
func curry2<T, U, R>(f: (T, U) -> R) -> T -> U -> R {
return { x -> { y -> f(x, y) } }
}
func map<T>(transform: T -> U)(l: List<T>) -> List<U>
Consider a reverse curried representation of map (other operators work similarly)
Now we can cache a mapper!
val mapPlusOne = map{ x -> x + 1 }
println( mapPlusOne([1,2,3]) ) // [2, 3, 4]
println( mapPlusOne([5,6] ) // [6, 7]
General usage is easier with pipe-forward:
// the pipe-forward operator: x |> f = f(x)
func |> <T, R, U>(x: T, f: T -> U) -> U { f(x) }
val addOneAndFilter = map{ x -> x + 1 } |> filter{ x -> x >= 3 }
val z = [1,2,3] |> addOneAndFilter
println( z ) // [3, 4]
We can also cache a pipeline!
// compress
// [1, 1, 1, 3, 3, 4, 4, 4, 5, 5] => [3, 1, 2, 3, 3, 4, 2, 5]
// assume [1,2] ++ 3 => [1,2,3]
// assume [1,2].takeRight(1) => 2
// assume [1,2,3].dropRight(1) => [1,2]
// assume xOpt.getOrElse(y) => `if xOpt is not null xOpt else y`
func compress(s: List<Int>): List<Int> {
return s.foldLeft( (nil, []) ) { (curr, b), x ->
if (x == curr) {
(curr, (b.takeRight(1) + 1) ++ b.dropRight(1))
} else {
curr.bind{ (x, b ++ curr) }.getOrElse( (x, b) )
}
}.right
}
Map a contact's index in a flattened list back to it's header
// assume we have `toMap()` which applied on a `List<(T, U)>` returns `Map<T, U>`
// assume we have `zipWithIndex()` ["a", "b"].zipWithIndex() => [("a", 0), ("b", 1)]
// assume we have `getOrElse` xOpt.getOrElse(y) => /* x if x is not null else y */
func mapCellIdxToHeader(sections: List<Section>) -> Map<Int, Section> {
val visibleCells = sections.flatMap{ section: Section ->
val contactsAsSections = section.contacts.map{ section }
it.header.bind{ [it] + contactsAsSections }.getOrElse{ contactsAsSections }
}
return visibleCells.zipWithIndex().map{ (x, y) -> (y, x) }.toMap()
}
// struct Contact{ name: String }
// struct Header{ title: String }
// struct Section{ maybeHeader: Header?, contacts: List<Contact> }
// when shown to the user, nil headers are not shown (skip that idx)
func mapCellIdxToHeader(sections: List<Section>) -> Map<Int, Section>
This is used in our contacts screens
(adapted for simpler example)