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

Typeclasses in FP Architecture

Typeclasses in FP Architecture

A brief introduction to Functional Programming and the power of coding to abstractions and architecting using Type classes.

http://github.com/47deg/typeclasses-in-fp-architecture

Raúl Raja Martínez

March 02, 2018
Tweet

More Decks by Raúl Raja Martínez

Other Decks in Technology

Transcript

  1. Type classes & FP Λrchitecture
    @raulraja @47deg 1

    View full-size slide

  2. Typeclasses in FP Architecture
    An introduc+on to FP & type classes illustra+ng
    the power of coding to abstrac+ons and Tagless Final Architectures
    @raulraja
    @47deg
    Interac0ve
    Presenta0on
    @raulraja @47deg 2

    View full-size slide

  3. What is Func,onal Programming
    In computer science, func0onal programming
    is a programming paradigm.
    A style of building the structure and elements
    of computer programs that treats computa0on
    as the evalua0on of mathema0cal func0ons
    and avoids changing-state and mutable data.
    -- Wikipedia
    @raulraja @47deg 3

    View full-size slide

  4. Common traits of Func/onal Programming
    • Higher-order func0ons
    • Immutable data
    • Referen0al transparency
    • Lazy evalua0on
    • Recursion
    • Abstrac0ons
    @raulraja @47deg 4

    View full-size slide

  5. Higher Order Func.ons
    When a func*ons takes another func*on as argument
    or returns a func*on as return type:
    def transform[B](list : List[Int])(transformation : Int => B) =
    list map transformation
    transform(List(1, 2, 4))(x => x * 10)
    @raulraja @47deg 5

    View full-size slide

  6. Inmutable data
    Once a value is instan-ated it can't be mutated in place.
    How can we change it's content then?
    case class Conference(name : String)
    @raulraja @47deg 6

    View full-size slide

  7. Referen&al Transparency
    When a computa-on returns the same value
    each -me is invoked
    Transparent :
    def pureAdd(x : Int, y : Int) = x + y
    Opaque :
    var x = 0
    def impureAdd(y : Int) = x += y; x
    @raulraja @47deg 7

    View full-size slide

  8. Lazy Evalua*on
    When a computa-on is evaluated
    only if needed
    import scala.util.Try
    def boom = throw new RuntimeException
    def strictEval(f : Any) = Try(f)
    def lazyEval(f : => Any) = Try(f)
    @raulraja @47deg 8

    View full-size slide

  9. Recursion
    Recursion is favored over itera0on
    def reduceIterative(list : List[Int]) : Int = {
    var acc = 0
    for (i <- list) acc = acc + i
    acc
    }
    @raulraja @47deg 9

    View full-size slide

  10. Recursion
    Recursion is favored over itera0on
    def reduceRecursive(list : List[Int], acc : Int = 0) : Int =
    list match {
    case Nil => acc
    case head :: tail => reduceRecursive(tail, head + acc)
    }
    @raulraja @47deg 10

    View full-size slide

  11. Abstrac(ons
    Each significant piece of func1onality in a program
    should be implemented in just one place in the source code.
    -- Benjamin C. Pierce in Types and Programming Languages (2002)
    @raulraja @47deg 11

    View full-size slide

  12. What is a Typeclass
    A typeclass is an interface/protocol that provides
    a behavior for a given data type.
    This is also known as Ad-hoc Polymorphism
    We will learn typeclasses by example...
    @raulraja @47deg 12

    View full-size slide

  13. Typeclasses
    [ ] Monoid : Combine values of the same type
    [ ] Functor : Transform values inside contexts
    @raulraja @47deg 13

    View full-size slide

  14. Monoid
    A Monoid expresses the ability of a value of a
    type to combine itself with other values of the same type
    in addi7on it provides an empty value.
    import simulacrum._
    @typeclass trait Monoid[A] {
    def combine(x : A, y : A) : A
    def empty : A
    }
    @raulraja @47deg 14

    View full-size slide

  15. Monoid
    implicit val IntAddMonoid = new Monoid[Int] {
    def combine(x : Int, y : Int) : Int = ???
    def empty = ???
    }
    @raulraja @47deg 15

    View full-size slide

  16. Monoid
    implicit val IntAddMonoid = new Monoid[Int] {
    def combine(x : Int, y : Int) : Int = x + y
    def empty = 0
    }
    @raulraja @47deg 16

    View full-size slide

  17. Monoid
    implicit val StringConcatMonoid = new Monoid[String] {
    def combine(x : String, y : String) : String = x + y
    def empty = ""
    }
    @raulraja @47deg 17

    View full-size slide

  18. Monoid
    implicit def ListConcatMonoid[A] = new Monoid[List[A]] {
    def combine(x : List[A], y : List[A]) : List[A] = x ++ y
    def empty = Nil
    }
    @raulraja @47deg 18

    View full-size slide

  19. Monoid
    We can code to abstrac-ons instead of coding to concrete
    types.
    def uberCombine[A : Monoid](x : A, y : A) : A =
    Monoid[A].combine(x, y)
    uberCombine(10, 10)
    @raulraja @47deg 19

    View full-size slide

  20. Typeclasses
    [x] Monoid : Combine values of the same type
    [ ] Functor : Transform values inside contexts
    @raulraja @47deg 20

    View full-size slide

  21. Functor
    A Functor expresses the ability of a container
    to transform its content given a func7on
    @typeclass trait Functor[F[_]] {
    def map[A, B](fa : F[A])(f : A => B) : F[B]
    }
    @raulraja @47deg 21

    View full-size slide

  22. Functor
    Most containers transforma.ons can be expressed as
    Functors.
    implicit def ListFunctor = new Functor[List] {
    def map[A, B](fa : List[A])(f : A => B) = fa map f
    }
    @raulraja @47deg 22

    View full-size slide

  23. Functor
    Most containers transforma.ons can be expressed as
    Functors.
    implicit def OptionFunctor = new Functor[Option] {
    def map[A, B](fa : Option[A])(f : A => B) = fa map f
    }
    @raulraja @47deg 23

    View full-size slide

  24. Functor
    Most containers transforma.ons can be expressed as
    Functors.
    import scala.concurrent.{Future, Await}
    import scala.concurrent.duration._
    import scala.concurrent.ExecutionContext.Implicits.global
    implicit def FutureFunctor = new Functor[Future] {
    def map[A, B](fa : Future[A])(f : A => B) = fa map f
    }
    @raulraja @47deg 24

    View full-size slide

  25. Functor
    We can code to abstrac-ons instead of coding to concrete
    types.
    def uberMap[F[_] : Functor, A, B](fa : F[A])(f : A => B) : F[B] =
    Functor[F].map(fa)(f)
    uberMap(List(1, 2, 3))(x => x * 2)
    @raulraja @47deg 25

    View full-size slide

  26. Typeclasses
    [x] Monoid : Combine values of the same type
    [x] Functor : Transform values inside contexts
    @raulraja @47deg 26

    View full-size slide

  27. Typeclasses
    Can we combine mul.ple
    abstrac.ons & behaviors?
    @raulraja @47deg 27

    View full-size slide

  28. What are our applica.on layers?
    ├── algebras
    │ ├── datasource
    │ │ └── NlpDataSource.scala
    │ ├── services
    │ │ ├── Config.scala
    │ │ └── TagService.scala
    │ ├── ui
    │ │ └── Presentation.scala
    │ └── usecases
    │ └── FetchTagsUseCase.scala
    ├── app
    │ └── main.scala
    └── runtime
    ├── datasource
    │ └── TextRazorNlpDataSource.scala
    ├── runtime.scala
    ├── services
    │ └── SystemEnvConfig.scala
    └── ui
    └── ConsolePresentation.scala
    @raulraja @47deg 28

    View full-size slide

  29. What are our applica.on layers?
    Presenta(on
    import simulacrum._
    @typeclass trait Presentation[F[_]] {
    def onUserRequestedTags(text: String): F[Unit]
    }
    @raulraja @47deg 29

    View full-size slide

  30. What are our applica.on layers?
    Use Case
    case class Tag(value: String)
    case class TaggedParagraph(text: String, tags: List[Tag])
    @typeclass trait FetchTagsUseCase[F[_]] {
    def fetchTagsInText(text: String): F[TaggedParagraph]
    }
    @raulraja @47deg 30

    View full-size slide

  31. What are our applica.on layers?
    Services
    case class AnalysisRequest(text: String)
    @typeclass trait TagService[F[_]] {
    def tag(request: AnalysisRequest): F[List[Tag]]
    }
    @raulraja @47deg 31

    View full-size slide

  32. What are our applica.on layers?
    Data Source
    case class Category(value: String)
    case class Entity(value: String)
    case class Topic(value: String)
    case class AnalysisResponse(
    categories: List[Category],
    entities: List[Entity],
    topics: List[Topic]
    )
    @typeclass trait NlpDataSource[F[_]] {
    def analyze(text: String): F[AnalysisResponse]
    }
    @raulraja @47deg 32

    View full-size slide

  33. What are our applica.on layers?
    Configura)on
    case class NlpApiKey(value: String)
    @typeclass trait Config[F[_]] {
    def nlpApiKey: F[NlpApiKey]
    }
    @raulraja @47deg 33

    View full-size slide

  34. Implementa)on
    Presenta(on
    import cats._
    import cats.Functor
    import cats.implicits._
    class ConsolePresentation[F[_]: Functor: FetchTagsUseCase] extends Presentation[F] {
    def onUserRequestedTags(text: String): F[Unit] =
    FetchTagsUseCase[F].fetchTagsInText(text).map { paragraph =>
    println(paragraph.tags.mkString(", "))
    }
    }
    @raulraja @47deg 34

    View full-size slide

  35. Implementa)on?
    Use case
    class DefaultFetchTagsUseCase[F[_]: Functor: TagService] extends FetchTagsUseCase[F] {
    def fetchTagsInText(text: String): F[TaggedParagraph] =
    TagService[F].tag(AnalysisRequest(text)).map { tags =>
    TaggedParagraph(text, tags)
    }
    }
    @raulraja @47deg 35

    View full-size slide

  36. Implementa)on?
    Tag Service
    class DefaultTagService[F[_]: Functor: NlpDataSource] extends TagService[F] {
    def tag(request: AnalysisRequest): F[List[Tag]] =
    NlpDataSource[F].analyze(request.text).map { r =>
    (r.categories, r.entities, r.topics).mapN { case (category, entity, topic) =>
    List(Tag(category.value), Tag(entity.value), Tag(topic.value))
    }.flatten.distinct
    }
    }
    @raulraja @47deg 36

    View full-size slide

  37. Implementa)on?
    Config
    class SystemEnvConfig[F[_]](implicit AE: ApplicativeError[F, Throwable]) extends Config[F] {
    val key = System.getenv("NLP_API_KEY")
    def nlpApiKey: F[NlpApiKey] =
    if (key == null || key == "")
    AE.raiseError(new IllegalStateException("Missing nlp api key"))
    else
    AE.pure(NlpApiKey(key))
    }
    @raulraja @47deg 37

    View full-size slide

  38. Implementa)on?
    Data Source
    import collection.JavaConverters._
    import scala.concurrent._
    import java.util.Arrays
    import com.textrazor.TextRazor
    object TextRazorClient {
    def client(apiKey: NlpApiKey) : TextRazor = {
    val c = new TextRazor(apiKey.value)
    c.addExtractor("entities")
    c.addExtractor("topics")
    c.setClassifiers(Arrays.asList("textrazor_newscodes"))
    c
    }
    }
    @raulraja @47deg 38

    View full-size slide

  39. Implementa)on?
    Data Source
    class TextRazorNlpDataSource[F[_]: Config](implicit ME: MonadError[F, Throwable]) extends NlpDataSource[F] {
    def analyze(text: String): F[AnalysisResponse] =
    for {
    apiKey <- Config[F].nlpApiKey
    response <- ME.catchNonFatal {
    val r = blocking {
    TextRazorClient.client(apiKey).analyze(text).getResponse
    }
    AnalysisResponse(
    r.getCategories.asScala.toList.map { c => Category(c.getLabel) },
    r.getEntities.asScala.toList.map { e => Entity(e.getEntityId) },
    r.getTopics.asScala.toList.map { t => Topic(t.getLabel) }
    )
    }
    } yield response
    }
    @raulraja @47deg 39

    View full-size slide

  40. Implementa)on?
    Run$me Module
    object runtime {
    implicit def presentation[F[_]: Functor: FetchTagsUseCase]: Presentation[F] =
    new ConsolePresentation
    implicit def useCase[F[_]: Functor: TagService] : FetchTagsUseCase[F] =
    new DefaultFetchTagsUseCase
    implicit def tagService[F[_]: Functor: NlpDataSource] : TagService[F] =
    new DefaultTagService
    implicit def dataSource[F[_]: Config]
    (implicit ME: MonadError[F, Throwable]) : NlpDataSource[F] =
    new TextRazorNlpDataSource
    implicit def config[F[_]](implicit A: ApplicativeError[F, Throwable]): Config[F] =
    new SystemEnvConfig
    }
    @raulraja @47deg 40

    View full-size slide

  41. Implementa)on?
    Applica'on
    import runtime._
    import scala.util.Try
    val text =
    """|
    | And now here is my secret, a very simple secret:
    | It is only with the heart that one can see rightly;
    | what is essential is invisible to the eye.
    | ― Antoine de Saint-Exupéry, The Little Prince
    """.stripMargin
    Presentation[Try].onUserRequestedTags(text)
    @raulraja @47deg 41

    View full-size slide

  42. Pros
    • Pure Applica-ons and Libraries
    • Tes-ng flexibility
    • Controlled Effects at the edge
    • Separa-on of concerns on steroids
    • A unified model to create components and compose them
    • A unified API to rule all data types when using just type classes.
    • Restricted to thoroughly tested lawful declara-ons with a base on mathema-cs
    @raulraja @47deg 42

    View full-size slide

  43. Cons
    • Requires understanding type classes and higher kinds
    • Poten7al peer and status quo push-back
    @raulraja @47deg 43

    View full-size slide

  44. Ques%ons? & Thanks!
    @raulraja
    @47deg
    h-p:/
    /github.com/47deg/typeclasses-in-fp-architecture
    h-ps:/
    /speakerdeck.com/raulraja/typeclasses-in-fp-architecture
    @raulraja @47deg 44

    View full-size slide