Slide 1

Slide 1 text

Functional Programming Patterns (for the pragmatic programmer) ~ @raulraja CTO @47deg

Slide 2

Slide 2 text

Acknowledgment • Scalaz • Rapture : Jon Pretty • Miles Sabin : Shapeless • Rúnar Bjarnason : Compositional Application Architecture With Reasonably Priced Monads • Noel Markham : A purely functional approach to building large applications • Jan Christopher Vogt : Tmaps

Slide 3

Slide 3 text

Functions are first class citizens in FP Architecture

Slide 4

Slide 4 text

I want my main app services to strive for • Composability • Dependency Injection • Interpretation • Fault Tolerance

Slide 5

Slide 5 text

Composability Composition gives us the power to easily mix simple functions to achieve more complex workflows.

Slide 6

Slide 6 text

Composability We can achieve monadic function composition with Kleisli Arrows A 㱺 M[B] In other words a function that for a given input it returns a type constructor… List[B], Option[B], Either[B], Task[B], Future[B]…

Slide 7

Slide 7 text

Composability When the type constructor M[_] it's a Monad it can be composed and sequenced in for comprehensions val composed = for { a <- Kleisli((x : String) 㱺 Option(x.toInt + 1)) b <- Kleisli((x : String) 㱺 Option(x.toInt * 2)) } yield a + b

Slide 8

Slide 8 text

Composability The deferred injection of the input parameter enables Dependency Injection val composed = for { a <- Kleisli((x : String) 㱺 Option(x.toInt + 1)) b <- Kleisli((x : String) 㱺 Option(x.toInt * 2)) } yield a + b composed.run("1")

Slide 9

Slide 9 text

Composability : Kleisli What about when the args are not of the same type? val composed = for { a <- Kleisli((x : String) 㱺 Option(x.toInt + 1)) b <- Kleisli((x : Int) 㱺 Option(x * 2)) } yield a + b

Slide 10

Slide 10 text

Composability : Kleisli By using Kleisli we just achieved • Composability • Dependency Injection • Interpretation • Fault Tolerance

Slide 11

Slide 11 text

Interpretation : Free Monads What is a Free Monad? -- A monad on a custom ADT that can be run through an Interpreter

Slide 12

Slide 12 text

Interpretation : Free Monads sealed trait Op[A] case class Ask[A](a: () 㱺 A) extends Op[A] case class Async[A](a: () 㱺 A) extends Op[A] case class Tell(a: () 㱺 Unit) extends Op[Unit]

Slide 13

Slide 13 text

Interpretation : Free Monads What can you achieve with a custom ADT and Free Monads? def ask[A](a: 㱺 A): OpMonad[A] = Free.liftFC(Ask(() 㱺 a)) def async[A](a: 㱺 A): OpMonad[A] = Free.liftFC(Async(() 㱺 a)) def tell(a: 㱺 Unit): OpMonad[Unit] = Free.liftFC(Tell(() 㱺 a))

Slide 14

Slide 14 text

Interpretation : Free Monads Functors and Monads for Free (No need to manually implement map, flatMap, etc...) type OpMonad[A] = Free.FreeC[Op, A] implicit val MonadOp: Monad[OpMonad] = Free.freeMonad[({type λ[α] = Coyoneda[Op, α]})#λ]

Slide 15

Slide 15 text

Interpretation : Free Monads At this point a program like this is nothing but Data describing the sequence of execution but FREE of it's runtime interpretation. val program = for { a <- ask(1) b <- async(2) _ <- tell(println("log something")) } yield a + b

Slide 16

Slide 16 text

Interpretation : Free Monads We isolate interpretations via Natural transformations AKA Interpreters. In other words with map over the outer type constructor Op object ProdInterpreter extends (Op ~> Task) { def apply[A](op: Op[A]) = op match { case Ask(a) 㱺 Task(a()) case Async(a) 㱺 Task.fork(Task.delay(a())) case Tell(a) 㱺 Task.delay(a()) } }

Slide 17

Slide 17 text

Interpretation : Free Monads We can have different interpreters for our production / test / experimental code. object TestInterpreter extends (Op ~> Id.Id) { def apply[A](op: Op[A]) = op match { case Ask(a) 㱺 a() case Async(a) 㱺 a() case Tell(a) 㱺 a() } }

Slide 18

Slide 18 text

Requirements • Composability • Dependency Injection • Interpretation • Fault Tolerance

Slide 19

Slide 19 text

Fault Tolerance Most containers and patterns generalize to the most common super-type or simply Throwable loosing type information. val f = scala.concurrent.Future.failed(new NumberFormatException) val t = scala.util.Try(throw new NumberFormatException) val d = for { a <- 1.right[NumberFormatException] b <- (new RuntimeException).left[Int] } yield a + b

Slide 20

Slide 20 text

Fault Tolerance We don't have to settle for Throwable!!! We could use instead… • Nested disjunctions • Coproducts • Delimited, Monadic, Dependently-typed, Accumulating Checked Exceptions

Slide 21

Slide 21 text

Fault Tolerance : Dependently- typed Acc Exceptions Introducing rapture.core.Result

Slide 22

Slide 22 text

Fault Tolerance : Dependently- typed Acc Exceptions Result is similar to \/ but has 3 possible outcomes (Answer, Errata, Unforeseen) val op = for { a <- Result.catching[NumberFormatException]("1".toInt) b <- Result.errata[Int, IllegalArgumentException]( new IllegalArgumentException("expected")) } yield a + b

Slide 23

Slide 23 text

Fault Tolerance : Dependently- typed Acc Exceptions Result uses dependently typed monadic exception accumulation val op = for { a <- Result.catching[NumberFormatException]("1".toInt) b <- Result.errata[Int, IllegalArgumentException]( new IllegalArgumentException("expected")) } yield a + b

Slide 24

Slide 24 text

Fault Tolerance : Dependently- typed Acc Exceptions You may recover by resolving errors to an Answer. op resolve ( each[IllegalArgumentException](_ 㱺 0), each[NumberFormatException](_ 㱺 0), each[IndexOutOfBoundsException](_ 㱺 0))

Slide 25

Slide 25 text

Fault Tolerance : Dependently- typed Acc Exceptions Or reconcile exceptions into a new custom one. case class MyCustomException(e : Exception) extends Exception(e.getMessage) op reconcile ( each[IllegalArgumentException](MyCustomException(_)), each[NumberFormatException](MyCustomException(_)), each[IndexOutOfBoundsException](MyCustomException(_)))

Slide 26

Slide 26 text

Requirements We have all the pieces we need Let's put them together! • Composability • Dependency Injection • Interpretation • Fault Tolerance

Slide 27

Slide 27 text

Solving the Puzzle How do we assemble a type that is: Kleisli + Custom ADT + Result for { a <- Kleisli((x : String) 㱺 ask(Result.catching[NumberFormatException](x.toInt))) b <- Kleisli((x : String) 㱺 ask(Result.catching[IllegalArgumentException](x.toInt))) } yield a + b We want a and b to be seen as Int but this won't compile because there are 3 nested monads

Slide 28

Slide 28 text

Solving the Puzzle : Monad Transformers Monad Transformers to the rescue! type ServiceDef[D, A, B <: Exception] = ResultT[({type λ[α] = ReaderT[OpMonad, D, α]})#λ, A, B]

Slide 29

Slide 29 text

Solving the Puzzle : Services Two services with different dependencies case class Converter() { def convert(x: String): Int = x.toInt } case class Adder() { def add(x: Int): Int = x + 1 } case class Config(converter: Converter, adder: Adder) val system = Config(Converter(), Adder())

Slide 30

Slide 30 text

Solving the Puzzle : Services Two services with different dependencies def service1(x : String) = Service { converter: Converter 㱺 ask(Result.catching[NumberFormatException](converter.convert(x))) } def service2 = Service { adder: Adder 㱺 ask(Result.catching[IllegalArgumentException](adder.add(22) + " added ")) }

Slide 31

Slide 31 text

Solving the Puzzle : Services Two services with different dependencies val composed = for { a <- service1("1").liftD[Config] b <- service2.liftD[Config] } yield a + b composed.exec(system)(TestInterpreter) composed.exec(system)(ProdInterpreter)

Slide 32

Slide 32 text

Conclusion • Composability : Kleisli • Dependency Injection : Kleisli • Interpretation : Free monads • Fault Tolerance : Dependently typed checked exceptions

Slide 33

Slide 33 text

Thanks! @raulraja @47deg http://github.com/47deg/func-architecture