- Parsing with Data.Yaml.Combinators, part 1
- Parsing with Data.Yaml.Combinators, part 2
- Parsing with Data.Yaml.Combinators, part 3
In my last post I made a first attempt at the parser. This time I will show how to turn a parser of Text into a parser of a new type, Gender. This will seem ludicrously easy if you know Haskell, but as I’m just learning the hard-won skill of deciphering the types of functions and operators, it took me a while to figure this out.
Here is the new input file:
persons: | |
- name: 'Coco Lenoix' | |
address: '1612 Havenhurst Drive' | |
gender: 'female' | |
- name: 'Diane Selwyn' | |
address: '2900 Griffith Park Boulevard' | |
gender: 'female' | |
organizations: | |
- name: 'StudioCanal' | |
address: '1 Place du Spectacle' | |
- name: 'ABC Studios' | |
address: '77 West 66th Street' |
We want to parse that gender field, but we don’t want it to end up a Text object. We want to store it in our resulting address book as a Gender object. The Gender object has a very simple type declaration.
data Gender = Male | |
| Female | |
| Unassigned | |
deriving Show |
The Unassigned constructor will be introduced in the next part of the series, where I’ll make the gender field optional.
So we will need a parser of Gender. If we look at the string in Data.Yaml.Combinators, we see that it creates a parser of Text.
string :: Parser Text
And if we want to make a field parser from a Parser we can use the field function.
field :: Text -> Parser a -> FieldParser a
That’s why we have the lines like:
personParser :: Parser Person | |
personParser = object $ Person | |
<$> field "name" string | |
<*> field "address" string |
Each of those create a FieldParser Text. FieldParser is an instance of Applicative and the <*> operator for the instance has the type:
(<*>) :: FieldParser (a -> b) -> FieldParser a -> FieldParser b
I.e. it takes a FieldParser that wraps a function from a to b, and an a wrapped in a FieldParser. It applies the function to the a and ends up with a b wrapped in a FieldParser. In effect it allows us to chain FieldParsers to create a multi-field-parser.
The <$> is another name for fmap and has the following type:
(<$>) :: (a -> b) -> FieldParser a -> FieldParser b
It takes a function, in this case the constructor Person and a FieldParser a (in our case FieldParser Text), and creates a new FieldParser. This is chained with fieldparsers for each field until it ends up being a FieldParser Person.
object is used to turn FieldParser Person into a Parser Person.
object :: FieldParser a -> Parser a
With our new field of Gender type we just need to chain a field parser for the field gender onto the end of personParser.
personParser :: Parser Person | |
personParser = object $ Person | |
<$> field "name" string | |
<*> field "address" string | |
<*> field "gender" genderParser |
The field function takes a Text and a FieldParser a. So our genderParser needs to have the type:
genderParser :: Parser Gender
To create that we will use the fmap function. fmap in Functor instance of Parser takes a function from a to b and a Parser a and creates a Parser b.
fmap :: (a-> b) -> Parser a -> Parser b
If we substitute Gender for b and Text for a we see that all we need is a function from Text to Gender to be able to make a Parser Gender from a Parser Text. We will provide that as a simple lambda and I use the LambdaCase extension to make that a little bit prettier.
{-# LANGUAGE OverloadedStrings #-} | |
{-# LANGUAGE LambdaCase #-} |
I use string to get a Parser Text. I then fmap the lambda over the Parser Text to transform it into a Parser Gender.
genderParser :: Parser Gender | |
genderParser = | |
fmap (\case "male" -> Male | |
"female" -> Female | |
_ -> error "gender: shall be either 'male' or 'female'.") string | |
organizationParser :: Parser Organization | |
organizationParser = object $ Organization |
We can now build and run to see the new output:
> contacts-parser input.yaml
Contacts [Person “Coco Lenoix” “1612 Havenhurst Drive” Female,Person “Diane Selwyn” “2900 Griffith Park Boulevard” Female] [Organization “StudioCanal” “1 Place du Spectacle”,Organization “ABC Studios” “77 West 66th Street”]
In the next part we will make the gender field optional, as we don’t all want to be assigned a gender. We will also look at a way to build an inner object for the address based on multiple fields in the Person and Organization objects.
Since I’m learning Haskell as I go here, please, if you notice me doing something in a non-idiomatic way, or if I’m making a statement that is obviously false, just put a comment below and I will both be grateful and try to set things straight.