Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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