$30 off During Our Annual Pro Sale. View Details »

Applicative Style Command Line Parsing

Applicative Style Command Line Parsing

Using command line parsing as an example, this will be a (code heavy) look at an interesting use of a data structure that gives us the ability to build-up functions that can act as both _parsers_ and _printers_. We will then explore how we can exploit applicative functors to build up a practical and clean API around this data structure.

Mark Hibberd

July 09, 2014
Tweet

More Decks by Mark Hibberd

Other Decks in Programming

Transcript

  1. Applicative Style
    Command Line Parsing
    @markhibberd

    View Slide

  2. View Slide

  3. Who let the
    programmers loose?

    View Slide

  4. !
    1 case class Person(name: String, age: Int)
    2
    3 object App {
    4 def main(args: Array[String]): Unit = {
    5 val dfault = Person("something-invalid", -1)
    6 val parser = Parser[Args]
    7 .option[String]("--name",
    (a: Args, s: String) => a.copy(name = s))
    8 .option[Int]("--age",
    (a: Args, n: Int) => a.copy(age = n))
    9 parser.dispatch(p, dfault) { p =>
    10 /* not really winning */
    11 }
    12 }
    13 }
    !

    View Slide

  5. !
    1 case class Person(name: String, age: Int)
    2
    3 object App {
    4 def main(args: Array[String]): Unit = {
    5 val dfault = Person("something-invalid", -1)
    6 val parser = Parser[Args]
    7 .option[String]("--name",
    (a: Args, s: String) => a.copy(name = s))
    8 .option[Int]("--age",
    (a: Args, n: Int) => a.copy(age = n))
    9 parser.dispatch(p, dfault) { p =>
    10 /* not really winning */
    11 }
    12 }
    13 }
    !
    making things up

    View Slide

  6. !
    1 case class Person(name: String, age: Int)
    2
    3 object App {
    4 def main(args: Array[String]): Unit = {
    5 val dfault = Person("something-invalid", -1)
    6 val parser = Parser[Args]
    7 .option[String]("--name",
    (a: Args, s: String) => a.copy(name = s))
    8 .option[Int]("--age",
    (a: Args, n: Int) => a.copy(age = n))
    9 parser.dispatch(p, dfault) { p =>
    10 /* not really winning */
    11 }
    12 }
    13 }
    !
    tight coupling

    View Slide

  7. !
    1 case class Person(name: String, age: Int)
    2
    3 object App {
    4 def main(args: Array[String]): Unit = {
    5 val dfault = Person("something-invalid", -1)
    6 val parser = Parser[Args]
    7 .option[String]("--name",
    (a: Args, s: String) => a.copy(name = s))
    8 .option[Int]("--age",
    (a: Args, n: Int) => a.copy(age = n))
    9 parser.dispatch(p, dfault) { p =>
    10 /* not really winning */
    11 }
    12 }
    13 }
    !
    unsafe

    View Slide

  8. Yes this is made-up -
    but I assure you, reality is
    far worse

    View Slide

  9. Who let the functional
    programmers loose?

    View Slide

  10. !
    1 case class Parse[A](parse: List[String]
    => ParseError \/ (List[String], A)) {
    2 def map[B](f: A => B): Parse[B] = ???
    3 def flatMap[B](f: A => Parse[B]): Parse[B] = ???
    4 def |||(that: => Parse[A]): Parse[A] = ???
    5 }

    View Slide

  11. !
    1 case class Person(name: String, age: Int)
    2
    3 object App {
    4 def main(args: Array[String]): Unit = {
    5 val parser = for {
    6 name <- option[String]("--name")
    7 age <- option[Int]("--age")
    8 } yield Person(name, age)
    9
    10 parser.dispatch(args) { x =>
    11 /* not really winning */
    12 }
    13 }
    14 }

    View Slide

  12. !
    1 case class Person(name: String, age: Int)
    2
    3 object App {
    4 def main(args: Array[String]): Unit = {
    5 val parser = for {
    6 name <- option[String]("--name")
    7 age <- option[Int]("--age")
    8 } yield Person(name, age)
    9
    10 parser.dispatch(args) { x =>
    11 /* not really winning */
    12 }
    13 }
    14 }
    this is actually better

    View Slide

  13. !
    1 case class Person(name: String, age: Int)
    2
    3 object App {
    4 def main(args: Array[String]): Unit = {
    5 val parser = for {
    6 name <- option[String]("--name")
    7 age <- option[Int]("--age")
    8 } yield Person(name, age)
    9
    10 parser.dispatch(args) { x =>
    11 /* not really winning */
    12 }
    13 }
    14 }
    !
    but the price is too high

    View Slide

  14. name

    View Slide

  15. View Slide

  16. View Slide

  17. flatMap

    View Slide

  18. !
    => age

    View Slide

  19. !
    =>

    View Slide

  20. !
    =>

    View Slide

  21. !
    =>
    !
    =>
    person

    View Slide

  22. Monads are for Beginners

    View Slide

  23. View Slide

  24. !
    1 def pure (v: A) : F[A]
    !
    2 def map (f: A => B )(v: F[A]) : F[B] // Functor
    !
    3 def flatMap (f: A => F[B])(v: F[A]) : F[B] // Monad
    !
    4 def ap (f: F[A => B])(v: F[A]) : F[B] // Applicative

    View Slide

  25. age
    name

    View Slide

  26. age
    !
    map Person name

    View Slide

  27. age => Person age
    !
    map Person name

    View Slide

  28. age => Person age
    !
    ap
    !
    map Person name

    View Slide

  29. person
    age => Person age
    !
    ap
    !
    map Person name

    View Slide

  30. !
    1 sealed trait Name
    2 case class Short(s: Char) extends Name
    3 case class Long(l: String) extends Name
    4 case class Both(s: Char, l: String) extends Name
    5
    6 sealed trait Parser[A]
    7 case class SwitchParser[A](name: Name, a: A) extends Parser[A]
    8 case class FlagParser[A](name: Name, read: Read[A]) extends Parser[A]
    9 case class ArgParser[A](p: Read[A]) extends Parser[A]
    10 case class SubCommandParser[A](p: Read[A]) extends Parser[A]

    View Slide

  31. !
    1 sealed trait ParserA[A]
    2 case class Empty[A]() extends ParserA[A]
    3 case class Value[A](a: A) extends ParserA[A]
    4 case class Parse[A](p: Parser[A], meta: Meta) extends ParserA[A]
    5 case class Ap[A, B](p: ParserA[A => B], a: ParserA[A]) extends ParserA[B]
    6 case class Alt[A](a: ParserA[A], b: ParserA[A]) extends ParserA[A]

    View Slide

  32. !
    1 def switch( n: Name): ParserA[Boolean] =
    2 Parse(SwitchParser(n, true),
    3 Meta(None, true)) ||| Value(false)
    4
    5 def option[A: Read](n: Name, meta: String): ParserA[A] =
    6 Parse(FlagParser(n, List(meta), Read.of[A]),
    7 Meta(None, true)) ||| Empty()

    View Slide

  33. |*|

    View Slide

  34. f |*| (a, b, c) ~>
    map -> ap -> ap

    View Slide

  35. f |*| (a, b, c) ~>
    (a |@| b |@| c)(f)

    View Slide

  36. f |*| (a, b, c) ~>
    ^(a, b, c)(f)

    View Slide

  37. f |*| (a, b, c) ~>
    f <$> a <*> b <*> c

    View Slide

  38. !
    1 case class Person(awesome: Boolean, name: String, age: Int)
    2
    3 val parser: ParserA[Person] = Person |*| (
    4 switch(Long("awesome"))
    5 , option(Long("name"))
    6 , option(Long("age"))
    7 )

    View Slide

  39. person
    age => Person age
    !
    ap
    !
    map Person name

    View Slide

  40. 1 trait Traversal[A] {
    2 def run[X](p: Parser[X], meta: Meta): A
    3 }
    4
    5 def traversal[A, B](p: ParserA[A], f: Traversal[B]): List[B] = p match {
    6 case Empty() => Nil
    7 case Value(_) => Nil
    8 case Parse(p, m) => List(f.run(p, m))
    9 case Ap(p, a) => traversal(p, f) ++ traversal(a, f)
    10 case Alt(p1, p2) => traversal(p1, f) ++ traversal(p2, f)
    11 }

    View Slide

  41. 1 case class Info(....)
    2
    3 object Info { implicit def InfoMonoid: Monoid[Info] = ??? }
    4
    5 class Usage extends Traversal[Info] {
    6 def run[X](p: Parser[X], meta: Meta): A = p match {
    7 case SwitchParser(...) =>
    8 case FlagParser(...) =>
    9 case ArgParser(...) =>
    10 case SubCommandParser(...) =>
    11 }
    12 }
    13
    14 object Usage {
    15 def get[A](p: ParserA[A]): Info =
    16 traverse(p, new Usage).suml
    17 }

    View Slide

  42. 1 trait Search[F[_], A] {
    2 def run[X](p: Parser[X]): F[A]
    3 }
    4
    5 def search[F[_}: MonadPlus, A](p: ParserA[A], f: Search[A])
    : F[ParserA[A]] =
    6 p match {
    7 case Empty() =>
    8 MonadPlus[F].empty
    9 case Value(_) =>
    10 MonadPlus[F].empty
    11 case Parse(p, m) =>
    12 f.run(p).map(_.pure[ParserA])
    13 case Ap(p, a) =>
    14 search(p, f).flatMap(x => (a <*> x).pure[F]) <|>
    15 search(p, a).flatMap(x => (x <*> k).pure[F])
    16 case Alt(p1, p2) =>
    17 search(f, p1) <+> search(f, p2)
    18 }

    View Slide

  43. Code

    View Slide