Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Boring Usecases for Exciting Types

Itamar Ravid
October 26, 2018

Boring Usecases for Exciting Types

Functional programming, in Scala and in general, is very jargon-y. Words and co-Words such as indexed state transformers, codensity monads and divisible functors are impressive, but without making effort to place them in our day-to-day context, they remain an academic interest. That's a shame, though - these data types and type classes can really make our life easier!

In this talk, I will take 3 plain and business-y use cases and show how they can benefit from some lesser known constructs from functional programming. Boilerplate will be slain, elegance will ensue and hopefully - you will find them useful enough to incorporate in your day-to-day work!

Itamar Ravid

October 26, 2018
Tweet

More Decks by Itamar Ravid

Other Decks in Programming

Transcript

  1. About me • Freelance software developer • Doing functional programming,

    microservices, DevOps • Co-organize the Underscore Scala meetup • Hit me up if you want to work together! Itamar Ravid - @itrvd 2
  2. Agenda • Why I wrote this talk • Three "boring"

    use-cases and types that can help Itamar Ravid - @itrvd 3
  3. Classic "draw an owl" problem How do we go from

    this: quicksort [] = [] quicksort (p:xs) = (quicksort lesser) ++ [p] ++ (quicksort greater) where lesser = filter (< p) xs greater = filter ( >= p) xs Itamar Ravid - @itrvd 9
  4. Classic "draw an owl" problem To this: -- | Fokkinga's

    postpromorphism postpro :: Recursive t => (forall b. Base t b -> Base t b) -- natural transformation -> (a -> Base t a) -- a (Base t)-coalgebra -> a -- seed -> t postpro e g = a where a = embed . fmap (hoist e . a) . g Itamar Ravid - @itrvd 10
  5. Actual agenda Purely functional and type-safe: • Time measurement and

    instrumentation • State machines • Resource allocation and release • Let's go! Itamar Ravid - @itrvd 13
  6. Examples ahead assume: // build.sbt scalacOptions += "-Ypartial-unification" addCompilerPlugin("com.olegpy" %%

    "better-monadic-for" % "0.2.4") // *.scala import cats._, cats.data._, cats.implicits._ import cats.effect._ Itamar Ravid - @itrvd 14
  7. Time measurement and instrumentation Let's start with a warm-up! Problem:

    I have a bunch of computations, and I want to measure their execution time. def computeCv(data: Data): IO[Double] = for { sample <- computeSample(data) mean <- computeMean(sample) variance <- computeVariance(sample) } yield mean / variance Itamar Ravid - @itrvd 15
  8. Time measurement and instrumentation Non-solution: manually instrument. def computeCv(data: Data):

    IO[Double] = for { startTime <- time() sample <- computeSample(data) sampleTime <- time() mean <- computeMean(sample) meanTime <- time() variance <- computeVariance(sample) varTime <- time() } yield mean / variance Itamar Ravid - @itrvd 16
  9. Time measurement and instrumentation To start, we define a timed

    combinator: def timed[A](fa: IO[A]): IO[(FiniteDuration, A)] = for { startTime <- time() a <- fa endTime <- time() } yield ((endTime - startTime).millis, a) fa is pure, so we can just pass it around. Itamar Ravid - @itrvd 18
  10. Time measurement and instrumentation We can also name this computation:

    def timed[A](fa: IO[A], name: String): IO[((String, FiniteDuration), A)] = for { startTime <- time() a <- fa endTime <- time() } yield (name -> (endTime - startTime).millis, a) Itamar Ravid - @itrvd 19
  11. Time measurement and instrumentation We can now use our new

    timed function: def computeCv(data: Data): IO[Double] = for { (sampleTime, sample) <- timed(computeSample(data), "sample") (meanTime, mean) <- timed(computeMean(sample), "mean") (varTime, variance) <- timed(computeVariance(sample), "var") } yield mean / variance Kinda tedious though! Itamar Ravid - @itrvd 20
  12. Time measurement and instrumentation To save us from that bookeeping,

    comes WriterT: case class WriterT[F[_], Log, Value]( run: F[(Log, Value)] ) Itamar Ravid - @itrvd 21
  13. Time measurement and instrumentation To save us from that bookeeping,

    comes WriterT: case class WriterT[F[_], Log, Value]( run: F[(Log, Value)] ) To be honest, WriterT is nothing more than a tuple. Itamar Ravid - @itrvd 22
  14. Tuples are magical! We can map them: type Metadata =

    List[String] type Annotated[A] = (Metadata, A) case class Person(name: String, age: Int) val person: Annotated[Person] = (List(), Person("Bob", 23)) val name: Annotated[String] = person.map(_.name) Itamar Ravid - @itrvd 23
  15. Tuples are magical! We can traverse them: def persistPerson(p: Person):

    IO[Int] val persistedId: IO[Annotated[Int]] = person.traverse(persistPerson) Itamar Ravid - @itrvd 24
  16. Tuples are magical! And if we have a Semigroup for

    the Metadata, we can flatMap too! val person: Annotated[Person] = (List("log1"), Person("Bob", 23)) val flatMapped: Annotated[Person] = for { p <- person newP <- (List("log2"), p.copy(age = 24)) } yield newP Itamar Ravid - @itrvd 25
  17. Tuples are magical! And if we have a Semigroup for

    the Metadata, we can flatMap too! val person: Annotated[Person] = (List("log1"), Person("Bob", 23)) val flatMapped: Annotated[Person] = for { p <- person newP <- (List("log2"), p.copy(age = 24)) } yield newP flatMapped = (List("log1", "log2"), Person("Bob", 24)) Itamar Ravid - @itrvd 26
  18. Back to WriterT WriterT is identical, apart from the additional

    effect: case class WriterT[F[_], Log, Value]( run: F[(Log, Value)] ) Itamar Ravid - @itrvd 27
  19. Time measurement and instrumentation Here's the type of our timed

    program: timedProgram: IO[((String, FiniteDuration), A)] Itamar Ravid - @itrvd 28
  20. Time measurement and instrumentation Here's the type of our timed

    program: timedProgram: IO[((String, FiniteDuration), A)] Adapted to WriterT, this is: timedProgram: WriterT[IO, (String, FiniteDuration), A] Itamar Ravid - @itrvd 29
  21. Time measurement and instrumentation Here's the type of our timed

    program: timedProgram: IO[((String, FiniteDuration), A)] Adapted to WriterT, this is: timedProgram: WriterT[IO, (String, FiniteDuration), A] This won't (sensibly) work! Itamar Ravid - @itrvd 30
  22. Time measurement and instrumentation Let's just use a map. type

    Timed[A] = WriterT[IO, Map[String, FiniteDuration], A] def timed(name: String, fa: IO[A]): Timed[A] = WriterT { for { startTime <- time() a <- fa endTime <- time() } yield (Map(name -> (endTime - startTime).millis), a) } Itamar Ravid - @itrvd 31
  23. Time measurement and instrumentation Our original program is only slightly

    changed: def computeCv(data: Data): Timed[Double] = for { sample <- timed(computeSample(data), "sample") mean <- timed(computeMean(sample), "mean") variance <- timed(computeVariance(sample), "var") } yield mean / variance Itamar Ravid - @itrvd 32
  24. Time measurement and instrumentation If you fancy infix syntax, that

    works too (implicit class): def computeCv(data: Data): Timed[Double] = for { sample <- computeSample(data).timed("sample") mean <- computeMean(sample).timed("mean") variance <- computeVariance(sample).timed("var") } yield mean / variance Itamar Ravid - @itrvd 33
  25. Time measurement and instrumentation To unpack the timing data, we

    can define another function: implicit class TimedIOOps[A](fa: Timed[A]) { def logTimings: IO[A] = for { (timing, a) <- timed.run _ <- IO(println(timing)) } yield a } Itamar Ravid - @itrvd 34
  26. Time measurement and instrumentation And use it: def computeCv(data: Data):

    IO[Double] = (for { sample <- computeSample(data).timed("sample") mean <- computeMean(sample).timed("mean") variance <- computeVariance(sample).timed("var") } yield mean / variance).logTimings Itamar Ravid - @itrvd 35
  27. Summary • WriterT is a good example of making auxillary

    details ambient • Other things you can do with it: • Accumulate events to emit when doing event sourcing; • Accumulate errors when emitting partial results; • and more and more! Itamar Ravid - @itrvd 36
  28. Safe state machines Problem: I want to safely model an

    authentication protocol: case class Connection(socket: Socket) trait Authentication { def connect(): IO[Connection] def handshake(connection: Connection): IO[Unit] = for { _ <- IO(connection.socket.write(HandshakeMagic)) response <- IO(connection.socket.read(HandshakeResponseLen)) _ <- verifyHandshake(response) } yield () def sendCreds(connection: Connection): IO[Unit] def authenticate(connection: Connection): IO[Unit] } Itamar Ravid - @itrvd 37
  29. Safe state machines Sure, I can just use the functions:

    val api: Authentication import api._ val auth: IO[Unit] = for { conn <- connect() _ <- handshake(conn) _ <- authenticate(conn) _ <- sendCreds(conn) } yield () Itamar Ravid - @itrvd 38
  30. Safe state machines But there's a bug here! val api:

    Authentication import api._ val auth: IO[Unit] = for { conn <- connect() _ <- handshake(conn) _ <- authenticate(conn) _ <- sendCreds(conn) } yield () Itamar Ravid - @itrvd 39
  31. Safe state machines Static types to the rescue! We will

    modify Connection: case class Handshake() case class Credentials() case class Auth() case class Done() case class Connection[S](socket: Socket) Itamar Ravid - @itrvd 40
  32. Safe state machines And the API: trait Authentication { def

    connect(): IO[Connection[Handshake]] def handshake(connection: Connection[Handshake]): IO[Connection[Credentials]] def sendCreds(connection: Connection[Credentials]): IO[Connection[Auth]] IO(println("Sending credentials!")) *> IO.pure(connection.copy()) def authenticate(connection: Connection[Auth]): IO[Connection[Done]] } Itamar Ravid - @itrvd 41
  33. Safe state machines Now we need to carry even more

    connections around: val api: Authentication import api._ val done: IO[Connection[Done]] = for { connHandshake <- connect() connCreds <- handshake(connHandshake) connAuth <- sendCreds(connCreds) connDone <- authenticate(connAuth) } yield connDone Safe, but ugly. Itamar Ravid - @itrvd 42
  34. Safe state machines FP saves the day! This is IndexedStateT:

    case class IndexedStateT[F, Start, End, Result]( run: Start => F[(End, Result)] ) Itamar Ravid - @itrvd 44
  35. Safe state machines FP saves the day! This is IndexedStateT:

    case class IndexedStateT[F, Start, End, Result]( run: Start => F[(End, Result)] ) For example: Start = Connection[Handshake], End = Connection[Credentials] F = IO, Result = Unit Itamar Ravid - @itrvd 45
  36. Safe state machines val handshake: Connection[Handshake] => IO[(Connection[Credentials], Unit)] type

    Protocol[A, B] = IndexedStateT[ IO, Connection[A], Connection[B], Unit ] val nicerHandshake: Protocol[Handshake, Credentials] Itamar Ravid - @itrvd 47
  37. Safe state machines We can modify our API to use

    it: trait Authentication { def connect: Protocol[Unit, Handshake] def handshake: Protocol[Handshake, Credentials] def sendCreds: Protocol[Credentials, Auth] = IndexedStateT.modifyF { conn => IO(println("Sending credentials!")) *> IO(conn.copy()) } def authenticate: Protocol[Auth, Done] } Itamar Ravid - @itrvd 48
  38. Safe state machines And this is how the usage looks

    like: val api: Authentication import api._ val authenticate: Protocol[Unit, Done] = for { _ <- connect _ <- handshake _ <- sendCreds _ <- authenticate } yield () Itamar Ravid - @itrvd 49
  39. Safe state machines The original example, by the way, errors

    out: val api: Authentication import api._ val authenticate: Protocol[Unit, Done] = for { _ <- connect _ <- handshake _ <- authenticate _ <- sendCreds } yield () Itamar Ravid - @itrvd 50
  40. Safe state machines cmd6.sc:6: polymorphic expression cannot be instantiated to

    expected type; found : [B, SC]IndexedStateT[IO,Connection[Auth],SC,B] required: IndexedStateT[IO,Connection[Credentials],?,?] _ <- authenticate ^ Itamar Ravid - @itrvd 51
  41. Summary • IndexedStateT is very cool for type-safe transitions •

    It is a Monad and an Applicative when SA = SB • For non-linear transitions: shapeless.Coproduct and shapeless.HList • IxStateT in Haskell is in the indexed-extras package Itamar Ravid - @itrvd 52
  42. Resource allocation and release Problem: I have many resources allocated

    in my app. I need to manage their acquisition and release. def createKafkaConsumer: IO[KafkaConsumer] trait KafkaConsumer { def close: IO[Unit] } def createDBConnection: IO[DBConnection] trait DBConnection { def close: IO[Unit] } Itamar Ravid - @itrvd 53
  43. Resource allocation and release How should my initialization look like?

    def runApp(): IO[Unit] = for { consumer <- createKafkaConsumer conn <- createDBConnection _ <- doSomething(consumer, conn) _ <- conn.close _ <- consumer.close } yield () Itamar Ravid - @itrvd 55
  44. Resource allocation and release How should my initialization look like?

    def runApp(): IO[Unit] = for { consumer <- createKafkaConsumer conn <- createDBConnection _ <- doSomething(consumer, conn) _ <- ignoreFailures(conn.close) _ <- ignoreFailures(consumer.close) } yield () Itamar Ravid - @itrvd 56
  45. Resource allocation and release If you know Java, you're probably

    aware of try-with- resources: try (BufferedReader br = new BufferedReader(new FileReader(path))) { return br.readLine(); } This works for any T <: AutoCloseable. Itamar Ravid - @itrvd 58
  46. Resource allocation and release Our example would look like this:

    try (Consumer consumer = new Consumer()) { try (DBConnection conn = new DBConnection()) { doSomething(consumer, conn); } } Itamar Ravid - @itrvd 59
  47. Resource allocation and release IO monads offer us the bracket

    operation: object IO { def bracket[R, A](acquire: IO[R]) (release: R => IO[Unit]) (use: R => IO[A]): IO[A] } Itamar Ravid - @itrvd 61
  48. Resource allocation and release But this gets us to the

    same place: def createConsumer: IO[Consumer] def createDBConnection: IO[DBConnection] bracket(createConsumer)(_.close) { consumer => // use consumer } Itamar Ravid - @itrvd 62
  49. Resource allocation and release But this gets us to the

    same place: def createConsumer: IO[Consumer] def createDBConnection: IO[DBConnection] bracket(createConsumer)(_.close) { consumer => bracket(createDBConnection)(_.close) { conn => // use both } } Itamar Ravid - @itrvd 63
  50. Resource allocation and release There's an interesting data type called

    Codensity in Scalaz: abstract class Codensity[F[_], A] { def apply[B](f: A => F[B]): F[B] } Itamar Ravid - @itrvd 64
  51. Resource allocation and release There's an interesting data type called

    Codensity in Scalaz: abstract class Codensity[F[_], A] { def apply[B](f: A => F[B]): F[B] } When we set F = IO, A = R, B = A, apply looks like this: def apply(f: R => IO[A]): IO[A] Itamar Ravid - @itrvd 65
  52. Resource allocation and release This is remarkably similar to part

    of bracket! def bracket[R, A](acquire: IO[R]) (release: R => IO[Unit]) (use: R => IO[A]): IO[A] def apply (f: R => IO[A]): IO[A] Remember: type signatures are never a coincidence ;-) Itamar Ravid - @itrvd 66
  53. Resource allocation and release Let's write a combinator using Codensity:

    def resource[R](acquire: IO[R])(release: R => IO[Unit]) = new Codensity[IO, R] { def apply[A](use: R => IO[A]): IO[A] = IO.bracket(acquire)(release)(use) } Itamar Ravid - @itrvd 67
  54. Resource allocation and release So now, when we write a

    resource, we get back a value: val consumer: Codensity[IO, Consumer] = resource(createKafkaConsumer)(_.close) Itamar Ravid - @itrvd 68
  55. Resource allocation and release So now, when we write a

    resource, we get back a value: val consumer: Codensity[IO, Consumer] = resource(createKafkaConsumer)(_.close) And we can use it: val things: IO[List[Thing]] = consumer { c => c.consumeSomeThings } Itamar Ravid - @itrvd 69
  56. Resource allocation and release It just so happens that Codensity

    is a Monad. So I can do this: val resources: Codensity[IO, (Consumer, DBConnection)] = for { consumer <- resource(createKafkaConsumer)(_.close) dbConn <- resource(createDBConnection)(_.close) } yield (consumer, dbConn) What's happening here though? Itamar Ravid - @itrvd 71
  57. Resource allocation and release Codensity's flatMap: class Codensity[F[_], A] {

    self => def flatMap[B](f: A => Codensity[F, B]): Codensity[F, B] = new Codensity[F, B] { def apply[R](use: B => F[R]): F[R] = self.apply { a => f(a).apply { b => use(b) } } } } Itamar Ravid - @itrvd 72
  58. Resource allocation and release If we substitue the bracketing in:

    self.apply { a => f(a).apply { b => use(b) } } Itamar Ravid - @itrvd 73
  59. Resource allocation and release If we substitue the bracketing in:

    IO.bracket(acquireA)(releaseA) { a => IO.bracket(acquireB)(releaseB) { b => use(b) } } Itamar Ravid - @itrvd 74
  60. Resource allocation and release Finally, I can do something with

    these! val something: IO[Something] = resources { case (consumer, conn) => doSomething(consumer, dbConn) } Finalizers will run in reverse order of acquisition. Itamar Ravid - @itrvd 75
  61. Resource allocation and release • Codensity is 18 lines of

    code in total • cats-effect includes Resource[F, R] • scalaz-zio includes Managed[E, R] • In Haskell, you can use ResourceT or Managed Itamar Ravid - @itrvd 82
  62. Summary • Don't be afraid of the exotic data types!

    • Be on the lookout for examples Itamar Ravid - @itrvd 83