Marcin Rzeźnicki
Scala Animal @VirtusLab
mrzeznicki@virtuslab.com
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
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)))
}
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)
}
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)) }
}
}
Kleisli arrows => A => M[B]
Choice arrows => Either
ArrowPlus => monoids
Loop arrows => output value fed back as input
ArrowApply == monad
ArrowError / Reader arrows .....
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:
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")
}
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)
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 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.
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, β] })#λ
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.
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]
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
}
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