- references
- http://blog.higher-order.com/assets/fpiscompanion.pdf
- https://typelevel.org/cats/typeclasses/functor.html
- https://typelevel.org/cats/typeclasses/monad.html
- https://stackoverflow.com/questions/14598990/confused-with-the-for-comprehension-to-flatmap-map-transformation
- https://docs.scala-lang.org/tutorials/FAQ/yield.html
- https://www.james-willett.com/scala-map-flatmap-filter/
- https://miklos-martin.github.io/learn/fp/2016/03/10/monad-laws-for-regular-developers.html
- https://typelevel.org/blog/2016/08/21/hkts-moving-forward.html
- https://netvl.github.io/scala-guidelines/type-system/higher-kinded-types.html
- https://dzone.com/articles/application-type-lambdas-scala-0
- https://carlo-hamalainen.net/2014/01/02/applicatives-compose-monads-do-not/
- https://github.com/kitlangton/zio-from-scatch
- https://www.manning.com/books/functional-programming-in-scala-second-edition
- https://github.com/fpinscala/fpinscala/wiki
- Scala with Cats Book - Noel Welsh
- Functional Programming in Scala - Paul Chiusano
- ZIO from Scratch — Part 1
- ZIO from Scratch — Part 2
- Software Transactional Memory
- A Pragmatic Introduction to Category Theory—Daniela Sfregola
- https://chat.openai.com/
- goals of this workshop
- introduction to scala features:
- for-comprehension
- higher kinded types
- developing basic intuitions concerning standard functional structures
- monoid
- functor and applicative functor
- monad
- show fundamental differences between above constructs
- apply knowledge to real life
- functional approach to validation
- functional approach to IO
- introduction to scala features:
- workshops order
ForComprehensionWorkshop
MonoidWorkshop
FunctorWorkshop
ApplicativeWorkshop
PersonValidatorWorkshop
MonadWorkshop
EchoWorkshop
- whenever we create an abstraction like Functor we should
- consider abstract methods it should have
- and laws we expect to hold for the implementations
- of course Scala won’t enforce any of these laws
- laws are important for two reasons
- help an interface form a new semantic level
- algebra may be reasoned about independently of the instances
- example
- we could proof that
Monoid[(A,B)]
constructed fromMonoid[A]
andMonoid[B]
is actually a monoid- we don’t need to know anything about
A
andB
to conclude this
- we don’t need to know anything about
- we could proof that
- we often rely on laws when writing various combinators
- example
- unzip
List[(A, B)]
intoList[A]
,List[B]
- same length
- corresponding elements in the same order
- algebraic reasoning can potentially save us a lot of work, since
we don’t have to write separate tests for these properties
- we could write a generic unzip function that works for any functor
- unzip
- example
- help an interface form a new semantic level
- monoid consists of the following:
- trait
trait Monoid[A] { def combine(a1: A, a2: A): A def zero: A }
- laws (semigroup with an identity element)
- associativity
combine(combine(x,y), z) == combine(x, combine(y,z)) for any choice of x: A, y: A, z: A
- identity
exists zero: A, that combine(x, zero) == x and combine(zero, x) == x for any x: A
- associativity
- is a type A and an implementation of
Monoid[A]
that satisfies the laws
- trait
- example
- string monoid
val stringMonoid = new Monoid[String] { def combine(a1: String, a2: String) = a1 + a2 val zero = "" }
- various Monoid instances don’t have much to do with each other
- monoid is a type, together with the monoid operations and a set of laws
- you may build some intuition by considering the various concrete instances
- but nothing guarantees all monoids you encounter will match your intuition
- we can say that
- type A forms a monoid
- type A is monoidal
- less precisely: type A is a monoid or even type A is monoidal
- but not: type A has monoid
- analogy: the page you’re reading forms a rectangle, is rectangular or it is a rectangle but not: it has a rectangle
- in any case, the Monoid[A] instance is simply evidence of this fact
- string monoid
- folding context
and we want to fold into the same type so
def foldRight[B](z: B)(f: (A, B) => B): B
monoid fit these argument types like a glovedef foldRight[A](z: A)(f: (A, A) => A): A
foldRight(monoid.zero)(monoid.combine)
foldLeft
andfoldRight
gives the same results when folding with monoid (associativity) - parallelism
- below three operations give the same result
combine(a, combine(b, combine(c, d))) combine(combine(combine(a, b), c), d) combine(combine(a, b), combine(c, d)) // allows for parallelism
- proof (associativity)
combine(combine(combine(a, b), c), d) = combine(combine(x, y), z), where x = combine(a, b), y = c, z = d combine(combine(x, y), z) = combine(x, combine(y, z)) combine(x, combine(y, z)) = combine(combine(a, b), combine(c, z)) so: combine(combine(combine(a, b), c), d) = combine(combine(a, b), combine(c, d))
- below three operations give the same result
- additional properties
- monoids compose
- example:
A, B monoids -> (A, B) is also a monoid
- example:
- monoid homomorphism
- when `A, B monoids => A.combine(f(x), f(y)) == f(B.combine(x, y))”
- monoid isomorphism
- homomorphism in both directions
- example:
String
andList[Char]
monoids with concatenation
- commutative combine
- when
combine(x, y) == combine(y, x))
- when
- monoids compose
- semigroup
- semigroup for a type A has an associative combine operation that returns an A given two input A values
trait Semigroup[A] { def combine(a1: A, a2: A): A }
- example:
NonEmptyList[A]
- there is no instance
Monoid[NonEmptyList[A]]
- you cannot specifyzero
element
- there is no instance
- semigroup for a type A has an associative combine operation that returns an A given two input A values
- definition
- we say that a type constructor
F
is a functor, and theFunctor[F]
instance constitutes proof thatF
is in fact a functor
trait Functor[F[_]] { def map[A, B](fa: F[A])(f: A => B): F[B] }
- we say that a type constructor
- must obey two laws:
fa.map(f).map(g) = fa.map(f.andThen(g))
fa.map(x => x) = fa
- functors compose: https://github.com/mtumilowicz/scala212-cats-category-theory-composing-functors
- for more formal reasoning, please refer: https://github.com/mtumilowicz/java11-category-theory-optional-is-not-functor
- definition
trait Applicative[F[_]] { def unit[A](a: => A): F[A] def map2[A, B, C](fa: F[A], fb: F[B])(f: (A, B) => C): F[C] }
- could be formulated using
unit
andapply
(therefore called Applicative), rather thanunit
andmap2
def apply[A,B](fab: F[A => B])(fa: F[A]): F[B]
- could be formulated using
- applicative laws
- left and right identity
in other words
map(v)(id) == v map(map(v)(g))(f) == map(v)(f compose g)
map2(unit(()), fa)((_,a) => a) == fa map2(fa, unit(()))((a,_) => a) == fa
- associativity (in terms of product)
def product[A,B](fa: F[A], fb: F[B]): F[(A,B)] = map2(fa, fb)((_,_))
def assoc[A,B,C](p: (A,(B,C))): ((A,B), C) = p match { case (a, (b, c)) => ((a,b), c) }
- product(product(fa,fb),fc) == map(product(fa, product(fb,fc)))(assoc)
- naturality law
fa.map2(fb)((a, b) => (f(a), g(b))) == fa.map(f).product(fb.map(g))
- left and right identity
- all applicatives are functors
- advantages
- preferable to implement combinators using as few assumptions as possible
- it’s better to assume that a data type can provide
map2
thanflatMap
- otherwise we’d have to write a new
traverse
every time we encountered a type that’s Applicative but not a Monad
- it’s better to assume that a data type can provide
- preferable to implement combinators using as few assumptions as possible
- validation context
- only reporting the first error means the user would have to repeatedly submit the form and fix one error at a time
- consider what happens in a sequence of
flatMap
calls like the followingvalidName(field1) flatMap (f1 => validBirthdate(field2) flatMap (f2 => validPhone(field3) map (f3 => ValidInput(f1, f2, f3))
- if
validName
fails with an error, thenvalidBirthdate
andvalidPhone
won’t even run
- if
- now think of doing the same thing with
map3
map3(validName(field1), validBirthdate(field2), validPhone(field3))(ValidInput(_,_,_))
- no dependency implied between the three expressions
- additional properties
- large number of the useful combinators on Monad can be defined using only unit and map2
- example
def map2[B, C](fb: F[B])(f: (A, B) => C): F[C] = fa.flatMap(a => fb.map(b => f(a, b)))
- example
- applicative functors compose
F[_]
andG[_]
are applicative functors =>F[G[_]]
is applicative functor
- large number of the useful combinators on Monad can be defined using only unit and map2
- definition
trait Monad[F[_]] { def unit[A](a: => A): F[A] def flatMap[A, B](ma: F[A])(f: A => F[B]): F[B] }
- monad laws
- given compose function
def compose[A,B,C](f: A => F[B], g: B => F[C]): A => F[C] = a => flatMap(f(a))(g)
- digression
- functions like that:
A => F[B]
are called Kleisli arrows
- functions like that:
- digression
- left identity and right identity
compose(f, unit) == f // m.flatMap(unit) == m compose(unit, f) == f // unit(a).flatMap(func) == func(a) // example for option option.flatMap(Some(_)) == option Some(value).flatMap(f) == f(value)
- similar to zero element in monoids
- associative law for monads
compose(compose(f, g), h) == compose(f, compose(g, h)) // example for option option.flatMap(f).flatMap(g) == option.flatMap(f(_).flatMap(g))
- similar to associative law for monoids
- given compose function
- monad is an implementation of one of the minimal sets of primitive combinators
satisfying the monad laws
- combinator sets
- unit and flatMap
def unit[A](a: => A): F[A] def flatMap[A, B](ma: F[A])(f: A => F[B]): F[B]
- unit and compose
def unit[A](a: => A): F[A] def compose[A,B,C](f: A => F[B], g: B => F[C]): A => F[C]
- unit, map and join
def unit[A](a: => A): F[A] def map[A, B](fa: F[A])(f: A => B): F[B] def join[A](mma: F[F[A]]): F[A]
- unit and flatMap
- combinator sets
- each monad brings its own set of additional primitive operations that are specific to it
- example:
Option
,Either
,List
- example:
- vs interfaces
- interfaces ~ relatively complete API for an abstract data type
- merely abstracting over the specific representation
- example: List - LinkedList, ArrayList
- monad ~ many vastly different data types can satisfy the Monad interface and laws
- like Monoid, is an abstract, purely algebraic interface
- interfaces ~ relatively complete API for an abstract data type
- chain of
flatMap
calls is like an imperative program with statements that assign to variables- monad specifies what occurs at statement boundaries
- example
Option
monad - may return None at some point and effectively terminate the processing
- example: identity monad
case class Id[A](value: A) { def map[B](f: A => B): Id[B] = Id(f(value)) def flatMap[B](f: A => Id[B]): Id[B] = f(value) }
val id = Id("Hello, ") .flatMap(a => Id("monad!") .flatMap(b => Id(a + b))) // id = Id(Hello, monad!)
- simply variable substitution
vs
for { a <- Id("Hello, ") b <- Id("monad!") } yield a + b
val a = "Hello, " val b = "monad!" val c = a + b
- variables
a
andb
get bound to"Hello, "
and"monad!”
- variables
- monads provide a context for introducing and binding variables, and performing variable substitution
- simply variable substitution
- unlike Functors and Applicatives, not all Monads compose
- to show that "monads do not compose", it is sufficient to find a counterexample, namely two monads
f
andg
such thatf g
is not a monad - proof: https://carlo-hamalainen.net/2014/01/02/applicatives-compose-monads-do-not/
- problem
- to implement
join
for nested monadsF
andG
, write something of a type like:F[G[F[G[A]]]] => F[G[A]]
- that can’t be written generally
- but if
G
also happens to have aTraverse
instanceF[F[G[G[A]]]] => F[G[A]] // use sequence to turn G[F[_]] into F[G[_]], then use join on F then on G
- to implement
- problem is often addressed with a custom-written version specifically constructed for composition
- example: monad transformers like
OptionT
- example: monad transformers like
- to show that "monads do not compose", it is sufficient to find a counterexample, namely two monads
- all monads are applicatives
- not all applicatives are monads
- difference between monads and applicatives
- applicative constructs context-free computations, while Monad allows for context sensitivity
join
andflatMap
can’t be implemented with justmap2
andunit
def join[A](f: F[F[A]]): F[A] // removes a layer of F
unit
function only lets us add anF
layermap2
lets us apply a function withinF
but does no flattening of layers
Option
applicative versus theOption
monad- combine the results from two (independent) lookups:
map2
val F: Applicative[Option] = ... val departments: Map[String,String] = ... val salaries: Map[String,Double] = ... val o: Option[String] = F.map2(departments.get("Alice"), salaries.get("Alice")) { (dept, salary) => s"Alice in $dept makes $salary per year" }
- result of one lookup to affect next lookup:
flatMap
val idsByName: Map[String,Int] val departments: Map[Int,String] = ... val o: Option[String] = idsByName.get("Bob") .flatMap { id => departments.get(id) } }
- combine the results from two (independent) lookups:
- each monad brings its own set of additional primitive operations that are specific to it
- example:
State[S, A]
def get[S]: State[S, S] def set[S](s: => S): State[S, Unit]
- example:
- monad is a monoid in a category of endofunctors
- endofunctor - functor that maps a category to itself
- let's look for minimal set of combinators for a monad: unit, map and join
- monoid = unit + join
- endofunctor = map
Monoid[M]
- objects: Scala types, arrows: Scala functionsMonad[F]
- objects: Scala functors, arrows: natural transformations- overview
zero
/unit
op
/join
Monoid[M]
1 => M
M² => M
Monad[F]
1 ~> F
F² ~> F
~>
is natural transformationM²
is(M, M)
F²
isF[F[A]]
1
is- for Monoid:
Unit
type - for Monad: identity functor
- for Monoid:
sealed trait IO[A] {
self => // lets us refer to this object inside closures
def unsafeRunSync: A
def map[B](f: A => B): IO[B] =
new IO[B] {
def run: B = f(self.run)
}
def flatMap[B](f: A => IO[B]): IO[B] =
new IO[B] {
def run: B = f(self.run).run
}
}
- in short:
IO
is a magic thing that says this function depends on something other than its arguments - clearly separates pure code from impure code, forcing us to be honest about where
interactions with the outside world are occurring
- referentially transparent description of a computation with effects
- IO computations are ordinary values
- we can store them in lists, pass them to functions, create them dynamically, and so on
- example
def PrintLine(msg: String): IO = new IO { def unsafeRun = println(msg) } def contest(p1: Player, p2: Player): IO = // pure function PrintLine(winnerMsg(winner(p1, p2)))
- given
IO[A]
will overflow the runtime call stack and throw aStackOverflowError
- solution: encapsulate operations into dedicated case classes and use tail recursion for evaluation
io.flatMap(f).flatMap(g) = io.flatMap(v => f(v) flatMap g)
- approach called trampoline
- solution: encapsulate operations into dedicated case classes and use tail recursion for evaluation
- represent an ability to abstract over type constructors
- suppose we have same implementations, different type constructors
def tuple[A, B](as: List[A], bs: List[B]): List[(A, B)] = as.flatMap{a => bs.map((a, _))} def tuple[A, B](as: Option[A], bs: Option[B]): Option[(A, B)] = as.flatMap{a => bs.map((a, _))} def tuple[E, A, B](as: Either[E, A], bs: Either[E, B]): Either[E, (A, B)] = as.flatMap{a => bs.map((a, _))}
- in programming, when we encounter such great sameness—not merely similar code, but identical code—we would like the opportunity to parameterize: extract the parts that are different to arguments, and recycle the common code for all situations
- we have a way to pass in implementations; that’s just higher-order functions
- we need "type constructor as argument"
def tuplef[F[_], A, B](fa: F[A], fb: F[B]): F[(A, B)] = ???
F[_]
means thatF
may not be a simple type, likeInt
orString
, but instead a one-argument type constructor, likeList
orOption
- higher-kinded types are sometimes used in libraries
- example: standard Scala collections
- Scala doesn’t allow us to use underscore syntax to simply say
State[Int, _]
to create an anonymous type constructor like we create anonymous functions- we have to go through type projections
({type L[a] = Map[K, a]})#L
- declares an anonymous type
- then access its
L
member with the#
syntax - type constructor declared inline like this is often called a type lambda in Scala
- removed in Scala 3
- we have to go through type projections
- each line in the expression using the
<-
is translated to aflatMap
call, except- the last line (
yield
) - it is translated to a concludingmap
call x <- c if cond
is translated toc.filter(x => cond)
- the last line (
flatMap
/map
for { bound <- list out <- f(bound) } yield out // is equivalent to (could be desugared with IntelliJ) list.flatMap { bound => f(bound).map { out => out } }
flatMap
/map
/filter
for { sl <- l el <- sl if el > 0 } yield el.toString.length // is equivalent to l.flatMap(sl => sl.filter(el => el > 0).map(el => el.toString.length))