The most imaportant ideas in modern functional programming are:
functional programming is all about putting data first. The first is defining what kinds of data we have in a problem domain, and what kinds of transformations we want on them. Then we are building up the data structures and the code to do the transformations.
functional programming is not about pure functions any more. Eventually, programs will produce side-effects and side-effect management is a puzzle with many parts.
Code:
val f: String => Int = str => str.length
f("Hello") shouldEqual 5
f("Bye") shouldEqual 3
Code:
val f: String => Int = str => str.length
val g: Int => String = len => "*" * len
val hide = g compose f // g o f
hide("Hello") shouldEqual "*****"
hide("Workday") shouldEqual "*******"
Code:
// identity function
def identity[A](a: A): A = a
identity("Hello") shouldEqual "Hello"
identity("Workday") shouldEqual "Workday"
Category Type-Class:
trait Category[~>[-_, +_]]{
def id[A]: A ~> A
def compose[A, B, C](f: B ~> C, g: A ~> B): A ~> C
}
Category Laws:
// identity function
def identity[A](a: A): A = a
val f: String => Int = str => str.length
val g: Int => String = len => "*" * len
val h: String => String = str => str.replace("*", ".")
//Associative Law:
(h compose g) compose f <-> h compose (g compose f))
//Identity Laws:
(identity compose f) <-> (f compose identity) <-> f
“a function which builds program fragments from program fragments; in a sense the programmer using combinators constructs much of the desired program automatically, rather than writing every detail by hand”
- John Hughes Generalising Monads to Arrows
Arrow is generalization of function, which provides more function combinators:Like first/second/split/combine, ...
Split:
Combine:
Multiplex:
Merge:
Arrow:
trait Arrow[~>[-_, +_]] extends Category[~>] {
def arr[B, C](f: B => C): B ~> C
def first[B, C, D](f: B ~> C): (B, D) ~> (C, D)
}
Laws:
def fst[A, B](p: (A, B)): A = p._1
def assoc[A, B, C](p: ((A, B), C)): (A, (B, C)) = (p._1._1, (p._1._2, p._2))
// Identity:
arr(identity) <-> id
// Distribute over Composition:
arr(f >>> g) <-> arr(f) >>> arr(g)
fst(f >>> g) <-> fst(f) >>> fst(g)
// Order must be irrelevant when piping & lifting:
fst(arr(f)) <-> arr(fst(f))
// Piping function simplification must be equivalen:
fst(f) >>> arr(fst) <-> arr(fst) >>> f
// Piped function must be commutative:
fst(f) >>> arr (id *** g) <-> arr(id *** g) >>> fst(f)
// Stacked bypasses can be flattened
fst(first(f)) >>> arr(assc) <-> arr(assc)
val f: String => Int = str => str.length
f("Hello") shouldEqual 5
Option("Hello").map(f) shouldEqual Option(5)
val f: String => Option[Int] = str =>
if(str.length <6) None
else Option(str.length)
f("Hello") shouldEqual None
f("Workday") shouldEqual Option(7)
val f: Option[String] => Int = str =>
str.map(_.toUpperCase).getOrElse("")
f(Option("Hello")) shouldEqual "HELLO"
f(None) shouldEqual ""
KleisliCategory Type-Class:
type Kleisli[M[+_], -A, +B] = A => M[A]
trait KleisliCategory[M[+_]]
extends Category[({type λ[-α, +β] = Kleisli[M, α, β]})#λ] {
// ...
}
KleisliCategory Type-Class (with Kind Projector plugin):
trait KleisliCategory[M[+_]]
extends Category[Kleisli[M, -?, +?]] {
// ...
}
KleisliArrow Type-Class:
type Kleisli[M[+_], -A, +B] = A => M[A]
trait KleisliCategory[M[+_]]
extends Category[Kleisli[M, -?, +?]] {
// ...
}
trait KleisliArrow[M[+_]]
extends Arrow[Kleisli[M, -?, +?]]
with KleisliCategory[M] {
// ...
}
But what is M[_] (only Option)?
Examples:
|
have many results |
|
sometimes have no result |
|
postponed result or may be no result |
|
write to log |
|
read from environment |
Functor & Monad (essence):
Functor : A => B => C[A] => C[B]
Monad : A => C[B] => C[A] => C[B]
CoMonad : C[A] => B => C[A] => C[B]
Arrow (essence):
Arrow : ( A => B ) >>> ( B => D ) => A => D
Kleisli : ( A => C[B]) >=> ( B => C[D]) => A => C[D]
CoKleisli : (C[A] => B ) =>= (C[B] => D ) => C[A] => D
For:
case class User(id: Int, name: String, password: String)
def getUserById: Int => Option[User] = ...
def updateDbUser: User => Option[Int] = ...
def updateName: String => User => Option[User] = ...
def updatePassword: String => User => Option[User] = ...
Monad usege:
def update(id: Int, updateUser: User => Option[User]): Option[User] =
getUserById(id).flatMap(user => updateUser(user)).flatMap(user => updateDbUser(user))
update(10, (user: User) => updateName(name).flatMap(updatePassword(password)))
Kleisli usege:
def update: (User => Option[User]) => Int => Option[User] =
updateFunc => Kleisli { getUserById } >==> updateFunc >==> updateDbUser
update(Kleisli { updateName(name) } >==> updatePassword(password))(10)
KleisliArrow Type-Class (based on Monad):
case class Kleisli[M[+ _], -A, +B](run: A => M[B])
trait KleisliCategory[M[+_]] extends Category[(Kleisli[M, -?, +?]] {
implicit def Monad: Monad[M]
def id[A]: Kleisli[M, A, A] = Kleisli(a => Monad.point(a))
def compose[A, B, C](bc: Kleisli[M, B, C], ab: Kleisli[M, A, B]): Kleisli[M, A, C] =
bc.flatMap(run(ac), k.run(_: B)))
}
trait KleisliArrow[M[+_]] extends Arrow[Kleisli[M, -?, +?]] with KleisliCategory[M] {
def arr[A, B](f: A => B): Kleisli[M, A, B] = Kleisli(a => Monad.point(f(a)))
def first[A, B, C](f: Kleisli[M, A, B]): Kleisli[M, (A, C), (B, C)] =
Kleisli[M, (A, C), (B, C)] { case (a, c) => Monad.map(f.run(a), (b: B) => (b, c)) }
}
CoKleisliArrow Type-Class (based on CoMonad):
case class Cokleisli[M[_], A, B](run: M[A] => B)
trait CokleisliCategory[M[+_]] extends Category[(Cokleisli[M, -?, +?]] {
implicit def Comonad: Comonad[M]
def id[A]: Cokleisli[M, A, A] = Cokleisli { ma => Comonad.extract(ma) }
def compose[A, B, C](bc: Cokleisli[M, B, C], ab: Cokleisli[M, A, B]): Cokleisli[M, A, C] =
Cokleisli { ma => ab.run(Comonad.extend(ma)(bc)) }
}
trait CokleisliArrow[M[+_]] extends Arrow[Cokleisli[M, -?, +?]] with CokleisliCategory[M] {
def arr[A, B](f: A => B): Cokleisli[M, A, B] = Cokleisli { ma => f(Comonad.extract(ma)) }
def first[A, B, C](f: Cokleisli[M, A, B]) =
Cokleisli[M, (A, C), (B, C)] {
w => (f.run(Comonad.map(w)(ac => ac._1)), Comonad.extract(w)._2)
}
}
And back to the Arrow
Motivation, is to find a generic interface for arrow wich cannot be based on monad.
ArrowMonad Type-Class:
trait ArrowApply[~>[-_, +_]] extends Arrow[~>] {
def app[A, B]: (A ~> B, A) ~> B
}
case class ArrowMonad[~>[-_, +_], +A](run: Unit ~> A)
implicit def _asMonad[~>[-_, +_]](implicit i: ArrowApply[~>]):
Monad[ArrowMonad[~>, +?]] = new Monad[ArrowMonad[~>, +?]] {
type M[+A] = ArrowMonad[~>, A]
def point[A](x: => A): M[A] = ArrowMonad { i.arr(_ => x) }
def flatMap[A, B](m: M[A], f: A => M[B]): M[B] =
ArrowMonad[~>, B] { m.run >>> i.arr((x: A) => (f(x).run, ())) >>> i.app[Unit, B] }
}
trait KleisliArrowApply[M[+ _]] extends ArrowApply[Kleisli[M, -?, +?]] {
private type ~>[-A, +B] = Kleisli[M, A, B]
def app[A, B]: (A ~> B, A) ~> B = Kleisli { case (f, a) => (f.run)(a) }
}
High order Arrow Definition (ArrowApply):
MonadArrow:
Id Category & (Co/)Monad Type-Class instances:
case class Id[A](a: A)
implicit def IdMonadInstance[A]: Monad[Id[A]] = ...
implicit def IdComonadInstance[A]: Comonad[Id[A]] = ...
Reader Type-Class:
type Reader[A, B] = Kleisli[Id, A, B]
// OR (completely symmetric with respect to Id Category)
type Reader[A, B] = Cokleisli[Id, A, B]
Reader transformer Type-Class:
type ReaderT[M[_], A, B] = Kleisli[M, A, B]