Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Applicative Style Command Line Parsing

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.

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.

Avatar for Mark Hibberd

Mark Hibberd

July 09, 2014
Tweet

More Decks by Mark Hibberd

Other Decks in Programming

Transcript

  1. ! 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 } !
  2. ! 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
  3. ! 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
  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 } ! unsafe
  5. ! 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 }
  6. ! 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 }
  7. ! 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
  8. ! 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
  9. ! 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
  10. ! 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]
  11. ! 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]
  12. ! 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()
  13. |*|

  14. ! 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 )
  15. 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 }
  16. 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 }
  17. 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 }