elm-community / parser-combinators

A parser combinator library for Elm.

Home Page:http://package.elm-lang.org/packages/elm-community/parser-combinators/latest

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Pipeline Combiner?

BrianHicks opened this issue · comments

I'm messing around with elm-combine for a project and wanted to use parsers in a pipeline style. I've come up with something that allows you to write code like this (new stuff at the very bottom):

type alias S3URL =
    { protocol : String
    , bucket : String
    , path : String
    }

{-| s3url parses URLs of the form `s3://bucket/path/to/file.tgz`
-}
s3url : Parser S3URL
s3url =
    let
        protocol =
            while ((/=) ':')

        bucket =
            while ((/=) '/')

        path =
            many anyChar |> map String.fromList
    in
        start S3URL
            |> consume protocol
            |> ignore (Combine.string "://")
            |> consume bucket
            |> consume path
  • start is an alias for succeed
  • consume is flip andMap
  • ignore is the only custom parser. It takes a parser to keep and a parser to skip, applies the parser to keep, and returns that result with the context from the skipped parser. The code is messy, but it could certainly be cleaned up. :)

The names of the functions are, of course, up for debate. I have this in my project as Combine.Pipeline, in the same way as json-decode-pipeline is Json.Decode.Pipeline.

Would this be a useful addition to elm-combine? Is this even a reasonable way to approach parser combinators?

Thanks for bringing this up! I've recently started thinking about this as well although I have yet to jot down a pipeline-friendly API. I think this could work well for small parsers, but I'm not sure about large/complex ones (like elm-ast or the Python example in this repo).

It's worth noting that the upcoming breaking changes already make pipelining a lot easier:

import Combine exposing (..)

-- Pipeline stuff
start : a -> Parser s a
start = succeed

consume : Parser s a -> Parser s (a -> b) -> Parser s b
consume = andMap

ignore : Parser s x -> Parser s a -> Parser s a
ignore p =
  andThen (($>) (skip p))

-- S3 URL
type alias S3URL =
    { protocol : String
    , bucket : String
    , path : String
    }

scheme : Parser s String
scheme =
  while ((/=) ':')

bucket : Parser s String
bucket =
  while ((/=) '/')

anything : Parser s String
anything =
  while (always True)

s3urlApplicative : Parser s S3URL
s3urlApplicative =
  S3URL
    <$> scheme <* string "://"
    <*> bucket
    <*> anything

s3urlPipelinedMaster : Parser s S3URL
s3urlPipelinedMaster =
  succeed S3URL
    |> andMap scheme
    |> ignore (string "://") -- I could have written scheme as `while ((/=) ':') <* string "://"`, making ignore redundant
    |> andMap bucket
    |> andMap anything

s3urlPipelinedMaster2 : Parser s S3URL
s3urlPipelinedMaster2 =
  map S3URL (scheme <* string "://")
    |> andMap bucket
    |> andMap anything

s3urlPipelined : Parser s S3URL
s3urlPipelined =
  start S3URL
    |> consume scheme
    |> ignore (string "://")
    |> consume bucket
    |> consume anything

That being said, although I prefer the applicative version over the rest, I think having pipeline-friendly aliases does make sense. Would you mind trying to convert the example parsers (I'd start with Calc, then Scheme and then maybe Python if you're feeling adventurous) to a pipeline style so we can compare and contrast to see what could be gained for more complex use cases?

It might also be worth taking a look at how parser combinator libraries in other pipeline-friendly languages[1][2] do things.

After playing around with it a little more (and waking up a little more) I've concluded that you're right. The applicative way of doing it is more expressive. Closing!