Skip to content

Instantly share code, notes, and snippets.

@tomtau
Created February 15, 2016 20:47
Show Gist options
  • Save tomtau/eba903d4d7e3d1dd04b5 to your computer and use it in GitHub Desktop.
Save tomtau/eba903d4d7e3d1dd04b5 to your computer and use it in GitHub Desktop.
Cats: FP in Scala (Hong Kong Functional Programming Meetup)
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "slide"
}
},
"source": [
"These is a rough notebook (it was used to generate slides for a short talk at Hong Kong Functional Programming Meetup). A better resource for Cats is the tutorial called [herding cats](http://eed3si9n.com/herding-cats/); for common FP abstractions, [this book](https://www.manning.com/books/functional-programming-in-scala) or [Typeclassopedia](http://typeclassopedia.bitbucket.org) are great references.\n",
"# Cats\n",
"## A library for functional programming in Scala\n",
"### Tomas Tauber (HKU)"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "skip"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
":: problems summary ::\n",
":::: WARNINGS\n",
"\tUnable to reparse com.github.alexarchambault.jupyter#jupyter-scala-api_2.11.6;0.2.0-SNAPSHOT from sonatype-snapshots, using Thu Dec 24 22:50:58 HKT 2015\n",
"\n",
"\tChoosing sonatype-snapshots for com.github.alexarchambault.jupyter#jupyter-scala-api_2.11.6;0.2.0-SNAPSHOT\n",
"\n",
"\tUnable to reparse com.github.alexarchambault#ammonite-api_2.11.6;0.3.1-SNAPSHOT from sonatype-snapshots, using Wed Oct 21 17:43:36 HKT 2015\n",
"\n",
"\tChoosing sonatype-snapshots for com.github.alexarchambault#ammonite-api_2.11.6;0.3.1-SNAPSHOT\n",
"\n",
"\tUnable to reparse com.github.alexarchambault.jupyter#jupyter-api_2.11;0.2.0-SNAPSHOT from sonatype-snapshots, using Wed Oct 21 23:03:52 HKT 2015\n",
"\n",
"\tChoosing sonatype-snapshots for com.github.alexarchambault.jupyter#jupyter-api_2.11;0.2.0-SNAPSHOT\n",
"\n",
":: problems summary ::\n",
":::: WARNINGS\n",
"\tUnable to reparse com.github.alexarchambault.jupyter#jupyter-scala-api_2.11.6;0.2.0-SNAPSHOT from sonatype-snapshots, using Thu Dec 24 22:50:58 HKT 2015\n",
"\n",
"\tChoosing sonatype-snapshots for com.github.alexarchambault.jupyter#jupyter-scala-api_2.11.6;0.2.0-SNAPSHOT\n",
"\n",
"\tUnable to reparse com.github.alexarchambault#ammonite-api_2.11.6;0.3.1-SNAPSHOT from sonatype-snapshots, using Wed Oct 21 17:43:36 HKT 2015\n",
"\n",
"\tChoosing sonatype-snapshots for com.github.alexarchambault#ammonite-api_2.11.6;0.3.1-SNAPSHOT\n",
"\n",
"\tUnable to reparse com.github.alexarchambault.jupyter#jupyter-api_2.11;0.2.0-SNAPSHOT from sonatype-snapshots, using Wed Oct 21 23:03:52 HKT 2015\n",
"\n",
"\tChoosing sonatype-snapshots for com.github.alexarchambault.jupyter#jupyter-api_2.11;0.2.0-SNAPSHOT\n",
"\n"
]
},
{
"data": {
"text/plain": []
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"load.compiler.ivy(\"org.scalamacros\" % \"paradise_2.11.6\" % \"2.1.0-M5\")\n",
"load.ivy(\"org.typelevel\" %% \"cats\" % \"0.4.1\")"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[32mimport \u001b[36mcats._, cats.std.all._, cats.syntax.eq._\u001b[0m"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"import cats._, cats.std.all._, cats.syntax.eq._"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "fragment"
}
},
"source": [
"![Purrfect](http://cdn.meme.am/instances/500x/57053770.jpg)"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "slide"
}
},
"source": [
"#### Agenda\n",
"* Motivation / Risk Summary\n",
"* Ad-hoc polymorphism with *Typeclasses*\n",
"* Operating on \"wrapped\" values with *Functor* and *Applicative*\n",
"* Data aggregations with *Monoid*\n",
"* Functional error handling with *Option*, *Xor*, and *Validated*"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "slide"
}
},
"source": [
"#### Motivation\n",
"* Different extremes in uses:\n",
" * \"*Better Java*\": Java programmer's view of Scala\n",
" * \"*Functional language* with a funky, but expressive type system\": Haskeller's view of Scala \n",
"* Cats / Scalaz / ...: A collection of common useful abstractions A.K.A. \"FP Design Patterns\"\n",
"* (for robust and modular code)\n",
"* (intuition is gained after their repeated usage)\n",
"* Knowing these abstractions can be useful: [How we used Category Theory to solve a problem in Java](http://techblog.realestate.com.au/how-we-used-category-theory-to-solve-a-problem-in-java/)"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"##### Cats\n",
"* Meant to be \"*nicer*\" (more lightweight, modular, documented...)\n",
"* As of now, still quite *experimental* and *not ready* for real deployement (unlike Scalaz with `scalaz-stream`, `Monocle` and other great stuff)\n",
"* *BUT*: it's a good educational material\n",
"* *ALSO*: some abstractions are shared / overlapping among different libraries (such as Spire and Algebird) \n",
"* *AND*: some applications are appearing (such as [iteratee.io](https://github.com/travisbrown/iteratee) or [circe](https://github.com/travisbrown/circe) for JSON)"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"#### Risk Summary (all libraries in general, not only Cats)\n",
"* *Safe zone*: numerical computing, batch and stream processing pipelines (e.g. [Algebird and Spark](http://esumitra.github.io/algebird-boston-spark/#/))\n",
"* *Middle ground* (potential costs of adaptation): error handling, parsing, and many other libraries for pure FP\n",
"* *Danger zone*: [modern FP architecture using free structures](http://degoes.net/articles/modern-fp/)"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "slide"
}
},
"source": [
"#### Ad-hoc polymorphism with typeclasses\n",
"I assume everyone is familiar with parametric polymorphism:"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"defined \u001b[32mfunction \u001b[36mmap\u001b[0m"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"def map[A,B](f: A=>B, l: List[A]): List[B] = l.map(f)"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "fragment"
}
},
"source": [
"* In Haskell, we can put additional *contraints* on these parametric types denoting required operations (e.g. `Ord` for `compare` operation):\n",
"```haskell\n",
"sort :: Ord a => [a] -> [a]\n",
"```\n",
"* A bit like Java interfaces, but\n",
" 1. Without `this` to refer to an associated object\n",
" 2. Haskell type class instances (implementations) are completely separate from its associated type\n",
" 3. Before Java 8, interfaces couldn't have default implementations\n",
"* Solution to many software extension and integration problems\n",
"* In Scala, we can emulate type classes with traits + implicit objects\n",
"* Nowadays (as done in Cats), we can write type classes can without much boilerplate ([simulacrum](https://github.com/mpilquist/simulacrum)) and at low cost ([machinist](https://github.com/typelevel/machinist))"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[32mimport \u001b[36mcats.syntax.order._\u001b[0m"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"import cats.syntax.order._"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[36mres4\u001b[0m: \u001b[32mInt\u001b[0m = \u001b[32m-1\u001b[0m"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"1.0 compare 2.0"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"ename": "",
"evalue": "",
"output_type": "error",
"traceback": [
"Compilation Failed",
"\u001b[31mMain.scala:297: type mismatch;",
" found : Double(2.0)",
" required: Int",
"1 compare 2.0",
" ^\u001b[0m"
]
}
],
"source": [
"1 compare 2.0"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"Under the hood, we got in implicit implementation of:\n",
"```scala\n",
"trait Order[@sp A] extends Any with PartialOrder[A] { self =>\n",
"\n",
" /** [...]\n",
" */\n",
" def compare(x: A, y: A): Int\n",
"\n",
" ...\n",
"```\n",
"with\n",
"```scala\n",
"trait IntInstances {\n",
" implicit val intAlgebra = new IntAlgebra\n",
"\n",
" val IntMinMaxLattice: BoundedDistributiveLattice[Int] =\n",
" BoundedDistributiveLattice.minMax[Int](Int.MinValue, Int.MaxValue)(intAlgebra)\n",
"}\n",
"\n",
"class IntAlgebra extends EuclideanRing[Int]\n",
" with Order[Int] with Serializable {\n",
"\n",
" def compare(x: Int, y: Int): Int =\n",
" if (x < y) -1 else if (x > y) 1 else 0\n",
" ...\n",
"```\n",
"\n",
"Automated instances can be derived through [kittens](https://github.com/milessabin/kittens)"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "slide"
}
},
"source": [
"#### Functor and Applicative\n",
"[Source of pictures](http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html)\n",
"##### Functor\n",
"For things we can \"map\" over, this is its type class:\n",
"```scala\n",
"@typeclass trait Functor[F[_]] extends functor.Invariant[F] { self =>\n",
" def map[A, B](fa: F[A])(f: A => B): F[B]\n",
"\n",
" ....\n",
"}\n",
"```\n",
"\n",
"Let's look at `Option`:\n",
"```scala\n",
" sealed trait Option[+A] {\n",
" ...\n",
" }\n",
" object None extends Option[Nothing] {\n",
" ...\n",
" }\n",
" case class Some[+A](x: A) extends Option[A] {\n",
" ...\n",
" }\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[36mres5\u001b[0m: \u001b[32mOption\u001b[0m[\u001b[32mInt\u001b[0m] = Some(5)"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"Some(2) map {_ + 3}"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "fragment"
}
},
"source": [
"![Option functor 1](http://adit.io/imgs/functors/fmap_just.png)"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[36mres6\u001b[0m: \u001b[32mOption\u001b[0m[\u001b[32mInt\u001b[0m] = None"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"(None: Option[Int]) map {_ + 3}"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "fragment"
}
},
"source": [
"![Option functor 2](http://adit.io/imgs/functors/fmap_nothing.png)"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[36mres7\u001b[0m: \u001b[32mList\u001b[0m[\u001b[32mInt\u001b[0m] = \u001b[33mList\u001b[0m(\u001b[32m5\u001b[0m, \u001b[32m7\u001b[0m, \u001b[32m9\u001b[0m)"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"List(2,4,6) map {_ + 3}"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "fragment"
}
},
"source": [
"![Functor list](http://adit.io/imgs/functors/fmap_list.png)"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[32mimport \u001b[36mcats.syntax.functor._\u001b[0m"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"import cats.syntax.functor._"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[36mfoo\u001b[0m: \u001b[32mInt\u001b[0m => \u001b[32mInt\u001b[0m = <function1>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"val foo = ((x: Int) => x + 3) map {_ + 2}"
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[36mres10\u001b[0m: \u001b[32mInt\u001b[0m = \u001b[32m15\u001b[0m"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"foo(10)"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "fragment"
}
},
"source": [
"![Function functor](http://adit.io/imgs/functors/fmap_function.png)"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "slide"
}
},
"source": [
"##### Apply and Applicative\n",
"What if we want this apply function from `Some((_:Int)+3)` to Some(2)?\n",
"\n",
"```scala\n",
"trait Apply[F[_]] extends Functor[F] with ApplyArityFunctions[F] { self =>\n",
" /** [...]\n",
" */\n",
" def ap[A, B](fa: F[A])(f: F[A => B]): F[B]\n",
"\n",
" ....\n",
"}\n",
"@typeclass trait Applicative[F[_]] extends Apply[F] { self =>\n",
" /** [...]\n",
" */\n",
" def pure[A](x: A): F[A]\n",
"\n",
" ....\n",
"}\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[32mimport \u001b[36mcats.syntax.apply._\u001b[0m"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"import cats.syntax.apply._"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"defined \u001b[32mclass \u001b[36mIdOp\u001b[0m\n",
"defined \u001b[32mfunction \u001b[36mnone\u001b[0m"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
" implicit class IdOp[A](val a: A) {\n",
" def some: Option[A] = Some(a)\n",
" }\n",
" def none[A]: Option[A] = None"
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[36mres13\u001b[0m: \u001b[32mOption\u001b[0m[\u001b[32mInt\u001b[0m] = Some(5)"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"{{(_: Int) + 3}.some } ap (2.some)"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "fragment"
}
},
"source": [
"![Applicative](http://adit.io/imgs/functors/applicative_just.png)"
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[36mres14\u001b[0m: \u001b[32mList\u001b[0m[\u001b[32mInt\u001b[0m] = \u001b[33mList\u001b[0m(\u001b[32m2\u001b[0m, \u001b[32m4\u001b[0m, \u001b[32m6\u001b[0m, \u001b[32m4\u001b[0m, \u001b[32m5\u001b[0m, \u001b[32m6\u001b[0m)"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"{List({(_: Int) * 2},{(_: Int) + 3}) } ap (List(1,2,3))"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "fragment"
}
},
"source": [
"![Applicative list](http://adit.io/imgs/functors/applicative_list.png)"
]
},
{
"cell_type": "code",
"execution_count": 17,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "slide"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[32mimport \u001b[36mcats.syntax.cartesian._\u001b[0m"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"import cats.syntax.cartesian._"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"We import this to get syntax sugar for operating on several Applicative values using the infamous `|@|` AKA \"Chelsea bun\" AKA \"Admiral Ackbar\" AKA \"The Scream\" AKA \"Macaulay Culkin\" AKA \"Home Alone\" operator:\n",
"![|@|](http://athenacinema.com/wp-content/uploads/2013/11/Home_Alone_Boy1.jpg)"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"We can then easily operate on multiple arguments"
]
},
{
"cell_type": "code",
"execution_count": 18,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[36mres16\u001b[0m: \u001b[32mOption\u001b[0m[\u001b[32mInt\u001b[0m] = Some(-2)"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"(3.some |@| 5.some) map { _ - _ }"
]
},
{
"cell_type": "code",
"execution_count": 19,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[36mres17\u001b[0m: \u001b[32mOption\u001b[0m[\u001b[32mList\u001b[0m[\u001b[32mInt\u001b[0m]] = Some(List(3, 4))"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"(3.some |@| List(4).some) map { _ :: _ }"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "slide"
}
},
"source": [
"#### Data aggregations with *Monoid*\n",
"Algebraic structure that can combine two things together and has an identity element.\n",
"```scala\n",
"trait Semigroup[@sp(Int, Long, Float, Double) A] extends Any with Serializable {\n",
"\n",
" /**[...]\n",
" */\n",
" def combine(x: A, y: A): A\n",
"...\n",
"trait Monoid[@sp(Int, Long, Float, Double) A] extends Any with Semigroup[A] {\n",
"\n",
" /**\n",
" * Return the identity element for this monoid.\n",
" */\n",
" def empty: A\n",
"...\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": 20,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "slide"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[32mimport \u001b[36mcats.syntax.semigroup._\u001b[0m"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"import cats.syntax.semigroup._"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"`|+|` is shorthand for `combine`"
]
},
{
"cell_type": "code",
"execution_count": 21,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[36mres19\u001b[0m: \u001b[32mInt\u001b[0m = \u001b[32m4\u001b[0m"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"1 |+| 3"
]
},
{
"cell_type": "code",
"execution_count": 22,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[36mres20\u001b[0m: \u001b[32mList\u001b[0m[\u001b[32mInt\u001b[0m] = \u001b[33mList\u001b[0m(\u001b[32m1\u001b[0m, \u001b[32m2\u001b[0m, \u001b[32m3\u001b[0m, \u001b[32m4\u001b[0m)"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"List(1,2) |+| List(3,4)"
]
},
{
"cell_type": "code",
"execution_count": 23,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[36mres21\u001b[0m: \u001b[32mString\u001b[0m = \u001b[32m\"Hong Kong\"\u001b[0m"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"\"Hong \" |+| \"Kong\""
]
},
{
"cell_type": "code",
"execution_count": 24,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[36mres22\u001b[0m: \u001b[32mOption\u001b[0m[\u001b[32mInt\u001b[0m] = Some(3)"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"1.some |+| 2.some"
]
},
{
"cell_type": "code",
"execution_count": 25,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[36mres23\u001b[0m: \u001b[32mOption\u001b[0m[\u001b[32mInt\u001b[0m] = Some(1)"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"1.some |+| none[Int]"
]
},
{
"cell_type": "code",
"execution_count": 26,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[36mres24\u001b[0m: \u001b[32mOption\u001b[0m[\u001b[32mInt\u001b[0m] = Some(2)"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"none[Int] |+| 2.some"
]
},
{
"cell_type": "code",
"execution_count": 27,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[36mres25\u001b[0m: \u001b[32mOption\u001b[0m[\u001b[32mInt\u001b[0m] = None"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"none[Int] |+| none[Int]"
]
},
{
"cell_type": "code",
"execution_count": 28,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[36mres26\u001b[0m: \u001b[32mInt\u001b[0m = \u001b[32m6\u001b[0m"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
" Foldable[List].fold(List(1, 2, 3))(Monoid.additive[Int])"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "slide"
}
},
"source": [
"##### Algebraic Properties\n",
"* *Associative* a+(b+c) = (a+b)+c: can be partitioned/run in parallel\n",
"* *Closed* (Monoid + Monoid = Monoid): reduce in multiple stages\n",
"* *Identity*: support for zero/empty elements\n",
"\n",
"[Cats on Spark](https://github.com/non/big-electric-cat)\n",
"```scala\n",
" implicit class rddOps[A: ClassTag](lhs: RDD[A]) {\n",
" def csum(implicit m: Monoid[A]): A = lhs.reduce(_ |+| _)\n",
" def cmin(implicit o: Order[A]): A = lhs.reduce(_ min _)\n",
" def cmax(implicit o: Order[A]): A = lhs.reduce(_ max _)\n",
" }\n",
" ...\n",
"```\n",
"\n",
"##### Other Monoids\n",
"* `Map` and `Set`\n",
"* Top k elements\n",
"* Bloom filter\n",
"* count-min sketch, HyperLogLog, stochastic gradient descent, moments of distributions..."
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "slide"
}
},
"source": [
"#### Functional error handling with *Option*, *Xor*, and *Validated*"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "fragment"
}
},
"source": [
"* Java error throwing and `null` (can be wrapped in *Try*)\n",
"* We'll look at more FP-friendly variants"
]
},
{
"cell_type": "code",
"execution_count": 29,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"defined \u001b[32mfunction \u001b[36mhalf\u001b[0m"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"def half(x: Int): Option[Int] = \n",
" if (x % 2 == 0) Some(x / 2)\n",
" else None"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"![half](http://adit.io/imgs/functors/half.png)\n",
"But a wrapped value?\n",
"![half wrapped](http://adit.io/imgs/functors/half_ouch.png)"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"need to use `flatMap`:\n",
"![flatmap](http://adit.io/imgs/functors/plunger.jpg)"
]
},
{
"cell_type": "code",
"execution_count": 30,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[36mres28\u001b[0m: \u001b[32mOption\u001b[0m[\u001b[32mInt\u001b[0m] = None"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"Some(3) flatMap half"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "fragment"
}
},
"source": [
"![Some monad](http://adit.io/imgs/functors/monad_just.png)"
]
},
{
"cell_type": "code",
"execution_count": 31,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[36mres29\u001b[0m: \u001b[32mOption\u001b[0m[\u001b[32mInt\u001b[0m] = Some(2)"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"Some(4) flatMap half"
]
},
{
"cell_type": "code",
"execution_count": 32,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[36mres30\u001b[0m: \u001b[32mOption\u001b[0m[\u001b[32mInt\u001b[0m] = None"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"None flatMap half"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "fragment"
}
},
"source": [
"![Nothing](http://adit.io/imgs/functors/monad_nothing.png)"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"```scala\n",
"@typeclass trait FlatMap[F[_]] extends Apply[F] {\n",
" def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B]\n",
" // AKA bind -- its operator shorthand is >>=\n",
" ....\n",
"}\n",
"```\n",
"![flatMap](http://adit.io/imgs/functors/bind_def.png)"
]
},
{
"cell_type": "code",
"execution_count": 33,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[32mimport \u001b[36mcats.syntax.flatMap._\u001b[0m"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"import cats.syntax.flatMap._"
]
},
{
"cell_type": "code",
"execution_count": 34,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[36mres32\u001b[0m: \u001b[32mOption\u001b[0m[\u001b[32mInt\u001b[0m] = None"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"20.some >>= half >>= half >>= half"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "slide"
}
},
"source": [
"![monad chain](http://adit.io/imgs/functors/monad_chain.png)"
]
},
{
"cell_type": "code",
"execution_count": 35,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "slide"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[36mres33\u001b[0m: \u001b[32mOption\u001b[0m[\u001b[32mInt\u001b[0m] = None"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"for {\n",
" x0 <- Some(20)\n",
" x1 <- half(x0)\n",
" x2 <- half(x1)\n",
" x3 <- half(x2)\n",
"} yield x3"
]
},
{
"cell_type": "code",
"execution_count": 36,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "slide"
}
},
"outputs": [
{
"data": {
"text/plain": [
"defined \u001b[32mclass \u001b[36mUser\u001b[0m\n",
"defined \u001b[32mobject \u001b[36mUser\u001b[0m"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"final class User private(val name: String, val age: Int, val email: String)\n",
"\n",
"object User {\n",
" def isValidEmail(email : String): Option[String] = \"\"\"^[-a-z0-9!#$%&'*+/=?^_`{|}~]+(\\.[-a-z0-9!#$%&'*+/=?^_`{|}~]+)*@([a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?\\.)*(aero|arpa|asia|biz|cat|com|coop|edu|gov|info|int|jobs|mil|mobi|museum|name|net|org|pro|tel|travel|[a-z][a-z])$\"\"\".r.findFirstIn(email) \n",
" def isValidAge(age: Int): Option[Int] = if (age >= 0) Some(age) else None\n",
" def isValidName(name: String): Option[String] = if (name.length > 0) Some(name) else None\n",
" \n",
" def create(name: String, age: Int, email: String): Option[User] =\n",
" for {\n",
" vname <- isValidName(name)\n",
" vage <- isValidAge(age)\n",
" vemail <- isValidEmail(email)\n",
" } yield (new User(vname,vage,vemail))\n",
"}"
]
},
{
"cell_type": "code",
"execution_count": 37,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[36mu1\u001b[0m: \u001b[32mOption\u001b[0m[\u001b[32mString\u001b[0m] = None\n",
"\u001b[36mu2\u001b[0m: \u001b[32mOption\u001b[0m[\u001b[32mString\u001b[0m] = Some(Li Ka-shing)"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"val u1 = User.create(\"\",-1,\"\") map {_.name}\n",
"val u2 = User.create(\"Li Ka-shing\",87,\"nospam@lksf.org\") map {_.name}"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"Fine when there's one type of error.\n",
"\n",
"But we have three types of errors:"
]
},
{
"cell_type": "code",
"execution_count": 38,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"defined \u001b[32mclass \u001b[36mUserError\u001b[0m\n",
"defined \u001b[32mobject \u001b[36mEmptyName\u001b[0m\n",
"defined \u001b[32mclass \u001b[36mInvalidEmail\u001b[0m\n",
"defined \u001b[32mclass \u001b[36mInvalidAge\u001b[0m"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"sealed abstract class UserError\n",
"case object EmptyName extends UserError\n",
"case class InvalidEmail(s: String) extends UserError\n",
"case class InvalidAge(age: Int) extends UserError"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"We could use `Either`,\n",
"but standard library's `Either` wouldn't work with fors\n",
"(doesn't have flatMap).\n",
"For that, Cats have a right-biased Either AKA Xor"
]
},
{
"cell_type": "code",
"execution_count": 39,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[32mimport \u001b[36mcats.data._\u001b[0m\n",
"\u001b[32mimport \u001b[36mcats.data.Xor._\u001b[0m"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"import cats.data._\n",
"import cats.data.Xor._"
]
},
{
"cell_type": "code",
"execution_count": 40,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [
{
"data": {
"text/plain": [
"defined \u001b[32mclass \u001b[36mUser2\u001b[0m\n",
"defined \u001b[32mobject \u001b[36mUser2\u001b[0m"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"final class User2 private(val name: String, val age: Int, val email: String)\n",
"\n",
"object User2 {\n",
" def isValidEmail(email : String): UserError Xor String = if (\"\"\"^[-a-z0-9!#$%&'*+/=?^_`{|}~]+(\\.[-a-z0-9!#$%&'*+/=?^_`{|}~]+)*@([a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?\\.)*(aero|arpa|asia|biz|cat|com|coop|edu|gov|info|int|jobs|mil|mobi|museum|name|net|org|pro|tel|travel|[a-z][a-z])$\"\"\".r.findFirstIn(email) == None) left(InvalidEmail(email)) else right(email) \n",
" def isValidAge(age: Int): UserError Xor Int = if (age >= 0) right(age) else left(InvalidAge(age))\n",
" def isValidName(name: String): UserError Xor String = if (name.length > 0) right(name) else left(EmptyName)\n",
" \n",
" def create(name: String, age: Int, email: String): UserError Xor User2 =\n",
" for {\n",
" vname <- isValidName(name)\n",
" vage <- isValidAge(age)\n",
" vemail <- isValidEmail(email)\n",
" } yield (new User2(vname,vage,vemail))\n",
"}"
]
},
{
"cell_type": "code",
"execution_count": 41,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[36mu1e\u001b[0m: \u001b[32mXor\u001b[0m[\u001b[32mUserError\u001b[0m, \u001b[32mString\u001b[0m] = Left(EmptyName)\n",
"\u001b[36mu2e\u001b[0m: \u001b[32mXor\u001b[0m[\u001b[32mUserError\u001b[0m, \u001b[32mString\u001b[0m] = Right(Li Ka-shing)"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"val u1e = User2.create(\"\",-1,\"\") map {_.name}\n",
"val u2e = User2.create(\"Li Ka-shing\",87,\"nospam@lksf.org\") map {_.name}"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"`Option` and `Xor` are \"flatmappable\" (monads) in a chain -- after a failure, they shortcircuit.\n",
"We may want to process all events.\n",
"For that, there's `Validated` which is an applicative functor."
]
},
{
"cell_type": "code",
"execution_count": 42,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[32mimport \u001b[36mcats.data.Validated\u001b[0m\n",
"\u001b[32mimport \u001b[36mValidated.{ valid, invalid }\u001b[0m"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"import cats.data.Validated\n",
"import Validated.{ valid, invalid }"
]
},
{
"cell_type": "code",
"execution_count": 43,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[36mres41\u001b[0m: \u001b[32mValidated\u001b[0m[\u001b[32mString\u001b[0m, \u001b[32mString\u001b[0m] = Invalid(event 2 failed!event 3 failed!)"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"(valid[String, String](\"event 1 ok\") |@|\n",
"invalid[String, String](\"event 2 failed!\") |@|\n",
"invalid[String, String](\"event 3 failed!\")) map {_ + _ + _}"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "subslide"
}
},
"source": [
"Cats also provided `NonEmptyList` which will be useful for accumulating errors:"
]
},
{
"cell_type": "code",
"execution_count": 44,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "fragment"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[32mimport \u001b[36mcats.data.{ NonEmptyList => NEL }\u001b[0m"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"import cats.data.{ NonEmptyList => NEL }"
]
},
{
"cell_type": "code",
"execution_count": 45,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [
{
"data": {
"text/plain": [
"defined \u001b[32mclass \u001b[36mUser3\u001b[0m\n",
"defined \u001b[32mobject \u001b[36mUser3\u001b[0m"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"final class User3 private(val name: String, val age: Int, val email: String)\n",
"\n",
"object User3 {\n",
" def isValidEmail(email : String): Validated[NEL[UserError], String] = if (\"\"\"^[-a-z0-9!#$%&'*+/=?^_`{|}~]+(\\.[-a-z0-9!#$%&'*+/=?^_`{|}~]+)*@([a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?\\.)*(aero|arpa|asia|biz|cat|com|coop|edu|gov|info|int|jobs|mil|mobi|museum|name|net|org|pro|tel|travel|[a-z][a-z])$\"\"\".r.findFirstIn(email) == None) invalid(NEL(InvalidEmail(email))) else valid(email) \n",
" def isValidAge(age: Int): Validated[NEL[UserError], Int] = if (age >= 0) valid(age) else invalid(NEL(InvalidAge(age)))\n",
" def isValidName(name: String): Validated[NEL[UserError], String] = if (name.length > 0) valid(name) else invalid(NEL(EmptyName))\n",
" \n",
" def create(name: String, age: Int, email: String): Validated[NEL[UserError], User3] =\n",
" (isValidName(name) |@|\n",
" isValidAge(age) |@|\n",
" isValidEmail(email)) map {new User3(_, _, _)}\n",
"} "
]
},
{
"cell_type": "code",
"execution_count": 48,
"metadata": {
"collapsed": false,
"slideshow": {
"slide_type": "subslide"
}
},
"outputs": [
{
"data": {
"text/plain": [
"\u001b[36mu1v\u001b[0m: \u001b[32mValidated\u001b[0m[\u001b[32mNonEmptyList\u001b[0m[\u001b[32mUserError\u001b[0m], \u001b[32mString\u001b[0m] = Invalid(OneAnd(EmptyName,List(InvalidAge(-1), InvalidEmail())))\n",
"\u001b[36mu2v\u001b[0m: \u001b[32mValidated\u001b[0m[\u001b[32mNonEmptyList\u001b[0m[\u001b[32mUserError\u001b[0m], \u001b[32mString\u001b[0m] = Valid(Li Ka-shing)"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"val u1v = User3.create(\"\",-1,\"\") map {_.name}\n",
"val u2v = User3.create(\"Li Ka-shing\",87,\"nospam@lksf.org\") map {_.name}"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "fragment"
}
},
"source": [
"There's also `Ior` which is a hybrid between `Xor` and `Validated` -- example usage:\n",
"* Accumulate warnings\n",
"* Shortcircuit on errors\n",
"\n",
"Anyway, we stop here for now\n",
"even though\n",
"there are other interesting things in Cats (and similar libraries) "
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "slide"
}
},
"source": [
"#### Summary\n",
"* A collection of useful abstractions for FP\n",
"* Extensible design with typeclasses\n",
"* Operating on wrapped values:\n",
"![recap](http://adit.io/imgs/functors/recap.png)\n",
"* \"Made-for-parallelism\" Monoids\n",
"* Functional error handling:\n",
" * `Option` for single-error shortcircuit\n",
" * `Xor` for multiple-error shortcircuit\n",
" * `Validated` for multiple-error log"
]
}
],
"metadata": {
"celltoolbar": "Slideshow",
"kernelspec": {
"display_name": "Scala 2.11",
"language": "scala211",
"name": "scala211"
},
"language_info": {
"codemirror_mode": "text/x-scala",
"file_extension": ".scala",
"mimetype": "text/x-scala",
"name": "scala211",
"pygments_lexer": "scala",
"version": "2.11.6"
}
},
"nbformat": 4,
"nbformat_minor": 0
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment