Slide 1

Slide 1 text

Boring Usecases for Exciting Types Itamar Ravid - Lambda World Cádiz, 2018 Itamar Ravid - @itrvd 1

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

Agenda • Why I wrote this talk • Three "boring" use-cases and types that can help Itamar Ravid - @itrvd 3

Slide 4

Slide 4 text

Itamar Ravid - @itrvd 4

Slide 5

Slide 5 text

Itamar Ravid - @itrvd 5

Slide 6

Slide 6 text

Itamar Ravid - @itrvd 6

Slide 7

Slide 7 text

Itamar Ravid - @itrvd 7

Slide 8

Slide 8 text

How do we grow as functional programmers Itamar Ravid - @itrvd 8

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

Examples Itamar Ravid - @itrvd 11

Slide 12

Slide 12 text

Examples We are sorely lacking! Itamar Ravid - @itrvd 12

Slide 13

Slide 13 text

Actual agenda Purely functional and type-safe: • Time measurement and instrumentation • State machines • Resource allocation and release • Let's go! Itamar Ravid - @itrvd 13

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

Itamar Ravid - @itrvd 17

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

Itamar Ravid - @itrvd 43

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

Safe state machines val handshake: Connection[Handshake] => IO[(Connection[Credentials], Unit)] Itamar Ravid - @itrvd 46

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

Resource allocation and release How should my initialization look like? Itamar Ravid - @itrvd 54

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

The smell is strong with you! Itamar Ravid - @itrvd 57

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

Resource allocation and release These are not the abstractions you're looking for! Itamar Ravid - @itrvd 60

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

Composition of resources, using Codensity Itamar Ravid - @itrvd 70

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

Itamar Ravid - @itrvd 76

Slide 77

Slide 77 text

Itamar Ravid - @itrvd 77

Slide 78

Slide 78 text

Itamar Ravid - @itrvd 78

Slide 79

Slide 79 text

Itamar Ravid - @itrvd 79

Slide 80

Slide 80 text

Itamar Ravid - @itrvd 80

Slide 81

Slide 81 text

Itamar Ravid - @itrvd 81

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

Summary • Don't be afraid of the exotic data types! • Be on the lookout for examples Itamar Ravid - @itrvd 83