Bruno C. D. S. Oliveira. ”The essence of the iterator pattern.” Journal of functional programming 19.3-4 (2009): 377-402. Markus Hauck (@markus1189) Beautiful Composition 3
of Least Power Composable Abstractions • design: the principle of least power • important property of an abstraction: composition Markus Hauck (@markus1189) Beautiful Composition 5
• something I notice often A Programmer “I just use flatMap and be done with it” • but: there are benefits of using sth with less power • parallel execution with Applicatives • less power equals more possibilities (Either vs. Validated) Markus Hauck (@markus1189) Beautiful Composition 6
The Principle “… picking not the most powerful solution but the least powerful” • https://www.w3.org/DesignIssues/Principles.html • same is valid for abstractions from functional programming • if you don’t need Monad, don’t require it • Applicative vs Functor, Monoid vs Semigroup, etc. The reason for this is that the less powerful the language, the more you can do with the data stored… Markus Hauck (@markus1189) Beautiful Composition 7
hide details and reason at a higher level • real power comes when those abstractions compose (otherwise throwaway islands) • most “classic” GoF Design Patterns in OO fail to compose • many “mathematical” abstractions in FP compose naturally • example: inductive monoid, product of monoids (same for Applicatives, …) Markus Hauck (@markus1189) Beautiful Composition 8
want to analyze text • collect metrics • single traversal • in essence a simple version of the GNU wc commandline tool 1 bash> wc moby-dick.txt 2 21206 208425 1193382 moby-dick.txt • lines, words and chars Markus Hauck (@markus1189) Beautiful Composition 9
• hard to change • mixed logic of the three metrics • run only certain metrics? (e.g., only words) • hardcoded control flow of char-by-char iteration • very little abstraction • can we do better? Markus Hauck (@markus1189) Beautiful Composition 12
(count each char) • number of lines (count newline characters) • number of words (harder, because it is context sensitive) • open for extension (closed for modification) Markus Hauck (@markus1189) Beautiful Composition 13
chars is easy, use (Int, +) as a Monoid • count 1 (combine 1) for every character h 0 + 1 = 1 e l l o \n w o r l d ! \n Markus Hauck (@markus1189) Beautiful Composition 18
chars is easy, use (Int, +) as a Monoid • count 1 (combine 1) for every character h e 1 + 1 = 2 l l o \n w o r l d ! \n Markus Hauck (@markus1189) Beautiful Composition 18
chars is easy, use (Int, +) as a Monoid • count 1 (combine 1) for every character h e l 2 + 1 = 3 l o \n w o r l d ! \n Markus Hauck (@markus1189) Beautiful Composition 18
chars is easy, use (Int, +) as a Monoid • count 1 (combine 1) for every character h e l l 3 + 1 = 4 o \n w o r l d ! \n Markus Hauck (@markus1189) Beautiful Composition 18
chars is easy, use (Int, +) as a Monoid • count 1 (combine 1) for every character h e l l o 4 + 1 = 5 \n w o r l d ! \n Markus Hauck (@markus1189) Beautiful Composition 18
chars is easy, use (Int, +) as a Monoid • count 1 (combine 1) for every character h e l l o \n 5 + 1 = 6 w o r l d ! \n Markus Hauck (@markus1189) Beautiful Composition 18
chars is easy, use (Int, +) as a Monoid • count 1 (combine 1) for every character h e l l o \n w 6 + 1 = 7 o r l d ! \n Markus Hauck (@markus1189) Beautiful Composition 18
chars is easy, use (Int, +) as a Monoid • count 1 (combine 1) for every character h e l l o \n w o 7 + 1 = 8 r l d ! \n Markus Hauck (@markus1189) Beautiful Composition 18
chars is easy, use (Int, +) as a Monoid • count 1 (combine 1) for every character h e l l o \n w o r 8 + 1 = 9 l d ! \n Markus Hauck (@markus1189) Beautiful Composition 18
chars is easy, use (Int, +) as a Monoid • count 1 (combine 1) for every character h e l l o \n w o r l 9 + 1 = 10 d ! \n Markus Hauck (@markus1189) Beautiful Composition 18
chars is easy, use (Int, +) as a Monoid • count 1 (combine 1) for every character h e l l o \n w o r l d 10 + 1 = 11 ! \n Markus Hauck (@markus1189) Beautiful Composition 18
chars is easy, use (Int, +) as a Monoid • count 1 (combine 1) for every character h e l l o \n w o r l d ! 11 + 1 = 12 \n Markus Hauck (@markus1189) Beautiful Composition 18
chars is easy, use (Int, +) as a Monoid • count 1 (combine 1) for every character h e l l o \n w o r l d ! \n 12 + 1 = 13 • so the result is 13 chars in total Markus Hauck (@markus1189) Beautiful Composition 18
count lines, use again (Int, +) as a Monoid • but only count 1 if the character is a \n h 0 + 0 = 0 e l l o \n w o r l d ! \n Markus Hauck (@markus1189) Beautiful Composition 19
count lines, use again (Int, +) as a Monoid • but only count 1 if the character is a \n h e 0 + 0 = 0 l l o \n w o r l d ! \n Markus Hauck (@markus1189) Beautiful Composition 19
count lines, use again (Int, +) as a Monoid • but only count 1 if the character is a \n h e l 0 + 0 = 0 l o \n w o r l d ! \n Markus Hauck (@markus1189) Beautiful Composition 19
count lines, use again (Int, +) as a Monoid • but only count 1 if the character is a \n h e l l 0 + 0 = 0 o \n w o r l d ! \n Markus Hauck (@markus1189) Beautiful Composition 19
count lines, use again (Int, +) as a Monoid • but only count 1 if the character is a \n h e l l o 0 + 0 = 0 \n w o r l d ! \n Markus Hauck (@markus1189) Beautiful Composition 19
count lines, use again (Int, +) as a Monoid • but only count 1 if the character is a \n h e l l o \n 0 + 1 = 1 w o r l d ! \n Markus Hauck (@markus1189) Beautiful Composition 19
count lines, use again (Int, +) as a Monoid • but only count 1 if the character is a \n h e l l o \n w 1 + 0 = 1 o r l d ! \n Markus Hauck (@markus1189) Beautiful Composition 19
count lines, use again (Int, +) as a Monoid • but only count 1 if the character is a \n h e l l o \n w o 1 + 0 = 1 r l d ! \n Markus Hauck (@markus1189) Beautiful Composition 19
count lines, use again (Int, +) as a Monoid • but only count 1 if the character is a \n h e l l o \n w o r 1 + 0 = 1 l d ! \n Markus Hauck (@markus1189) Beautiful Composition 19
count lines, use again (Int, +) as a Monoid • but only count 1 if the character is a \n h e l l o \n w o r l 1 + 0 = 1 d ! \n Markus Hauck (@markus1189) Beautiful Composition 19
count lines, use again (Int, +) as a Monoid • but only count 1 if the character is a \n h e l l o \n w o r l d 1 + 0 = 1 ! \n Markus Hauck (@markus1189) Beautiful Composition 19
count lines, use again (Int, +) as a Monoid • but only count 1 if the character is a \n h e l l o \n w o r l d ! 1 + 0 = 1 \n Markus Hauck (@markus1189) Beautiful Composition 19
count lines, use again (Int, +) as a Monoid • but only count 1 if the character is a \n h e l l o \n w o r l d ! \n 1 + 1 = 2 • we counted 2 lines in total Markus Hauck (@markus1189) Beautiful Composition 19
chars and lines • for multiple metrics, do multiple passes?! • no — because monoids compose • inductive: monoid + base monoid • product: tuple of monoids Markus Hauck (@markus1189) Beautiful Composition 20
some Monoids are based inductively on others 1 def optionMonoid[A: Monoid] = new Monoid[Option[A]] { /*...*/ } • Option, Future, IO, Task, … • the Option-Monoid works like this: 1 None |+| y === y 2 x |+| None === x 3 Some(x) |+| Some(y) === Some(x |+| y) Markus Hauck (@markus1189) Beautiful Composition 21
Stopwords • as an example: filter out (don’t count) stopwords • stopwords = most common words that are not interesting (“the”, “a”, …) • idea: if it is a stopword, use None, otherwise regular count with Some Markus Hauck (@markus1189) Beautiful Composition 22
Stopwords • assuming both “is” and “a” are classified as stopwords: this None |+| Some(1) = Some(1) is a test text Markus Hauck (@markus1189) Beautiful Composition 23
Stopwords • assuming both “is” and “a” are classified as stopwords: this is Some(1) |+| None = Some(1) a test text Markus Hauck (@markus1189) Beautiful Composition 23
Stopwords • assuming both “is” and “a” are classified as stopwords: this is a Some(1) |+| None = Some(1) test text Markus Hauck (@markus1189) Beautiful Composition 23
Stopwords • assuming both “is” and “a” are classified as stopwords: this is a test Some(1) |+| Some(1) = Some(2) text Markus Hauck (@markus1189) Beautiful Composition 23
Stopwords • assuming both “is” and “a” are classified as stopwords: this is a test text Some(2) |+| Some(1) = Some(3) • count without stopwords is 3 Markus Hauck (@markus1189) Beautiful Composition 23
Stopwords • use Option plus Max,Min to get longest/shortest non-stopword • more options: • don’t count chars like !?,. etc. using Option again • use Future/Task/IO to get parallelism • and sooo much more Markus Hauck (@markus1189) Beautiful Composition 24
base instance does not have to be a Monoid • using Option we can lift any Semigroup • empty becomes None • useful for e.g. Max and Min to represent lower/upper bound Markus Hauck (@markus1189) Beautiful Composition 25
if A and B have a Monoid instance, so does (A,B) 1 def tupleMonoid[A: Monoid, B: Monoid]: Monoid[(A, B)] = 2 new Monoid[(A, B)] { 3 def empty = (Monoid[A].empty, Monoid[B].empty) 4 5 def combine(x: (A, B), y: (A, B)) = (x._1 |+| y._1, x._2 |+| y._2) 6 } • combine the two A’s and the two B’s • we can fuse our two metrics! Markus Hauck (@markus1189) Beautiful Composition 26
we have two of our three target metrics • but, we can do a lot more than that! • find longest word • count occurrences by word • average word length (as own monoid or derive) • map of key to value (for any monoid as value) Markus Hauck (@markus1189) Beautiful Composition 28
problem: we can’t detect words • we require some form of memory or state • alternative idea: pre-split the text into words Markus Hauck (@markus1189) Beautiful Composition 30
for now, but: • composes very well from small blocks • easy to extend using custom metrics • Option can lift any Semigroup • works beautifully with everything that can be folded 1 trait Foldable[F[_]] { 2 // rest omitted 3 def foldMap[A, B](fa: F[A])(f: A => B)(implicit B: Monoid[B]): B 4 } Markus Hauck (@markus1189) Beautiful Composition 32
Applicatives adds richer structure, monoidal in effects • pure is like empty for effects • product1 is like combine for effects 1 def empty : F 2 def pure(x: A) : F[A] 3 4 def combine( x: F, y: F) : F 5 def product(fa: F[A], fb: F[B]): F[(A, B)] 1from Semigroupal Markus Hauck (@markus1189) Beautiful Composition 35
a moment, was all we learned about Monoids for nothing?! • rejoice, we can reuse everything we have until now • in two ways • lift Monoid inside Applicative • promote any Monoid to an Applicative Markus Hauck (@markus1189) Beautiful Composition 36
case class Const[A, B](getConst: A) • simple case class with phantom type parameter • … and one actual value • define Functor and Applicative Markus Hauck (@markus1189) Beautiful Composition 38
functor instance is a little strange • but the applicative instance is super awesome • remember Option lifting any Semigroup? • allows you to lift any Monoid into an Applicative! • that means we can still use everything we already have • (nb: Const is a monoid isomorphism) Markus Hauck (@markus1189) Beautiful Composition 41
Monoid composition? Nesting and tupling • we get the same for Applicative (from e.g. cats) • Nested • Tuple2K • why is that a big deal again? Markus Hauck (@markus1189) Beautiful Composition 42
monoids, foldMap is the essential operation • for applicatives, change to traverse • we can derive foldMap from traverse 1 trait Traverse[F[_]] extends Functor[F] with Foldable[F] { 2 def traverse[G[_]: Applicative, A, B](fa: F[A])(f: A => G[B]): G[F[B]] 3 } Markus Hauck (@markus1189) Beautiful Composition 48
• we can re-use the counting of chars and lines (and any monoidal metric) • as seen, have to be more clever for words • how to do this with applicatives? • State, IO, ST, … Markus Hauck (@markus1189) Beautiful Composition 49
if Applicative brings us so much power, how good must it be with Monad • unfortunately, Monads are not as good as they seem • Functor and Applicative are closed under composition • arbitrary nesting of Functor inside Functor / or Applicative inside Applicative • Monad is not closed, not guaranteed to be a Monad at all! • and Product (Tuple) does not work either Markus Hauck (@markus1189) Beautiful Composition 53
there is no traverse for real streams?! • it does not make sense for a stream (why?) • but: do we need the actual traverse • realization: traverse_ (Foldable) is enough! 1 trait Foldable[F[_]] { 2 def traverse_[G[_], A, B](fa: F[A])(f: A => G[B])( 3 implicit G: Applicative[G] 4 ): G[Unit] 5 } Markus Hauck (@markus1189) Beautiful Composition 55
what’s the difference between traverse and traverse_ • the regular traverse keeps the whole structure! • the traverse_ variant only sequences effects! • can be implemented using a foldlike method • important: Applicative effect sequencing is associative as per the laws • that means we can work on the stream in parallel Markus Hauck (@markus1189) Beautiful Composition 56
our framework needs the traverse_ • can be implemented for anything that has a proper fold • example with fs2 Stream 1 implicit class Fs2StreamOps[A, G[_], F[_]](s: fs2.Stream[F, A]) { 2 def traverse_[B]( 3 f: A => G[B] 4 )(implicit A: Applicative[G]): fs2.Stream[F, G[Unit]] = 5 s.fold(Applicative[G].pure(()))(_ <* f(_)) 6 } Markus Hauck (@markus1189) Beautiful Composition 57
suddenly we can use all the stream processing goodness • we could chunk the input and process in parallel (but keep order) • by carefully choosing the Applicative, operate with constant memory • use existing connectors to read from DB, Kafka, etc. • very flexible and almost for free! Markus Hauck (@markus1189) Beautiful Composition 59
way to cacluate metrics over text • using Monoid and Applicative • works with iteration and streaming • Principle Of Least Power: sometimes Monads are overrated Markus Hauck (@markus1189) Beautiful Composition 60
a product of selective functors • example use case: find file containing a word • applicative: cannot stop and iterates over everything, static analysis using free structure • monad: can abort iteration after first match, no static analysis • selective: abort early and also analysis via Free structure Markus Hauck (@markus1189) Beautiful Composition 64