Parsing with Data.Yaml.Combinators, part 2

This entry is part 2 of 3 in the series Data.Yaml.Combinators

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'
view raw input.yaml hosted with ❤ by GitHub

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
view raw Contacts.hs hosted with ❤ by GitHub

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
view raw Contacts.hs hosted with ❤ by GitHub

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
view raw Contacts.hs hosted with ❤ by GitHub

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 #-}
view raw Contacts.hs hosted with ❤ by GitHub

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
view raw Contacts.hs hosted with ❤ by GitHub

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.

Series Navigation<< Parsing with Data.Yaml.Combinators, part 1Parsing with Data.Yaml.Combinators, part 3 >>

Leave a Reply

Your email address will not be published.