Upload
sukant-hajra
View
377
Download
3
Embed Size (px)
Citation preview
Validation with Functional Programming
Sukant Hajra / @shajra
April 14, 2016
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 1 / 61
Some Goals
explore data structures for managing errorsuse type classes to get nice APIs (DSLs)see some examples with parsing
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 2 / 61
scalaz.\/
Constructionscala> import scalaz.syntax.either._
import scalaz.syntax.either._
scala> 1.right[String]res0: scalaz.\/[String,Int] = \/-(1)
scala> "fail".left[Int]res1: scalaz.\/[String,Int] = -\/(fail)
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 3 / 61
scalaz.\/
Monadic Syntaxscala> 1.right[String].flatMap { a =>
| 2.right[String].map { b =>| a + b| }| }
res2: scalaz.\/[String,Int] = \/-(3)
scala> for {| a <- 1.right[String]| b <- 2.right[String]| } yield a + b
res3: scalaz.\/[String,Int] = \/-(3)
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 4 / 61
scalaz.\/
Applicative Syntaxscala> import scalaz.syntax.apply._
import scalaz.syntax.apply._
scala> 1.right[String] |@| 2.right[String] apply { _ + _ }res4: scalaz.\/[String,Int] = \/-(3)
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 5 / 61
scalaz.\/
Semanticsscala> (1.right[String] |@| 2.right[String]).
| apply { _ + _ }res5: scalaz.\/[String,Int] = \/-(3)
scala> (1.right[String] |@| "fail 2".left[Int]).| apply { _ + _ }
res6: scalaz.\/[String,Int] = -\/(fail 2)
scala> ("fail 1".left[Int] |@| 2.right[String]).| apply { _ + _ }
res7: scalaz.\/[String,Int] = -\/(fail 1)
scala> ("fail 1".left[Int] |@| "fail 2".left[Int]).| apply { _ + _ }
res8: scalaz.\/[String,Int] = -\/(fail 1)
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 6 / 61
scalaz.ValidationNel
Constructionscala> import scalaz.syntax.validation._
import scalaz.syntax.validation._
scala> 1.success[String]res9: scalaz.Validation[String,Int] = Success(1)
scala> "fail".failure[Int]res10: scalaz.Validation[String,Int] = Failure(fail)
scala> 1.successNel[String]res11: scalaz.ValidationNel[String,Int] = Success(1)
scala> "fail".failureNel[Int]res12: scalaz.ValidationNel[String,Int] =
Failure(NonEmptyList(fail))
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 7 / 61
scalaz.ValidationNel
Semanticsscala> (1.successNel[String] |@| 2.successNel[String]).
| apply { _ + _ }res13: scalaz.Validation[scalaz.NonEmptyList[String],Int] =
Success(3)
scala> (1.successNel[String] |@| "fail 2".failureNel[Int]).| apply { _ + _ }
res14: scalaz.Validation[scalaz.NonEmptyList[String],Int] =Failure(NonEmptyList(fail 2))
scala> ("fail 1".failureNel[Int] |@| "fail 2".failureNel[Int]).| apply { _ + _ }
res15: scalaz.Validation[scalaz.NonEmptyList[String],Int] =Failure(NonEmptyList(fail 1, fail 2))
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 8 / 61
scalaz.ValidationNel
Dangerous flatMapscala> "fail 1".failureNel[Int].flatMap { a =>
| "fail 2".failureNel[Int].map { b =>| a + b| }| }
warning: there was one deprecation warning; re-run with-deprecation for details
res16: scalaz.Validation[scalaz.NonEmptyList[String],Int] =Failure(NonEmptyList(fail 1))
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 9 / 61
Composition
Defining some functionsimport scalaz.std.function._
import scalaz.syntax.compose._
def add: Int => Int => Int = a => b => a + bval mult: Int => Int => Int = a => b => a * b
Usagescala> add(2) >>> mult(10) >>> add(3) apply 0res18: Int = 23
scala> add(2) <<< mult(10) <<< add(3) apply 0res19: Int = 32
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 10 / 61
What more is possible?
Consider this broken configval confText = """name = "Donald Trump"year_born = 1946nemesis {year_born = "Age of Enlightment"name = 1650
}"""
Both nemesis.year_born and nemesis.name have the wrong type.
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 11 / 61
What more is possible?
Could we catch both defects with this API?case class UserData(name: String, yearBorn: Int)
def sub(key: String): ConfRead[Json] = ???def required[A : Readable](key: String): ConfRead[A] = ???
val userRead: ConfRead[UserData] =(required[String]("name") |@| required[Int]("year_born")).apply(UserData)
def fullRead: ConfRead[(UserData, UserData)] =(userRead |@| (sub("nemesis") >>> userRead)).tupled
Config.parse map fullRead.read
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 12 / 61
Type Classes (a review)
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 13 / 61
Useful type classes
We’ll quickly review:
SemigroupComposeCovariant FunctorApplicative FunctorMonad
Also useful, but not discussed today:
ProfunctorArrow or Strong Profunctor
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 14 / 61
High quality type classes
Type classes are statically dispatched interfaces that should
be implementable for many data typesprovide functions that lead to rich librarieshave only one instance per type (global uniqueness) 1
have laws (strong contracts).
Auto-deriving instances is also nice.
1Global uniqueness is debated by a fewSukant Hajra / @shajra Validation with Functional Programming April 14, 2016 15 / 61
Semigroup
Definitiontrait Semigroup[A] {def append(x: A, y: A): A
}
object Semigroup {def apply[A](implicit ev: Semigroup[A]): Semigroup[A] = ev
}
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 16 / 61
Semigroup
Lawstrait SemigroupLaws[A] extends Semigroup[A] {
def semigroupAssociativity(x: A, y: A, z: A) =append(x, append(y, z)) == append(append(x, y), z)
}
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 17 / 61
Semigroup
Syntaximplicit class SemigroupOps[A : Semigroup](a1: A) {
def |+|(a2: A): A = Semigroup[A].append(a1, a2)
}
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 18 / 61
Compose
Definitiontrait Compose[F[_, _]] {def compose[A, B, C](fbc: F[B, C], fab: F[A, B]): F[A, C]
}
object Compose {def apply[F[_, _]](implicit ev: Compose[F]): Compose[F] = ev
}
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 19 / 61
Compose
Lawstrait ComposeLaws[F[_, _]] extends Compose[F] {
def composeAssociativity[A, B, C, D](fab: F[A, B], fbc: F[B, C], fcd: F[C, D]) =
compose(fcd, compose(fbc, fab)) ==compose(compose(fcd, fbc), fab)
}
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 20 / 61
Compose
Syntaximplicit class ComposeOps[F[_, _], A, B](f1: F[A, B])(
implicit ev: Compose[F]) {
def <<<[C](f2: F[C, A]): F[C, B] = Compose[F].compose(f1, f2)
def >>>[C](f2: F[B, C]): F[A, C] = Compose[F].compose(f2, f1)
}
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 21 / 61
(Covariant) Functor
Definitiontrait Functor[F[_]] {def map[A, B](fa: F[A])(f: A => B): F[B]
}
object Functor {def apply[F[_]](implicit ev: Functor[F]): Functor[F] = ev
}
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 22 / 61
(Covariant) Functor
Lawstrait FunctorLaws[F[_]] extends Functor[F] {
def functorIdentity[A](fa: F[A]) =map(fa)(identity[A]) == fa
def functorComposition[A, B, C](fa: F[A], f: A => B, g: B => C) =
map(map(fa)(f))(g) == map(fa)(f andThen g)
}
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 23 / 61
Functor
Syntaximplicit class FunctorOps[F[_] : Functor, A](fa: F[A]) {
def map[B](f: A => B): F[B] = Functor[F].map(fa)(f)
def fpair: F[(A, A)] = map { a => (a, a) }
def fproduct[B](f: A => B): F[(A, B)] =map { a => (a, f(a)) }
}
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 24 / 61
Applicative (Functor)
Definitiontrait Applicative[F[_]] extends Functor[F] {
def pure[A](a: A): F[A]
def ap[A, B](fa: F[A])(fab: F[A => B]): F[B]
}
object Applicative {def apply[F[_]](implicit ev: Applicative[F]): Applicative[F] = ev
}
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 25 / 61
Applicative (Functor)
Lawstrait ApplicativeLaws1[F[_]] extends
Applicative[F] withFunctorLaws[F] {
def applicativeIdentity[A](fa: F[A]) =ap(fa)(pure[A => A](identity)) == fa
def applicativeHomomorphism[A, B](a: A, ab: A => B) =ap(pure(a))(pure(ab)) == pure(ab(a))
def applicativeInterchange[A, B](a: A, f: F[A => B]) =ap(pure(a))(f) == ap(f)(pure { (ff: A => B) => ff(a) })
}
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 26 / 61
Applicative (Functor)
Lawstrait ApplicativeLaws2[F[_]] extends ApplicativeLaws1[F] {
def applicativeDerivedMap[A, B](f: A => B, fa: F[A]) =map(fa)(f) == ap(fa)(pure(f))
def applicativeComposition[A, B, C](fa: F[A], fab: F[A => B], fbc: F[B => C]) =
ap(ap(fa)(fab))(fbc) ==ap(fa)(ap(fab)(map[B=>C, (A=>B)=>(A=>C)](fbc) {bc => ab => bc compose ab }))
}
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 27 / 61
Applicative (Functor)
Syntaximplicit class ApplicativeOps1[F[_] : Applicative, A](
fa: F[A]) {
def <*>[A, B](f: F[A => B]) = Applicative[F].ap(fa)(f)
def *>[B](fb: F[B]): F[B] =Applicative[F].ap(fa)(Functor[F].map(fb)(Function.const))
def <*[B](fb: F[B]): F[A] =Applicative[F].ap(fb)(fa map Function.const)
}
implicit class ApplicativeOps2[A](a: A) {def pure[F[_] : Applicative]: F[A] = Applicative[F].pure(a)
}
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 28 / 61
Monad
Definitiontrait Monad[F[_]] extends Applicative[F] {
def bind[A, B](fa: F[A])(fab: A => F[B]): F[B]
}
object Monad {def apply[F[_]](implicit ev: Monad[F]): Monad[F] = ev
}
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 29 / 61
Monad
Lawstrait MonadLaws[F[_]] extends Monad[F] {
def monadRightIdentity[A](fa: F[A]) =bind(fa)(pure) == fa
def monadLeftIdentity[A, B](a: A, f: A => F[B]) =bind(pure(a))(f) == f(a)
def monadAssociativity[A, B, C](fa: F[A], f: A => F[B], g: B => F[C]) =
bind(bind(fa)(f))(g) == bind(fa) { a => bind(f(a))(g) }
def monadDerivedAp[A, B](fab: F[A => B], fa: F[A]) =bind(fa) { a => map(fab) { f => f(a) } } == ap(fa)(fab)
}
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 30 / 61
Monad
Syntaximplicit class MonadOps[F[_] : Monad, A](fa: F[A]) {
def flatMap[B](f: A => F[B]): F[B] = Monad[F].bind(fa)(f)
def >>=[B](f: A => F[B]): F[B] = flatMap(f)
def >>![B](f: A => F[B]): F[A] =flatMap { a => Monad[F].map(f(a)) { b => a } }
def >>[B](fb: F[B]): F[B] = flatMap(Function.const(fb))
import scalaz.Leibniz.===
def join[B](implicit ev: A === F[B]): F[B] =Monad[F].bind(ev.subst[F](fa))(identity)
}
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 31 / 61
Data types
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 32 / 61
Non-empty List
Definitionobject nel {
case class Nel[A](head: A, tail: List[A]) {
def toList: List[A] = head :: tail
override def toString: String =s"""Nel(${head}, ${tail mkString ","})"""
}
object Nel {def of[A](head: A, tail: A*): Nel[A] =new Nel(head, tail.toList)
}
}; import nel._
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 33 / 61
Non-empty List
Semigroup Instanceimport scalaz.Semigroupimport scalaz.std.list._
import scalaz.syntax.semigroup._
implicit def nelSemigroup[A]: Semigroup[Nel[A]] =new Semigroup[Nel[A]] {def append(x: Nel[A], y: => Nel[A]) =Nel[A](x.head, x.tail |+| (y.head :: y.tail))
}
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 34 / 61
Non-empty List
Monad Instanceimport scalaz.Monadimport scalaz.syntax.monad._
implicit val nelMonad: Monad[Nel] =new Monad[Nel] {def point[A](a: => A) = Nel(a, List.empty)def bind[A, B](as: Nel[A])(f: A => Nel[B]) = {val front = f(as.head)Nel(front.head,front.tail |+| (as.tail >>= (f andThen { _.toList })))
}}
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 35 / 61
Non-empty List
Usagescala> Nel.of(1,2,3) |+| Nel.of(4,5,6)res2: nel.Nel[Int] = Nel(1, 2,3,4,5,6)
scala> Nel.of(1,2) >>= { x => Nel.of(x, x+10) }res3: nel.Nel[Int] = Nel(1, 11,2,12)
scala> (Nel.of(1,2) |@| Nel.of(10,100)).apply { _ * _ }res4: nel.Nel[Int] = Nel(10, 100,20,200)
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 36 / 61
Disjunction
Definitionobject disj {
sealed trait Disj[A, B] {def fold[C](ifLeft: A => C)(ifRight: B => C) =this match {case LeftD(a) => ifLeft(a)case RightD(b) => ifRight(b)
}}
case class LeftD[A, B](a: A) extends Disj[A, B]case class RightD[A, B](b: B) extends Disj[A, B]
}; import disj._
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 37 / 61
Disjunction
Syntaximplicit class DisjOps[A](a: A) {def left[B]: Disj[A, B] = LeftD(a)def right[B]: Disj[B, A] = RightD(a)
}
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 38 / 61
Disjunction
Instancesimplicit def disjMonad[E]: Monad[Disj[E, ?]] =new Monad[Disj[E, ?]] {
def point[A](a: => A) = a.right
def bind[A, B](d: Disj[E, A])(f: A => Disj[E, B]) =d.fold { _.left[B] } { f(_) }
}
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 39 / 61
Disjunction
Usagescala> (1.right[String] |@| 2.right[String]).
| apply { _ + _ }res5: disj.Disj[String,Int] = RightD(3)
scala> ("fail 1".left[Int] |@| "fail 2".left[Int]).| apply { _ + _ }
res6: disj.Disj[String,Int] = LeftD(fail 1)
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 40 / 61
Validation
Definitionobject checked {
sealed trait Checked[A, B] {def fold[C](ifFail: A => C)(ifPass: B => C) =this match {case Fail(a) => ifFail(a)case Pass(b) => ifPass(b)
}}
case class Fail[A, B](a: A) extends Checked[A, B]case class Pass[A, B](b: B) extends Checked[A, B]
}; import checked._
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 41 / 61
Validation
Syntaximplicit class CheckedOps1[A](a: A) {def pass[B]: Checked[B, A] = Pass(a)def fail[B]: Checked[A, B] = Fail(a)def passNel[B]: Checked[Nel[B], A] = Pass(a)def failNel[B]: Checked[Nel[A], B] = Fail(Nel of a)
}
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 42 / 61
Validation
Instancesimport scalaz.Applicative
implicit def checkedApplicative[E : Semigroup]: Applicative[Checked[E, ?]] =
new Applicative[Checked[E, ?]] {
def point[A](a: => A) = a.pass[E]
def ap[A, B](c: => Checked[E, A])(f: => Checked[E, A=>B]) =c.fold { e1 =>f.fold(e2 => (e1 |+| e2).fail[B])(_ => e1.fail[B])
} { a =>f.fold(_.fail[B])(_.apply(a).pass[E])
}
}
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 43 / 61
Validation
Usagescala> (1.passNel[String] |@| 2.passNel[String]).
| apply { _ + _ }res8: checked.Checked[nel.Nel[String],Int] = Pass(3)
scala> ("fail 1".failNel[Int] |@| "fail 2".failNel[Int]).| apply { _ + _ }
res9: checked.Checked[nel.Nel[String],Int] = Fail(Nel(fail 2,fail 1))
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 44 / 61
Read
Definitioncase class Read[F[_], A, B](read: A => F[B])
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 45 / 61
Read
Compose Instanceimport scalaz.Compose
implicit def readCompose[F[_] : Monad]: Compose[Read[F, ?, ?]] =
new Compose[Read[F, ?, ?]] {def compose[A, B, C](f: Read[F, B, C], g: Read[F, A, B]) =Read { a => g.read(a) >>= f.read }
}
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 46 / 61
Read
Applicative Instanceimplicit def readApplicative[F[_] : Applicative, A]
: Applicative[Read[F, A, ?]] =new Applicative[Read[F, A, ?]] {def point[B](b: => B) = Read { a => b.pure[F] }def ap[B, C](r: => Read[F, A, B])(f: => Read[F, A, B=>C]) =Read { a => (f.read(a) |@| r.read(a)) { _ apply _ } }
}
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 47 / 61
ReadC
Definitioncase class ReadC[A, E, B](read: A => Checked[E, B])
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 48 / 61
ReadC
Instancesimplicit def readCCompose[E]: Compose[ReadC[?, E, ?]] =new Compose[ReadC[?, E, ?]] {def compose[A, B, C]
(f: ReadC[B, E, C], g: ReadC[A, E, B]) =ReadC { a => g.read(a).fold(_.fail[C])(f.read) }
}
implicit def readCApplicative[A, E : Semigroup]: Applicative[ReadC[A, E, ?]] =
new Applicative[ReadC[A, E, ?]] {def point[B](b: => B) =ReadC { a => b.pure[Checked[E, ?]] }
def ap[B, C](r: => ReadC[A, E, B])(f: => ReadC[A, E, B=>C]) =
ReadC { a => (f.read(a) |@| r.read(a)) { _ apply _ } }}
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 49 / 61
Parsing Example
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 50 / 61
Knobs library extension
Tracking path traversedimport scalaz.Showimport scalaz.std.string._
import scalaz.syntax.foldable._
case class Path(elems: List[String]) {
def -\(elem: String) = new Path(elem.trim +: elems)
override def toString = elems.reverse intercalate "."
}
def emptyPath: Path = new Path(List.empty)
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 51 / 61
Knobs library extension
Path-Config pairimport knobs.Config
case class Knobs(path: Path, config: Config) {def -\(elem: String): Knobs =Knobs(path -\ elem, config subconfig elem)
}
def rootKnobs(config: Config): Knobs =Knobs(emptyPath, config)
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 52 / 61
Knobs library extension
Faultsimport scala.reflect.runtime.universe.{ typeTag, TypeTag }
sealed abstract class ParseFault { def path: Path }
final case class KeyNotFound(path: Path) extends ParseFault
final case class WrongType(path: Path, typetag: TypeTag[_])extends ParseFault
def keyNotFound(path: Path): ParseFault =KeyNotFound(path)
def wrongType[A : TypeTag](path: Path): ParseFault =WrongType(path, typeTag[A])
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 53 / 61
Knobs library extension
Type aliastype Parse[A, B] = ReadC[A, Nel[ParseFault], B]type KnobsRead[A] = Parse[Knobs, A]
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 54 / 61
Knobs library extension
Read subconfigdef sub(path: String): KnobsRead[Knobs] =ReadC { _ -\ path passNel }
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 55 / 61
Knobs library extension
Lookup configurationimport knobs.{ CfgValue, Configured }
def required[A : Configured : TypeTag](path: String):KnobsRead[A] =
ReadC { conf =>val reportedPath = conf.path -\ pathconf.config.lookup[CfgValue](path).fold(keyNotFound(reportedPath).fail[CfgValue])(_.pass).fold(_.failNel[A]) { v =>v.convertTo[A].fold(wrongType[A](reportedPath).failNel[A])(_.passNel)
}}
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 56 / 61
Knobs library extension
Config readerimport scalaz.syntax.compose._
case class UserData(name: String, yearBorn: Int)
val userRead: KnobsRead[UserData] =(required[String]("name") |@| required[Int]("year_born")).apply(UserData)
def fullRead: KnobsRead[(UserData, UserData)] =(userRead |@| (sub("nemesis") >>> userRead)).tupled
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 57 / 61
Knobs library extension
Broken config exampleval confText = """name = "Donald Trump"year_born = 1946nemesis {year_born = "Age of Enlightment"name = 1650
}"""
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 58 / 61
Knobs library extension
Config readerscala> Config.parse(confText).map { conf =>
| fullRead read rootKnobs(conf)| }
res25:scalaz.\/[Throwable,checked.Checked[nel.Nel[ParseFault],(UserData,UserData)]] =\/-(Fail(Nel(WrongType(nemesis.year_born,TypeTag[Int]),WrongType(nemesis.name,TypeTag[String]))))
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 59 / 61
Wrapping up
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 60 / 61
This material
is compiler-checked (half-literate programming) using:I Rob Norris’s tut SBT pluginI pandocI LATEXI Beamer
is at https://github.com/shajra/shajra-presentations.
Sukant Hajra / @shajra Validation with Functional Programming April 14, 2016 61 / 61