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

Avatar for Raúl Raja Martínez

Raúl Raja Martínez

March 02, 2018
Tweet

More Decks by Raúl Raja Martínez

Other Decks in Technology

Transcript

  1. 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
  2. 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
  3. Common traits of Func/onal Programming • Higher-order func0ons • Immutable

    data • Referen0al transparency • Lazy evalua0on • Recursion • Abstrac0ons @raulraja @47deg 4
  4. 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
  5. 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
  6. 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
  7. 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
  8. 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
  9. 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
  10. 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
  11. 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
  12. Typeclasses [ ] Monoid : Combine values of the same

    type [ ] Functor : Transform values inside contexts @raulraja @47deg 13
  13. 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
  14. Monoid implicit val IntAddMonoid = new Monoid[Int] { def combine(x

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

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

    : String, y : String) : String = x + y def empty = "" } @raulraja @47deg 17
  17. 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
  18. 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
  19. Typeclasses [x] Monoid : Combine values of the same type

    [ ] Functor : Transform values inside contexts @raulraja @47deg 20
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. Typeclasses [x] Monoid : Combine values of the same type

    [x] Functor : Transform values inside contexts @raulraja @47deg 26
  26. 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
  27. What are our applica.on layers? Presenta(on import simulacrum._ @typeclass trait

    Presentation[F[_]] { def onUserRequestedTags(text: String): F[Unit] } @raulraja @47deg 29
  28. 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
  29. 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
  30. 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
  31. What are our applica.on layers? Configura)on case class NlpApiKey(value: String)

    @typeclass trait Config[F[_]] { def nlpApiKey: F[NlpApiKey] } @raulraja @47deg 33
  32. 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
  33. 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
  34. 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
  35. 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
  36. 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
  37. 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
  38. 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
  39. 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
  40. 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
  41. Cons • Requires understanding type classes and higher kinds •

    Poten7al peer and status quo push-back @raulraja @47deg 43