The Essence of the Iterator pattern treats iterating over collections as two problems, which exhibit traversal of collections (and modifying the content) and accumulating values based on the contents. Jeremy Gibbons and Bruno C.d.S. Oliveira show how Applicative Functors and related type classes can be used in functional programming to solve these problems.
Paper: http://www.cs.ox.ac.uk/jeremy.gibbons/publications/iterator.pdf
The paper "Essence of the iterator pattern" is widely quoted amongst the functional programming community and illustrates nicely how recent acadamic research (Applicative Functors, McBride, 2008) finds its way into language design and application of functional programming languages such as Scala or Haskell.
The slides give a brief introduction and were presented at the "Papers We Love" Meetup in Hamburg.
2. Goal
“Imperative iterations using the pattern have two
simultaneous aspects: mapping and accumulating.”
Jeremy Gibbons & Bruno C. d. S. Oliveira
Markus Klink, @joheinz, markus.klink@inoio.de
3. Functionalmappingand
accumulating
trait Traverse[F[_]] extends Functor[F] with Foldable[F] {
def traverse[G[_]: Applicative, A, B](fa: F[A])(f: A => G[B]): G[F[B]]
}
trait Applicative[F[_]] extends Functor[F] {
def ap[A,B](fa: F[A])(f: F[A => B]) : F[B]
def pure(a: A) : F[A]
def map[A,B](fa: F[A])(f: A => B) : F[B] = ap(fa)(pure(f))
}
Traverse takes a structure F[A], injects each element
via the function f: A => G[B] into an Applicative
G[_] and combines the results into G[F[B] using the
applicative instance of G.
Markus Klink, @joheinz, markus.klink@inoio.de
4. Acloser look
trait Foldable[F[_]] {
def foldRight[A, B](fa: F[A], z: => B)(f: (A, B) => B): B
}
trait Traverse[F[_]] extends Functor[F] with Foldable[F] {
def traverse[G[_]: Applicative, A, B](fa: F[A])(f: A => G[B]): G[F[B]]
}
Look at the similarities!
Markus Klink, @joheinz, markus.klink@inoio.de
5. Traversing is "almost" like
Folding:
» Look! No zero element:
def foldRight[A, B](fa: F[A], z: => B)(f: (A, B) => B): B
def traverse[G[_]: Applicative, A, B](fa: F[A])(f: A => G[B]): G[F[B]]
» Look! B is wrapped in an Applicative and our F:
def foldRight[A, B](fa: F[A], z: => B)(f: (A, B) => B): B
def traverse[G[_]: Applicative, A, B](fa: F[A])(f: A => G[B]): G[F[B]]
Markus Klink, @joheinz, markus.klink@inoio.de
7. val l = List(1,2,3)
val result: Future[List[String]] = l.traverse(a => Future.successful { a.toString })
How?
// traverse implementation of List, G[_] is the future
override def traverse[G[_]: Applicative, A, B](fa: List[A])(f: (A) => G[B]): G[List[B]] = {
val emptyListInG: G[List[B]] = Applicative[G].pure(List.empty[B]) // if the list is empty we need a Future { List.empty() }
// f(a) gives us another G[B], which we can inject into B => B => Future[List[B]]
foldRight(fa, emptyListInG) { (a: A, fbs: G[List[B]]) => Applicative[G].apply2(f(a), fbs)(_ +: _) }
}
// applicative instance of Future, example...
override def ap[A,B](F: => Future[A])(f: => Future[A => B]) : Future[B] = {
for {
a <- F
g <- f
} yield { g(a) }
}
Markus Klink, @joheinz, markus.klink@inoio.de
8. Gibbons & Oliveiraclaimthatwe
can do:
val x : List[Char]= "1234".toList
val result : Int = x.traverse(c => ???)
assert(4 == result)
The claim is that we can accumulate values, or write
the length function just with Traverse/Applicative
Markus Klink, @joheinz, markus.klink@inoio.de
9. How?
» we need a result type G[List[Int]] which equals
Int
» G needs to be an applicative
» we need to calculate a sum of all the values.
» we need a zero value in case the list is empty,
because of ...
val emptyListInG: G[List[B]] =
Applicative[G].pure(List.empty[B])
Markus Klink, @joheinz, markus.klink@inoio.de
10. Each Monoid gives risetoan
applicative
trait Monoid[F] extends Semigroup[F] {
self =>
/**
* the identity element.
*/
def zero: F
def applicative: Applicative[Lambda[a => F]] = new Applicative[Lambda[a => F]] {
// mapping just returns ourselves
override def map[A, B](fa: F)(f: A => B) : F = fa
// putting any value into this Applicative will put the Monoid.zero in it
def pure[A](a: => A): F = self.zero
// Applying this Applicative combines each value with the append function.
def ap[A, B](fa: => F)(f: => F): F = self.append(f, fa)
}
Markus Klink, @joheinz, markus.klink@inoio.de
11. How2
Part of the trick is this type: Applicative[Lambda[a
=> F]]!
It means that we throw everything away and create a
type G[_] which behaves like F. So...
val x : List[Char]= "1234".toList
val charCounter : Applicative[Lambda[a => Int]] = Monoid[Int].applicative
// we just "reversed" the parameters of traverse
// the summing is done automatically via append
charCounter.traverse(x)(_ => 1)
Markus Klink, @joheinz, markus.klink@inoio.de
12. Counting lines
In the previous example we assigned each char in the
list to a 1 and let the Monoid do the work.
val x : List[Char] = "1233n1234n"
val lineCounter : Applicative[Lambda[a => Int]] = Monoid[Int].applicative
lineCounter.traverse(x){c => if (c == 'n') 1 else 0 }
Markus Klink, @joheinz, markus.klink@inoio.de
13. Products ofApplicatives
Doing bothatthe sametimewithinasingle
traversal
val x : List[Char]= "1234n1234n"
val counter : Applicative[Lambda[a => Int]] = Monoid[Int].applicative
val charLineApp : Applicative[Lambda[a => (Int, Int)]] =
counter.product[Lambda[a => Int](counter)
val (chars, lines) = counter.traverse(x){c => (1 -> if (c == 'n') 1 else 0 }
Markus Klink, @joheinz, markus.klink@inoio.de
14. Countingwords
Counting words cannot work on the current position
alone. We need to track changes from spaces to non
spaces to recognize the beginning of a new word.
def atWordStart(c: Char): State[Boolean, Int] = State { (prev: Boolean) =>
val cur = c != ' '
(cur, if (!prev && cur) 1 else 0)
}
val WordCount : Applicative[Lambda[a => Int]] =
State.stateMonad[Boolean].compose[Lambda[a => Int](counter)
val StateWordCount = WordCount.traverse(text)(c => atWordStart(c))
StateWordCount.eval(false)
Markus Klink, @joheinz, markus.klink@inoio.de
15. Usingthe productofall3
countersto implementwc
val AppCharLinesWord: Applicative[Lambda[a => ((Int, Int), State[Boolean, Int])]] =
Count // char count
.product[Lambda[a => Int]](Count) // line count
.product[Lambda[a => State[Boolean, Int]]](WordCount) // word count
val ((charCount, lineCount), wordCountState) = AppCharLinesWord.traverse(text)((c: Char) =>
((1, if (c == 'n') 1 else 0), atWordStart(c)))
val wordCount: Int = wordCountState.eval(false)
Markus Klink, @joheinz, markus.klink@inoio.de
17. // start value
public Integer count = 0;
public Collection<Person> collection = ...;
for (e <- collection) {
// or we use an if
count = count + 1;
// side effecting function
// or we map into another collection
e.modify();
}
Obviously in a functional programming language, we
would not want to modify the collection but get back
a new collection.
We also would like to get the (stateful) counter
back.
Markus Klink, @joheinz, markus.klink@inoio.de
18. def collect[G[_]: Applicative, A, B](fa: F[A])(f: A => G[Unit])(g: A => B): G[F[B]] =
{
val G = implicitly[Applicative[G]]
val applicationFn : A => G[B] = a => G.ap(f(a))(G.pure((u: Unit) => g(a)))
self.traverse(fa)(applicationFn)
}
def collectS[S, A, B](fa: F[A])(f: A => State[S, Unit])(g: A => B): State[S, F[B]] =
{
collect[Lambda[a => State[S, a]], A, B](fa)(f)(g)
}
val list : List[Person] = ...
val stateMonad = State.stateMonad[Int]
val (counter, updatedList) = list.collectS{
a => for { count <- get; _ <- put(count + 1) } yield ()}(p => p.modify()).run(0)
Markus Klink, @joheinz, markus.klink@inoio.de
20. // start value
public Collection<Person> collection = ...;
for (e <- collection) {
e.modify(stateful());
}
Now the modification depends on some state we are
collecting.
Markus Klink, @joheinz, markus.klink@inoio.de
21. def disperse[G[_]: Applicative, A, B, C](fa: F[A])(fb: G[B], g: A => B => C): G[F[C]] =
{
val G = implicitly[Applicative[G]]
val applicationFn: A => G[C] = a => G.ap(fb)(G.pure(g(a)))
self.traverse(fa)(applicationFn)
}
def disperseS[S, A, C](fa: F[A])(fb: State[S, S], g: A => S => C) : State[S,F[C]] =
{
disperse[Lambda[a => State[S, a]], A, S, C](fa)(fb, g)
}
Markus Klink, @joheinz, markus.klink@inoio.de