Functional Collection Operators
Map / Filter / Fold etc.
(aka higher-order functions)
The big three
map / filter / fold
Map - transform things
how to use
// 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 - transform things
implementation
// 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 - keep things
how to use
// 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 - keep things
implementation
// 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
Fold - combine things
how to use
// 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
Fold - combine things
implementation
// 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
Other useful operators
Zip
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
Flatten
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
FlatMap
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
(aka bind or >>=)
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)
Why use them?
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)
Why not just use
for-loops?
the operators are:
Concise
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
Abstract over iteration
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
(how + what)
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 and Pure
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)
no side-effects
Data-structure independent!
again the "M" can be many shapes
Typeclasses
Classify sets of "M"s
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)
M is a Monad if:
// 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")
Monads
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 }
}
}
Typeclasses
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
Operator Variations
List comprehensions
// 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
Curried in reverse
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:
What is currying?
// curry a function with two parameters
func curry2<T, U, R>(f: (T, U) -> R) -> T -> U -> R {
return { x -> { y -> f(x, y) } }
}
Curried in reverse
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!
Examples
Examples
// 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
}
Examples
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)
Higher Order Collection Operators
By bkase
Higher Order Collection Operators
PL Part 2
- 797