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 forsucceed
consume
isflip 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!