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. 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