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.

42d9867a0fee0fa6de6534e9df0f1e9b?s=128

Mark Hibberd

July 09, 2014
Tweet

Transcript

  1. Applicative Style Command Line Parsing @markhibberd

  2. None
  3. Who let the programmers loose?

  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 } !
  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
  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
  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
  8. Yes this is made-up - but I assure you, reality

    is far worse
  9. Who let the functional programmers loose?

  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 }
  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 }
  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
  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
  14. name

  15. None
  16. None
  17. flatMap

  18. ! => age

  19. ! =>

  20. ! =>

  21. ! => ! => person

  22. Monads are for Beginners

  23. None
  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
  25. age name

  26. age ! map Person name

  27. age => Person age ! map Person name

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

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

    name
  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]
  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]
  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()
  33. |*|

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

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

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

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

    b <*> c
  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 )
  39. person age => Person age ! ap ! map Person

    name
  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 }
  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 }
  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 }
  43. Code