+ All Categories
Home > Technology > Writing DSL with Applicative Functors

Writing DSL with Applicative Functors

Date post: 26-May-2015
Category:
Upload: david-galichet
View: 1,087 times
Download: 0 times
Share this document with a friend
Description:
Présentation au Paris Scala User Group le 21/08/2014
Popular Tags:
43
Writing DSL with Applicative Functors David Galichet Freelance functional programmer twitter: @dgalichet
Transcript
Page 1: Writing DSL with Applicative Functors

Writing DSL with Applicative Functors

David Galichet Freelance functional programmer

!twitter: @dgalichet

Page 2: Writing DSL with Applicative Functors

Content normalization

• We want to parse heterogeneous data formats (CSV, XML …) and transform them to a pivot format (Scala object in our case)

• The transformation should be described as a DSL

• This DSL must be simple to use, and enable any kinds of data transformations or verifications

Page 3: Writing DSL with Applicative Functors

Expected DSL format

val reader = ( ! Pick(0).as[String].map(_.capitalize) and ! Pick(1).as[Date].check(_.after(now())) !).reduce(FutureEvent)!!reader("PSUG; 21/08/2014") // returns Success(FutureEvent("PSUG", date))

Inspired by Play2 Json API

Page 4: Writing DSL with Applicative Functors

Conventions & material

• Code is available on Github : https://github.com/dgalichet/PsugDSLWritingWithApplicative

• Code revision is define on the top of any slides including source code (just checkout the specified tag)

tag: step1

Page 5: Writing DSL with Applicative Functors

Reading single entry• We have a CSV line and we want to read one column

• We will introduce several abstractions:

• Picker: fetch a data from CSV or XML

• Result: either a Success or a Failure

• Converter: convert value from Picker to Reader

• Reader: container with methods to process its content

tag: step1

Page 6: Writing DSL with Applicative Functors

Introducing Pickertag: step1

case class Picker(p: String => Result[String]) {! def as[T](implicit c: Converter[T]): Reader[T] = c.convert(p)!}!!object CsvPicker {! def apply[T](i: Int)(implicit separator: Char): Picker = Picker { s: String =>! val elems = s.trim.split(separator)! if (i > 0 && elems.size > i) Success(elems(i).trim)! else Failure(s"No column ${i} for ${s}")! }!}

Page 7: Writing DSL with Applicative Functors

Introducing Pickertag: step1

case class Picker(p: String => Result[String]) {! def as[T](implicit c: Converter[T]): Reader[T] = c.convert(p)!}!!object CsvPicker {! def apply[T](i: Int)(implicit separator: Char): Picker = Picker { s: String =>! val elems = s.trim.split(separator)! if (i > 0 && elems.size > i) Success(elems(i).trim)! else Failure(s"No column ${i} for ${s}")! }!}

Picker wraps a function from String to Result

Page 8: Writing DSL with Applicative Functors

The Resulttag: step1

sealed trait Result[+T] case class Success[T](t: T) extends Result[T] case class Failure(error: String) extends Result[Nothing]!

Page 9: Writing DSL with Applicative Functors

The Convertertag: step1

trait Converter[T] { def convert(p: String => Result[String]): Reader[T] }!!object Converter {! implicit val string2StringConverter = new Converter[String] {! override def convert(p: String => Result[String]) = Reader[String](p)! // See code on Github for more converters!}

Page 10: Writing DSL with Applicative Functors

The Convertertag: step1

trait Converter[T] { def convert(p: String => Result[String]): Reader[T] }!!!object Converter {! implicit val string2StringConverter = new Converter[String] {! override def convert(p: String => Result[String]) = Reader[String](p)! // See code on Github for more converters!}

Convert the content of the Picker to a Reader

Page 11: Writing DSL with Applicative Functors

The Readertag: step1

case class Reader[O](p: String => Result[O]) { def apply(s: String): Result[O] = p(s) }

A Reader doesn’t contain a value but a process to transform original data (CSV line or XML) to a Result

Page 12: Writing DSL with Applicative Functors

Usage sampletag: step1

import Converter._ // import implicit converters!implicit val separator = ‘;’!!CsvPicker(1).as[String].apply("foo;bar") === "bar"

Page 13: Writing DSL with Applicative Functors

Enhancing the Reader

• The first defines a very simple Reader. We must add a method to combine two instances of Reader

• We will also enhance Failure to store multiple error messages

tag: step2

Page 14: Writing DSL with Applicative Functors

Enhancing the Readertag: step2

case class Reader[O](p: String => Result[O]) {! def apply(s: String): Result[O] = p(s)!! def and[O2](r2: Reader[O2]): Reader[(O, O2)] = Reader { s: String =>! (p(s), r2.p(s)) match {! case (Success(s1), Success(s2)) => Success((s1, s2))! case (Success(_), Failure(f)) => Failure(f)! case (Failure(f), Success(_)) => Failure(f)! case (Failure(f1), Failure(f2)) => Failure(f1 ++ f2)! }! }! def map[T](f: O => T): Reader[T] = Reader { s: String =>! p(s) match {! case Success(o) => Success(f(o))! case f: Failure => f! }! }! def reduce[T] = map[T] _ // alias for map!}

Page 15: Writing DSL with Applicative Functors

Enhancing Result typetag: step2

sealed trait Result[+T]!case class Success[T](t: T) extends Result[T]!case class Failure(error: NonEmptyList[String]) extends Result[Nothing]!!object Failure {! def apply(s: String): Failure = Failure(NEL(s))!}!!case class NonEmptyList[T](head: T, tail: List[T]) { def toList = head::tail def ++(l2: NonEmptyList[T]): NonEmptyList[T] = NonEmptyList(head, tail ++ l2.toList) } object NEL { def apply[T](h: T, t: T*) = NonEmptyList(h, t.toList) }

Page 16: Writing DSL with Applicative Functors

Usage sampletag: step2

implicit val separator = ';' implicit val dtFormatter = new SimpleDateFormat("dd/MM/yyyy") import Converter.string2StringConverter import Converter.string2DateConverter!!val reader = ( CsvPicker(1).as[String] and CsvPicker(2).as[Date] ).reduce { case (n, d) => FutureEvent(n, d) }!reader("foo;bar;12/10/2014") === Success(FutureEvent("bar", dtFormatter.parse("12/10/2014")))!!case class FutureEvent(name: String, dt: Date)

Page 17: Writing DSL with Applicative Functors

Usability problemtag: step2

• The use of reduce (or map) method to transform a Reader[(0, 02)] into an instance of Reader[FutureEvent] for example is quite verbose

• This will be even more verbose for instances of Reader[(0, (02, 03))]

• We want the API to automatically bind tuple elements to a constructor as we can encounter in Play2 Json API

Page 18: Writing DSL with Applicative Functors

Applicative functorstag: step3

• To tackle our problem, we will use Applicative Functors and play2 functional library (and especially FunctionalBuilder)

• This approach is inspired by @sadache (Sadek Drobi) article https://gist.github.com/sadache/3646092

• An Applicative Functor is a Type Class relying on ad-hoc polymorphism to extends a Class with some properties

• Play2 functional library (or Scalaz) provides mechanism to compose Applicatives in a smart way

Page 19: Writing DSL with Applicative Functors

Applicative functorstag: step3

M is an Applicative Functor if there exists the following methods :

def pure[A](a: A): M[A] def map[A, B](m: M[A], f: A => B): M[B] def apply[A, B](mf: M[A => B], ma: M[A]): M[B]!

with the following Laws : • Identity: apply(pure(identity), ma) === ma where ma is an Applicative M[A] • Homomorphism: apply(pure(f), pure(a)) === pure(f(a)) where f: A =>

B and a an instance of A • Interchange: mf if an instance of M[A => B]

apply(mf, pure(a)) === apply(pure {(g: A => B) => g(a)}, mf)!• Composition: map(ma, f) === apply(pure(f), ma)

Page 20: Writing DSL with Applicative Functors

Applicative functorstag: step3

trait Applicative[M[_]] { def pure[A](a: A): M[A] def map[A, B](m: M[A], f: A => B): M[B] def apply[A, B](mf: M[A => B], ma: M[A]): M[B] } Applicative is an Higher Kinded type

(parameterized with M that take a single type parameter)

Page 21: Writing DSL with Applicative Functors

Reader is an Applicativetag: step3

case class Reader[O](p: String => Result[O]) { def apply(s: String): Result[O] = p(s)! def map[T](f: O => T): Reader[T] = Reader { s: String => p(s) match { case Success(o) => Success(f(o)) case f: Failure => f } }!}!object Reader { def map2[O, O1, O2](r1: Reader[O1], r2: Reader[O2])(f: (O1, O2) => O): Reader[O] = Reader { s: String => (r1.p(s), r2.p(s)) match { case (Success(s1), Success(s2)) => Success(f(s1, s2)) case (Success(_), Failure(e)) => Failure(e) case (Failure(e), Success(_)) => Failure(e) case (Failure(e1), Failure(e2)) => Failure(e1 ++ e2) } } …!}

Page 22: Writing DSL with Applicative Functors

Reader is an Applicativetag: step3

object Reader { … // map2 implicit val readerIsAFunctor: Functor[Reader] = new Functor[Reader] { override def fmap[A, B](m: Reader[A], f: (A) => B) = m.map(f) } implicit val readerIsAnApplicative: Applicative[Reader] = new Applicative[Reader] { override def pure[A](a: A) = Reader { _ => Success(a) } override def apply[A, B](mf: Reader[A => B], ma: Reader[A]) = map2(mf, ma)((f, a) => f(a)) override def map[A, B](m: Reader[A], f: A => B) = m.map(f) } }

Page 23: Writing DSL with Applicative Functors

Usage sampletag: step3

import Converter.string2StringConverter import Converter.string2DateConverter!import play.api.libs.functional.syntax._ import Reader.readerIsAnApplicative!!implicit val separator = ';' implicit val dtFormatter = new SimpleDateFormat("dd/MM/yyyy")!!val reader = ( CsvPicker(1).as[String] and CsvPicker(2).as[Date] )(FutureEvent) // here we use CanBuild2.apply reader("foo;bar;12/10/2014") === Success(FutureEvent("bar", dtFormatter.parse("12/10/2014")))!

Page 24: Writing DSL with Applicative Functors

Usage sample (errors accumulation)

tag: step3

import Converter.string2StringConverter import Converter.string2DateConverter!import play.api.libs.functional.syntax._ import Reader.readerIsAnApplicative!!implicit val separator = ';' implicit val dtFormatter = new SimpleDateFormat("dd/MM/yyyy")!!val reader = ( CsvPicker(1).as[Int] and CsvPicker(2).as[Date] )((_, _)) reader(List("foo", "not a number", "not a date")) === Failure(NEL(!"Unable to format 'not a number' as Int", !"Unable to format 'not a date' as Date"))!

Page 25: Writing DSL with Applicative Functors

Benefitstag: step3

• Making Reader an Applicative Functor give ability to combine efficiently instances of Reader

• Due to Applicative properties, we still accumulate errors

• Play2 functional builder give us a clean syntax to define our DSL

Page 26: Writing DSL with Applicative Functors

Introducing XML Pickertag: step4

case class Picker(p: String => Result[String]) { def as[T](implicit c: Converter[T]): Reader[T] = c.convert(p) }!!object XmlPicker { def apply[T](query: Elem => NodeSeq): Picker = Picker { s: String => try { val xml = XML.loadString(s) Success(query(xml).text) } catch { case e: Exception => Failure(e.getMessage) } } }!

Page 27: Writing DSL with Applicative Functors

Usage sampletag: step4

import play.api.libs.functional.syntax._ import Reader.readerIsAnApplicative!import Converter._ implicit val dF = new SimpleDateFormat("dd/MM/yyyy")!val xml = """ <company name="Dupont and Co"> <owner> <person firstname="jean" lastname="dupont" birthdate="11/03/1987"/> </owner> </company>""" val r = ( XmlPicker(_ \\ "person" \ "@firstname").as[String] and XmlPicker(_ \\ "person" \ "@lastname").as[String] and XmlPicker(_ \\ "person" \ "@birthdate").as[Date] )(Person)!r(xml) === Success(Person("jean","dupont",dF.parse("11/03/1987"))) case class Person(firstname: String, lastname: String, birthDt: Date)

Page 28: Writing DSL with Applicative Functors

Implementation problemtag: step4

• The Reader[O] takes a type argument for the output. The input is always a String

• With this implementation, an XML content will be parsed (with XML.load) as many times as we use XmlPicker. This will cause unnecessary overhead

• We will have the same issue (with lower overhead) with our CsvPicker

Page 29: Writing DSL with Applicative Functors

Introducing Reader[I, 0]tag: step5

To resolve this problem, we will modify Reader to take a type parameter for the input

Page 30: Writing DSL with Applicative Functors

Introducing Reader[I, 0]tag: step5

case class Reader[I, O](p: I => Result[O]) { def apply(s: I): Result[O] = p(s) def map[T](f: O => T): Reader[I, T] = Reader { s: I => p(s) match { case Success(o) => Success(f(o)) case f: Failure => f } }!}!object Reader { def map2[I, O, O1, O2](r1: Reader[I, O1], r2: Reader[I, O2])(f: (O1, O2) => O): Reader[I, O] = Reader { s: I => (r1.p(s), r2.p(s)) match { case (Success(s1), Success(s2)) => Success(f(s1, s2)) case (Success(_), Failure(e)) => Failure(e) case (Failure(e), Success(_)) => Failure(e) case (Failure(e1), Failure(e2)) => Failure(e1 ++ e2) } }

Page 31: Writing DSL with Applicative Functors

Introducing Reader[I, 0]tag: step5

object Reader {!implicit def readerIsAFunctor[I] = new Functor[({type λ[A] = Reader[I, A]})#λ] { override def fmap[A, B](m: Reader[I, A], f: (A) => B) = m.map(f) } implicit def readerIsAnApplicative[I] = new Applicative[({type λ[A] = Reader[I, A]})#λ] { override def pure[A](a: A) = Reader { _ => Success(a) } override def apply[A, B](mf: Reader[I, A => B], ma: Reader[I, A]) = map2(mf, ma)((f, a) => f(a)) override def map[A, B](m: Reader[I, A], f: (A) => B) = m.map(f) }

Page 32: Writing DSL with Applicative Functors

What are Type lambdas ?tag: step5

If we go back to Applicative definition, we can see that it’s an Higher Kinded type (same with Functor) : !trait Applicative[M[_]] { … } // Applicative accept parameter M that take itself any type as parameter!!Our problem is that Reader[I, 0] takes two parameters but Applicative[M[_]] accept types M with only one parameter. We use Type Lambdas to resolve this issue: !new Applicative[({type λ[A] = Reader[I, A]})#λ]!

Page 33: Writing DSL with Applicative Functors

Go back to Reader[I, 0]tag: step5

object Reader {!implicit def readerIsAFunctor[I] = new Functor[({type λ[A] = Reader[I, A]})#λ] { override def fmap[A, B](m: Reader[I, A], f: A => B) = m.map(f) } implicit def readerIsAnApplicative[I] = new Applicative[({type λ[A] = Reader[I, A]})#λ] { override def pure[A](a: A) = Reader { _ => Success(a) } override def apply[A, B](mf: Reader[I, A => B], ma: Reader[I, A]) = map2(mf, ma)((f, a) => f(a)) override def map[A, B](m: Reader[I, A], f: A => B) = m.map(f) }

Page 34: Writing DSL with Applicative Functors

Go back to Reader[I, 0]tag: step5

object Reader {!import scala.language.implicitConversions // Here we help the compiler a bit. Thanks @skaalf (Julien Tournay) !// and https://github.com/jto/validation implicit def fcbReads[I] = functionalCanBuildApplicative[({type λ[A] = Reader[I, A]})#λ] implicit def fboReads[I, A](a: Reader[I, A])(implicit fcb: FunctionalCanBuild[({type λ[x] = Reader[I, x]})#λ]) = new FunctionalBuilderOps[({type λ[x] = Reader[I, x]})#λ, A](a)(fcb)

Page 35: Writing DSL with Applicative Functors

Converter[I, T]tag: step5

trait Converter[I, T] { def convert(p: I => Result[String]): Reader[I, T] }!object Converter { implicit def stringConverter[I] = new Converter[I, String] { override def convert(p: I => Result[String]) = Reader[I, String](p) }!! implicit def dateConverter[I](implicit dtFormat: DateFormat) = new Converter[I, Date] { override def convert(p: I => Result[String]) = Reader[I, Date] { s: I => p(s) match { case Success(dt) => try { ! Success(dtFormat.parse(dt)) } catch { case e: ParseException => Failure(s"...") } case f: Failure => f }}}

Page 36: Writing DSL with Applicative Functors

Picker[I]tag: step5

case class Picker[I](p: I => Result[String]) { def as[T](implicit c: Converter[I, T]): Reader[I, T] = c.convert(p) } object CsvPicker { def apply[T](i: Int): Picker[List[String]] = Picker { elems: List[String] => if (i > 0 && elems.size > i) Success(elems(i).trim) else Failure(s"No column ${i} found in ${elems.mkString(";")}") }} object XmlPicker { def apply[T](query: Elem => NodeSeq): Picker[Elem] = Picker { elem: Elem => try { Success(query(elem).text) } catch { case e: Exception => Failure(e.getMessage) }}}!

Page 37: Writing DSL with Applicative Functors

Usage sampletag: step5

import play.api.libs.functional.syntax._ import Reader._!import Converter._!implicit val dF = new SimpleDateFormat("dd/MM/yyyy")!val xml = XML.loadString(""" <company name="Dupont and Co"> <owner> <person firstname="jean" lastname="dupont" birthdate="11/03/1987"/> </owner> </company>""")!!val r = ( XmlPicker(_ \\ "person" \ "@firstname").as[String] and XmlPicker(_ \\ "person" \ "@lastname").as[String] and XmlPicker(_ \\ "person" \ "@birthdate").as[Date] )(Person)!r(xml) === Success(Person("jean", "dupont", dF.parse("11/03/1987")))

Page 38: Writing DSL with Applicative Functors

Adding combinatorstag: step6

• We now add new abilities to Reader

• We especially want a method to validate content

Page 39: Writing DSL with Applicative Functors

Adding combinatorstag: step6

case class Reader[I, O](p: I => Result[O]) {!! def flatMap[T](f: O => Reader[I, T]): Reader[I, T] = Reader { s: I => p(s) match { case Success(o) => f(o)(s) case f: Failure => f } } def verify(f: O => Result[O]): Reader[I, O] = flatMap { o: O => Reader( _ => f(o)) }!}

Page 40: Writing DSL with Applicative Functors

Usage sampletag: step6

val r: Reader[String, String] = Reader { Success(_) } r.verify { x => if (x == "OK") Success(x) else Failure("KO") }("OK") === Success("OK")

Page 41: Writing DSL with Applicative Functors

Conclusion• We have created a simple and powerful DSL for

processing CSV and XML content

• This DSL give us ability to Pick data, transform and verify it and also accumulate encountered errors

• We have seen that making Reader an instance of the Applicative Functor Type Class add it new capabilities

• Using ad-hoc polymorphism using Type Classes gives us ability to extends Reader without altering it

Page 42: Writing DSL with Applicative Functors

Follow-up• Type Classes are defined by functions that must be

implemented with regards to their laws (left/right identity …)

• Proving the correctness of Type Class Laws can be a bit tricky we usual approach

• I will introduce the framework ScalaCheck at scala.io 2014, and show how to test them

Page 43: Writing DSL with Applicative Functors

Follow-up

• In the roadmap that has been announced by @typesafe (http://scala-lang.org/news/roadmap-next), it seems that Scala 2.14 (aka « Don Giovanni ») will clean up lambda types syntax


Recommended