That’s a peppy purple person you’ve parsed there.

The Purple People Parser

Where we purposefully parse probably purple people.

Michel Belleville
Published in
6 min readMay 10, 2020

--

me> Now what?

wat> Yes 😃

me> Not you. I mean, now we know a Parser is a Functor, an Applicative and a Monad… what do we do with one? 😅

wat> We parse, I guess?

me> But how? 😅 😅

wat> Here, have a look at this:

run : Parser a -> String -> Result (List DeadEnd) a

me> Reminds me of Json.Decode.decodeString

wat> Yep ; while Json.Decode.decodeString uses a Decoder a and a String (or Value) to give you a Result error a, Parser.run works a Parser a onto a String to give you a Result (List DeadEnd) a.

me> A Result error a being a type that can either hold a successful value (a here) or an error descriptor (error here), we know that List DeadEnd is the type of errors we might get running the Parser, right?

wat> That’s right 🎓 How about we write our first Parser then?

me> Let’s... But what should we parse?

wat> Let’s say I have these types:

type alias ColorfulPerson =
{ firstName: String
, lastName: String
, color: Color
, heads: Int
}
type Color
= Blue
| Green
| Purple
-- (our database is mostly aliens and cosplayers)

me> So far, so good.

wat> Now let’s say we’re exploiting an unusual database format (aliens don’t like to use JSON) that serializes a person in a String like this:

"(Vraakraks|Cinqeids[green;5])"
^ ^ ^ ^
firstName lastName color heads
-- we call their 4 first head 'Vraak' but the 5th insists on
-- the formal 'their Fluidities of house Cinqeids'

me> That looks needlessly complicated indeed.

wat> Yep. Let’s un-complicate this 😉

me> How should we proceed?

wat> As usual, let’s see whether we can cut this big problem into a series of smaller problems. Let’s start with the number of heads. If we only had to parse the number of heads, not bothering with characters around, we could use the built-in Parser.int parser, right?

me> Right. One solved, many to go. What about the… color?

wat> An intersting choice. As you see, we only have three options for colors, and they will be spelled out for us lowercase. Which means we could treat them like keywords, and I think I have just the Parser for that:

keyword : String -> Parser ()

me> Sooo… this is one of the strange ones we encountered last time, isn’t it?

wat> Yes. It can only parse the string you gave it ; when it succeeds, you get an empty tuple ()

me> …which is not very useful 😏

wat> Not in itself, no, but remember, when a Parser () succeeds, even though the value () is not very interesting, the successful context is. After all, your Parser () could be a failure. So, how about we use that context to produce aColor value to replace that empty tuple wrapped in a successful Parser?

me> Hmm… so I guess we need a function that takes a Parser a and produces a Parser b without altering the context of success or failure… Sounds a lot like a job for map : (a -> b) -> Parser a -> Parser b and map takes an a -> b transformation function… all I need is a function that takes that useless empty tuple and always returns the right color then.

greenParser : Parser Color
greenParser =
Parser.keyword "green" |> Parser.map (\_ -> Green)
-- we don't care much about the empty tuple so the transformation
-- function is `(\_ -> Green)` and its unnamed, unused argument `_`

wat> Exactly 😁 We leverage the keyword parser to recognize the "green" string and make an empty tuple, then replaced that useless empty tuple with a more interesting Green value that fits in the Color type. Now let’s assume we have, by the same logic, also made a blueParser and a purpleParser

me> …how do we put them all together to parse a string that be either one of those three?

wat> By the power of the oneOf function:

oneOf : List (Parser a) -> Parser a

me> Let me see… oneOf takes a list of Parser a and makes a Parser a out of them… the doc says it’ll try all the Parsers until it finds one that fits… well, looks like we can do this then:

colorParser : Parser Color
colorParser =
oneOf
[ blueParser
, greenParser
, purpleParser
]

wat> There it is, a complete color parser. Brilliant. Now let’s talk about name. What’s in a name? Or rather, what’s not supposed to be in a name?

me> Erm… I guess none of these chars: "()[]|;" since they are kinds of separators/wrappers?

wat> Good. Let’s see how this could help here:

variable :
{ start : Char -> Bool
, inner : Char -> Bool
, reserved : Set String
}
-> Parser String

me> Hmm… so, we give that function some kind of configuration object that has a start function that takes a Char and returns a Bool, I guess to decide where the variable name starts… then there’s a similar inner function which I guess decides whether we’re still parsing a variable name… then a reserved that is a Set String… probably a list of keywords that are not allowed as variable names?

wat> Admit you’re reading the docs right now 😏

me> You said t’s a good habit 😅

wat> That it is 😉 Now let’s define our nameParser:

notNameChars : Set Char
notNameChars =
Set.fromList ['(', ')', '[', ']', '|', ';']
isNameChar : Char -> Bool
isNameChar char=
not (Set.member char notNameChars)
nameParser : Parser String
nameParser =
variable
{ start = isNameChar
, inner = isNameChar
, reserved = Set.empty
}

me> Ok, we have parser for all our fields… now how do we put them all together?

wat> Let’s do something easy first. Let’s say we only have to parse the color and number of heads (and their separator), nothing else. Our input would look like:

green;5

me> Hmm… we’ll have two elements so… I guess we’re going to need that weird infix version of andMap?

wat> We might have to use (|=). Remember its signature?

me> Yup.

(|=) : Parser (a -> b) -> Parser a -> Parser b-- looks like any other `andMap` except the arguments are inverted
-- since it's an infix function that does not need a (|>) to
-- spoon-feed its last argument ;)

wat> Now, let’s say we’re going to put our Color and Int into a (Color, Int) tuple for now, we’ll have something that looks like this:

headAndColorParser : Parser (Color, Int)
headAndColorParser =
Parser.succeed (\color heads -> (color, heads))
|= colorParser
|= Parser.int

me> Wait a minute… and the separator?

wat> Good boy, you’ve noticed my evil trap (that I totally put there on purpose of course). Yes, if we used that parser as is, it wouldn’t match green;5 because after green it would immediately expect an Int and fail on the ;. Very well done indeed.

me> We could do something like this though:

headAndColorParser : Parser (Color, Int)
headAndColorParser =
Parser.succeed (\color _ heads -> (color, heads))
|= colorParser
|= Parser.symbol ";"
|= Parser.int

wat> Yes… but that unused parameter between color and head for the () the symbol parser will produce… I think we can do better.

me> Aw man…

wat> So, let’s introduce a new and useful function:

(|.) : Parser keep -> Parser ignore -> Parser keep

me> This… takes a Parser keep to keep, a Parser ignore to ignore… and keeps the Parser keep. Looks like just the function we need:

headAndColorParser : Parser (Color, Int)
headAndColorParser =
Parser.succeed (\color heads -> (color, heads))
|= colorParser
|. Parser.symbol ";"
|= Parser.int

wat> Now let’s put it all together to produce a proper person now…

me> Let me!

colorfulPersonParser : Parser ColorfulPerson
colorfulPersonParser =
Parser.succeed ColorfulPerson
|. Parser.symbol "("
|= nameParser
|. Parser.symbol "|"
|= nameParser
|. Parser.symbol "["
|= colorParser
|. Parser.symbol ";"
|= Parser.int
|. Parser.symbol "])"

wat> There you go. A precious proper person parser 😀

me> That was easier than I thought.

wat> Isn’t it always… 😼

--

--