Fun with Arrows

Marcin Rzeźnicki

Scala Animal @VirtusLab

mrzeznicki@virtuslab.com

FUN WITH ARROWS

  • https://github.com/VirtusLab/kleisli-examples
  • http://virtuslab.com/blog/arrows-monads-and-kleisli-part-i/
  • http://virtuslab.com/blog/arrows-monads-and-kleisli-part-ii/

WHAT'S AN ARROW?

Hughes, John (May 2000). "Generalising Monads to Arrows"

Haskell wiki: Arrow a b c represents a process that takes as input something of type b and outputs something of type c.

Arrow is a generalization of function i.e. it is an abstraction of things that behave like functions

You can treat arrow as a function type that gives you more combinators to work with than function’s compose or andThen

ARROW INTERFACE

... IN SCALA

trait Arrow[=>:[_, _]] {
  def id[A]: A =>: A
  def arr[A, B](f: A => B): A =>: B

  def compose[A, B, C](fbc: B =>: C, fab: A =>: B): A =>: C

  def first[A, B, C](f: A =>: B): (A, C) =>: (B, C)
  def second[A, B, C](f: A =>: B): (C, A) =>: (C, B)

  def merge[A, B, C, D](f: A =>: B, g: C =>: D): (A, C) =>: (B, D) = 
        compose(first(f), second(g))
  def split[A, B, C](fab: A =>: B, fac: A =>: C): A =>: (B, C) = 
        compose(merge(fab, fac), arr((x: A) => (x, x)))
}

+ FANCY OPS

final class ArrowOps[=>:[_, _], A, B](val self: A =>: B)(implicit val arr: Arrow[=>:]) {
  def >>>[C](fbc: B =>: C): A =>: C = arr.compose(fbc, self)

  def <<<[C](fca: C =>: A): C =>: B = arr.compose(self, fca)

  def ***[C, D](g: C =>: D): (A, C) =>: (B, D) = arr.merge(self, g)

  def &&&[C](fac: A =>: C): A =>: (B, C) = arr.split(self, fac)
}

object Arrow extends ArrowInstances {
  implicit def ToArrowOps[F[_, _], A, B](v: F[A, B])(implicit arr: Arrow[F]): 
        ArrowOps[F, A, B] = new ArrowOps(v)
}

FUNCTION IS ARROW

trait ArrowInstances {
  // function is arrow
  implicit object FunctionArrow extends Arrow[Function1] {
    override def id[A]: A => A = identity[A]

    override def arr[A, B](f: (A) => B): A => B = f

    override def compose[A, B, C](fbc: B => C, fab: A => B): A => C = 
        fbc compose fab

    override def first[A, B, C](f: A => B): ((A, C)) => (B, C) = prod => 
        (f(prod._1), prod._2)

    override def second[A, B, C](f: A => B): ((C, A)) => (C, B) = prod => 
        (prod._1, f(prod._2))

    override def merge[A, B, C, D](f: (A) => B, g: (C) => D): ((A, C)) => (B, D) = 
        { case (x, y) => (f(x), g(y)) }
  }
}

... BUT ARROWS COME IN VARIOUS FLAVORS

Kleisli arrows => A => M[B]

Choice arrows => Either

ArrowPlus => monoids

Loop arrows => output value fed back as input

ArrowApply == monad

ArrowError / Reader arrows .....

Arrows as a bridge between imperative and functional

Let's take "Java code with slightly different keywords", and try to refactor it using  arrows.

Such code is often littered with problems, including but not limited to:

  • not composable (hard to add a new requirements, flow hard to follow)
  • exceptions control flow
  • side-effects

REFACTOR THIS...


  def changeAssignedWorker(productionLotId: Long, newWorkerId: Long): Unit = {
    val productionLot = productionLotsRepository.findExistingById(productionLotId)

    verifyWorkerChange(productionLot, newWorkerId)

    val updatedProductionLot = productionLot.copy(workerId = Some(newWorkerId))

    productionLotsRepository.save(updatedProductionLot)
  }

  private def verifyWorkerCanBeAssignedToProductionLot(productionLot: ProductionLot, workerId: Long): Unit = {
    val productionLotId = productionLot.id.get
    val productionLotHasNoWorkerAssigned = productionLot.workerId.isEmpty

    require(productionLotHasNoWorkerAssigned, s"Production lot: $productionLotId has worker already assigned")
  }

INTO

REPLACING EXCEPTIONS WITH EITHER

makes error handling the part of normal control flow and allows for a fully type-safe and functional code in the presence of errors

 

Problem: no easy way to compose these functions (every function would have to unpack a value from Either and pack result back into a new instance of Either)

REPLACING EXCEPTIONS WITH EITHER

Many different computation types can be expressed using arrows.

Arrow does not impose restrictions on underlying type as long as you can come up with implementations of abstract operations that arrow typeclass needs ie. first/second and compose. While ordinary A => B functions are clearly not sufficient to express our new idea, there exists another abstraction designed to do that. It is called Kleisli

KLEISLI ARROWS

Kleisli represents monadic computations i.e. functions of type A => M[B].

 

We’ll be able to implement arrow on Kleisli, creating so called Kleisli arrows ie. arrows that can compose functions of type A => M[B] as long as M is a monad.

DETOUR: TYPE LAMBDAS

one of the most loathed Scala features :-)

 

Monad is of kind * -> * while Either‘s kind is * -> * -> * (that’s fancy way of saying that Monad type constructor takes one type parameter while Either takes two)

 

Solution:({ type λ[β] = Either[L, β] })#λ

DETOUR: TYPE LAMBDAS

But type lambdas do not work :-(

 

Quoting Paul Philips:

Type lambdas are cool and all, but not a single line of the compiler was ever written with them in mind. They’re just not going to work right: the relevant code is not robust.

DETOUR: TYPE LAMBDAS

One of the most famous compiler bugs: SI-2712

 

You need type alias to use functions returning Either as Kleisli

[info] — because —
[info] argument expression’s type is not compatible with formal parameter type;
[info] found :
org.virtuslab.blog.kleisli.Kleisli[[R]scala.util.Either[org.virtuslab.blog.kleisli.Error,R],Long,org.virtuslab.blog. kleisli.ProductionLot]
[info] required: ?F[?G, ?A, ?B]

DETOUR: UNAPPLY TRICK

trait Unapply[TC[_[_]], MA] {

  /** The type constructor */
  type M[_]

  /** The type that `M` was applied to */
  type A

  /** The instance of the type class */
  def TC: TC[M]

  /** Compatibility. */
  @inline final def apply(ma: MA): M[A] = ???
}

  implicit def unapplyMFABC3[TC[_[_]], F[_], M0[F[_], _, _, _], A0, B0, C0]
    (implicit TC0: TC[M0[F, A0, B0, ?]]): Unapply[TC, M0[F, A0, B0, C0]] {
    type M[X] = M0[F, A0, B0, X]
    type A = C0
  } =
    new Unapply[TC, M0[F, A0, B0, C0]] {
      type M[X] = M0[F, A0, B0, X]
      type A = C0
      def TC = TC0
      def leibniz = refl
    }

RAILWAY ORIENTED PROGRAMMING

RAILWAY ORIENTED PROGRAMMING

Idea taken from: http://fsharpforfunandprofit.com/rop/

 

Arrows can express this too

    type Track[T] = Either[Error, T]
    def track[A, B](f: A => Track[B]) = Kleisli[Track, A, B](f)

    val getFromDb = track { productionLotsRepository.findExistingById }
    val validate = (env: Env) => track { verify(_: ProductionLot, env) } >>> 
                                 track { verifyProductionLotNotDone }
    val save = track { productionLotsRepository.save }

    (env: Env) => (
      getFromDb
      >>> validate(env)
    ).map(copy(_, env)) >>> save

DEALING WITH SIDE EFFECTS

OTHER GOODIES

  • mixing monads
  • ChoiceArrow
  • Arrow transformers

THANKS

Fun with Arrows

By Marcin Rzeźnicki

Fun with Arrows

  • 960