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

  • 793