Fun with arrows

Marcin Rzeźnicki

Scala Animal

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/

PROBLEM

Logika biznesowa napisana w "Scavie" - Java z trochę innymi słowami kluczowymi. Zazwyczaj:

  • imperatywny
  • komunikacja poprzez wyjątki
  • Unit
  • zerowe możliwości kompozycji

 

Logika utopiona w kodzie - niewidoczny flow (nawet jeżeli kod nie jest fatalny)

 

 

 

ANEGDOTA

Dostałem taki komentarz:

I know this is a bit of a religious thing in FP, but personally I don't think there is anything wrong with throwing exceptions. Of all the problems I have encountered in the years of programming, having to deal with exceptions and throwing exceptions was never a core problem. Throwing and catching exceptions is well defined even if it escapes explicit/compile-time types (so is pattern matching).

 

 

 

 

PROBLEM

Realistyczny przykład takiego stylu (to kiedyś był prawdziwy kod produkcyjny)

 

 

 

Jeśli chcemy używać FP, tego typu API nie ma sensu

WYMAGANIA

  • walidacja
  • brak wyjątków
  • logowanie
  • rozgałęzienia w logice

 

Chcemy nie tylko wyeliminować problemy - ale również zmienić kod na czysto funkcyjny

WYMAGANIA - WALIDACJA

  • załóżmy, że nie możemy dokonać żadnej modyfikacji jeśli status ustawiony jest na Done
  • nieuchronny copy-paste (?)

 

OBSERWACJE

Wszystkie metody napisane są wg tego samego wzorca: pobierz dane z bazy, sprawdź poprawność, zmodyfikuj encję, zapisz do bazy.

Czy kompozycja funkcji będzie w sam raz? Każda metoda może być przepisana jako: getFromDb andThen verify andThen copy andThen save.

Pozostaje problem z unit oraz wyjątki - potrzeba potężniejszej abstrakcji

 

 

 

 

INSPIRACJE

  • Railway Oriented Programming (http://fsharpforfunandprofit.com/rop/)
  • talk na Scala Days (https://www.parleys.com/tutorial/functional-programming-arrows)

STRZAŁKI - DEFINICJE

Wikipedia:

Arrows are a type class used in programming to describe computations in a pure and declarative fashion. [Arrows] provide a referentially transparent way of expressing relationships between logical steps in a computation. Unlike monads, arrows don’t limit steps to having one and only one input.

 

 

 

 

STRZAŁKI - DEFINICJE

Haskell wiki:

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

 

W praktyce: można rozumieć strzałki jako typ rozszerzający funkcję (=>) o dodatkowe kombinatory (poza compose i andThen)

Komponowanie strzałek jest bardziej ekspresywne i może zawierać bogatą semantykę np. komponowanie obliczeń monadycznych

 

 

STRZAŁKI - INTERFEJS

  • arr (s -> t) -> A s t
  • first (A s t) -> A (s,u) (t,u) / second (A s t) -> A (u, s) (u, t)
  • kompozycja: A s t >>> A t u -> A s u
  • scalenie: A s t *** A u v -> A (s,u) (t,v)
  • rozdzielenie: A s t &&& A s u -> A s (t, u)

STRZAŁKI - INTERFEJS

KOCHAMY SYMBOLICZNE OPERATORY

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




KOCHAMY SYMBOLICZNE OPERATORY ?

Part of my problem with Arrows is the names most people give the functions. andThen and orElse I understand; their names reflect their meaning in some way. *** and &&& not so much.

 

Interesting read, except for accumulating =>:, >>>, <<<, ***, &&&, >>=, >=>, >==>, <=<, <==<. Looks like ancient sbt to me ;)

 

 

 

 

KOCHAMY SYMBOLICZNE OPERATORY !

Yeah, well, there are two camps of thought on symbolic operator. It's love or hate, nothing in between :-) I tend to like them because they're concise, but I totally understand people who find them cryptic. At least one could argue that things like '***' have somewhat established meaning and that helps to avoid cognitive overload, don't they?

 

I have no objection to both being available.

 

 

 

 

FUNKCJA TO STRZAŁKA

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

FLOW

WYMAGANIE-WALIDACJA

private def productionLotArrow[Env](verify: (ProductionLot, Env) => Unit,
                                       copy: (ProductionLot, Env) => ProductionLot): 
                                       (Long, Env) => Long = {
  val verifyProductionLotNotDoneF: ((ProductionLot, Env)) => Unit = { 
    case (pl, _) => verifyProductionLotNotDone(pl) 
  }

  Function.untupled(
    (productionLotsRepository.findExistingById _ *** identity[Env])
      >>> ((verify.tupled &&& verifyProductionLotNotDoneF)
        &&& (copy.tupled >>> productionLotsRepository.save))
        >>> (_._2)
  )
}

private def verifyProductionLotNotDone(productionLot: ProductionLot): Unit =
  require(productionLot.status != ProductionLotStatus.Done, 
          "Attempt to operate on finished production lot")

ANEGDOTA

Kolejny komentarz:

And so it happens that the original half-functional code looks the most readable and easy to grasp version. And yeah, I can copy and paste that to insert another verification…

 

 

 

 

WYJĄTKI = EITHER

Obsługa błędów staje się częścią control flow.

Umożliwia bycie type-safe, pure etc. mimo przykrej konieczności obsługiwania błędów.

Przepiszmy wszystkie miejsca gdzie chcemy "rzucić" wyjątek z użyciem Either.

Możemy stworzyć ADT do reprezentowania różnych przypadków

 

 

 

 

OBSERWACJE

Strzałka się nie kompiluje :-(

Nie ma tego jak naprawić, bo kompozycja funkcji zwracających Either jest średnio wykonalna (każda składowa funkcja musiałaby umieć "odpakować" rezultat z Either - masa boilerplate'u).

Monads do not compose

 

 

 

 

 

OBSERWACJE

Abstrakcja strzałki jest w stanie wyrazić wiele "typów" obliczeń. Nie ma żadnych ograniczeń co do typów na jakich operujemy - musimy umieć tylko wyrazić obliczenia na tych typów za pomocą kombinatorów first/second oraz compose

Funkcje A => B są ewidentnie niewystarczające w obliczu monady, ale istnieją inne abstrakcje, w których monadyczne obliczenie jest łatwo wyrażalne.

Są one znane jako ...

 

 

 

 

STRZAŁKI KLEISLIEGO

Mówiąc językiem Scali:

Strzałki Kleisliego są implementacją klasy typów (typeclass) strzałek w oparciu o funkcje A => M[B], gdzie M jest instancją monady

 

 

 

 

A CO TO JEST MONADA?

:-)

CO TO JEST MONADA?

trait Monad[M[_]] {
  def point[A](a: => A): M[A]
  def bind[A, B](ma: M[A])(f: A => M[B]): M[B]
  def fmap[A, B](ma: M[A])(f: A => B): M[B]
}

final class MonadOps[M[_], A](val self: M[A])(implicit val monad: Monad[M]) {
  def >>=[B](f: A => M[B]) = monad.bind(self)(f)
}

object Monad extends MonadInstances {
  implicit def ToMonadOps[M[_], A](v: M[A])(implicit m: Monad[M]): MonadOps[M, A] = 
    new MonadOps(v)
}

SCALA WOES

Either w scala.util nie jest monadą:

  • brak tzw. biasu
  • type kind monady to * -> *, Either : * -> * -> * (w Scali, w przeciwieństwie do np. Haskella nie ma curryingu na poziomie konstruktora typu)

 

 

 

 

OBSERWACJE

Wprawdzie scala.util.Either nie jest monadą, ale bardzo łatwo można to naprawić odpowiednio implementując instancję klasy typów (albo używamy scalaz). Oba problemy załatwiamy za jednym posunięciem:

 

 

 

 

implicit def eitherMonad[L] = new Monad[({ type λ[β] = Either[L, β] })#λ] {
  override def point[A](a: => A): Either[L, A] = Right(a)

  override def bind[A, B](ma: Either[L, A])(f: (A) => Either[L, B]): Either[L, B] = 
    ma.right.flatMap(f)

  override def fmap[A, B](ma: Either[L, A])(f: (A) => B): Either[L, B] = 
    ma.right.map(f)
}

SCALA WOES

Type lambdy

  • piekielna składnia:

 

  • 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.

 

 

 

 

implicit def ToArrowOpsFromKleisliLike[G[_], F[G[_], _, _], A, B](v: F[G, A, B])(implicit 
    arr: Arrow[({ type λ[α, β] = F[G, α, β] })#λ]) =
    new ArrowOps[({ type λ[α, β] = F[G, α, β] })#λ, A, B](v)

OBSERWACJE

Składnię można naprawić, dzięki projektowi kind-projector. Polecany plugin do kompilatora, dla lubiących "zabawę z typami":

 

 

 

 

 

 

-  implicit def eitherMonad[L] = new Monad[({ type λ[β] = Either[L, β] })#λ] {
+  implicit def eitherMonad[L] = new Monad[Either[L, ?]] {

FLOW Z WYJĄTKAMI

WYMAGANIE - WYJĄTKI

type E[R] = Either[Error, R]

private def productionLotArrow[Env](verify: (ProductionLot, Env) => E[ProductionLot],
                                       copy: (ProductionLot, Env) => ProductionLot): 
                                       Env => Long => E[Long] = {
  val verifyProductionLotNotDoneF: (ProductionLot) => E[ProductionLot] = 
    verifyProductionLotNotDone

  (env: Env) => (
    Kleisli[E, Long, ProductionLot]{ productionLotsRepository.findExistingById }
    >>> Kleisli { verify(_, env) }
    >>> Kleisli { verifyProductionLotNotDoneF }
  ).map(copy(_, env)) >==> productionLotsRepository.save _
}

+ MAŁY REFACTOR

private def productionLotArrow[Env](verify: (ProductionLot, Env) => Either[Error, ProductionLot],
                                       copy: (ProductionLot, Env) => ProductionLot): 
                                       Env => Long => Either[Error, Long] = {
    type Track[T] = Either[Error, T]
    def track[A, B](f: A => Track[B]) = Kleisli(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
}

SCALA WOES

SI-2712 - bug, którego numer pamiętam:

  • czemu musimy używać aliasu Kleisli[Track, A, B]?
  • [error] — because —
    [error] argument expression’s type is not compatible with formal parameter type;
    [error] found : org.virtuslab.blog.kleisli.ProductionLot =>
    Either[org.virtuslab.blog.kleisli.Error,org.virtuslab.blog.kleisli.ProductionLot]
    [error] required: ?A => ?M[?B]

 

 

 

 

SCALA WOES

SI-2712 - bug, którego numer pamiętam:

  • to czemu nie napisaliśmy np. 
    Kleisli[Either[Error, ?], Long, ProductionLot]?
  • [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]

 

 

 

 

SCALA WOES

SI-2712 - bug, którego numer pamiętam:

  • ale przecież istnieje unifikacja!
    F[_, _, _] / Kleisli, 
    G[_] / [R]scala.util.Either[org.virtuslab.blog.kleisli.Error,R], 
    A / Long, 
    B/ ProductionLot
    

 

 

 

 

WYMAGANIE - LOGOWANIE

Problematyczna operacja z funkcjonalnego punktu widzenia:

  • String => Unit - nie ma użytecznej wartości zwracanej, side-effect
  • musimy wprowadzić możliwość zrobienia "reroutingu" flow "wokół" tego typu funkcji (dead-end)

 

 

 

 

WYMAGANIE - LOGOWANIE

WYMAGANIE - LOGOWANIE

WYBÓR

Strzałka wyboru: ekwiwalent if w for-comprehension.

Dodatkowe kombinatory:

  • left - ekwiwalent LeftProjection na Either
  • right
  • +++
  • |||

 

 

 

 

WYBÓR

WYBÓR

Przykładowa implementacja strzałki wyboru dla funkcji

 

 

 

 

 


  implicit object FunctionArrow extends Arrow[Function1] with Choice[Function1] {

// ...

    override def left[A, B, C](f: (A) => B): (Either[A, C]) => Either[B, C] = 
      _.left.map(f)

    override def right[A, B, C](f: (A) => B): (Either[C, A]) => Either[C, B] = 
      _.right.map(f)

    override def multiplex[A, B, C, D](f: (A) => B, g: (C) => D): 
                                        (Either[A, C]) => Either[B, D] = {
      case Left(a)  => Left(f(a))
      case Right(c) => Right(g(c))
    }

    override def fanin[A, B, C](f: (A) => C, g: (B) => C): (Either[A, B]) => C = 
      _.fold(f, g)
  }

WYMAGANIA


  private val logger = Logger.getLogger(this.getClass.getName)

  private def productionLotArrow[Env](verify: (ProductionLot, Env) => Either[Error, ProductionLot],
                                         copy: (ProductionLot, Env) => ProductionLot,
                                         log: (ProductionLot, Env) => Unit): 
                                         Env => Long => Either[Error, Long] = {
    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 }

    val logError: Error => Unit = error => 
      logger.warning(s"Cannot perform operation on production lot: $error")
    val logSuccess: Long => Unit = id => 
      logger.fine(s"Production lot $id updated")

    (env: Env) =>
      ((
        getFromDb -| (log(_, env))
        >>> validate(env)).map(copy(_, env))
        >>> save)
        .run -| (logError ||| logSuccess)
  }

DALSZE PRACE

  • Różne monady - naturalna transformata, lifting (GitHub)
  • Eliminacja explicit env - Reader monad + EitherT, transformatory strzałek
  • Stworzenie biblioteki

DZIĘKUJĘ

Fun with Arrows@chambers

By Marcin Rzeźnicki

Fun with Arrows@chambers

  • 905