Few words about Kleisli category

Scala Meetup, October 2015, Dublin

by  Yuriy Polyulya  @ Workday

Baseline & Goals:

The most imaportant ideas in modern functional programming are:

  • It's all about data

    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.

  • Managing of side-effect

    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.

Category

url
url
url
url


Code:


val f: String => Int = str => str.length

f("Hello") shouldEqual 5
f("Bye")   shouldEqual 3
          
  • first category property:

url
url

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 "*******"
          
  • second category property:

url

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
          

Arrow

“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:


url

Combine:


url

Multiplex:


url

Merge:


url

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)
          

Kleisli & Co-Kleisli Category & Arrow

url

val f: String => Int = str => str.length

f("Hello")              shouldEqual 5
Option("Hello").map(f)  shouldEqual Option(5)
        
url

val f: String => Option[Int] = str =>
  if(str.length <6) None
  else Option(str.length)

f("Hello")    shouldEqual None
f("Workday")  shouldEqual Option(7)
        
url

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, -?, +?]] {
  // ...
}
          
Erik Osheim, Kind Projector on GitHub

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)?

  • All functions have effects, the things the function does. The simplest effect is just accepting parameters and returning a single value as a result.
  • Everything else we might conceive a function doing is a side-effect. By wrapping a value in a container, we can emulate all the various side-effects that are possible.

Examples:


A => List[B]
                  
have many results

A => Option[B]
                  
sometimes have no result

A => Future[B]
                  
postponed result or may be no result

A => (S, B)
                  
write to log

A => (Unit => E, B)
                  
read from environment

Kleisli Arrows & Monads

Common data manipulation techniques for dealing with side-effecting containers:

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
          

  • Arrows build a container for the whole functions, where Monads just give a common structure for their outputs.

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)
          

Kleisli & CoKleisli Arrow with Monad & Comonad

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):

url

MonadArrow:

url

Kleisli & CoKleisli Arrows and Id Category

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]
        

Arrows, Applicatives, Monads Summary

url
url
url
url
url

Used materials and References:

  • John Hughes. November 10, 1998, Generalising Monads to Arrows.
  • Chris Heunen and Bart Jacobs, Arrows, like Monads, are Monoids.
  • Ted Cooper (theod@pdx.edu), CS510 – Spring 2014, Arrow Basics.
  • John Hughes, S-41296 Sweden, Programming with Arrows.
  • Robert Atkey, LFCS, 2008, What is a Categorical Model of Arrows?
  • Kazuyuki Asada, Kyoto 606-8502, Japan, Arrows are Strong Monads.
  • K. Asada and I. Hasuo, (CMCS 2010)., 2010, Categorifying computations into components via arrows as profunctors.
  • Sam Lindley, Philip Wadler and Jeremy Yallop, 2008, Idioms are oblivious, arrows are meticulous, monads are promiscuous


Blogs & Blogposts:

  • Ruminations of a Programmer, Debasish Ghosh
  • Programming Cafe, Bartosz Milewski's
  • Questions?


  • Remarks?