Parsing with Data.Yaml.Combinators, part 3

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

In the last part I tried my best to explain the operators used to combine and apply the field parsers to build up an object parser. We also created a parser for one of our own simple types. In this part we will make the inclusion of a gender in a person record optional and we will also address one of the problems I encountered, how to create an inner object based on some of the fields in a record.

Here is the new input.yaml:

persons:
- name: 'Coco Lenoix'
street: 'Havenhurst Drive'
house_number: 1612
- name: 'Diane Selwyn'
street: 'Griffith Park Boulevard'
house_number: 2900
gender: 'female'
- name: 'Adam Kersher'
street: 'Spring Street'
house_number: 634
gender: 'male'
organizations:
- name: 'StudioCanal'
street: 'Place du Spectacle'
house_number: 1
- name: 'ABC Studios'
street: 'West 66th Street'
house_number: 77
view raw input.yaml hosted with ❤ by GitHub

We’ll start with making the gender field optional. This is fine in YAML; you just leave the field out, but our Person type requires a Gender field to be constructed, so we will need to provide a default. We’ll start by importing Data.Maybe (fromMaybe) as the optField function will return a FieldParser (Maybe Gender) and the <*> operator expects a FieldParser Gender. fromMaybe takes a default value of type a and a Maybe a and returns an a.

fromMaybe :: a -> Maybe a -> a
import Data.Maybe (fromMaybe)
view raw Contacts.hs hosted with ❤ by GitHub

But fromMaybe can’t operate directly on the FieldParser (Maybe Gender). Luckily Maybe is an instance of Functor, so we’ll use our trusty fmap again (this time in it’s <$> operator form).

personParser :: Parser Person
personParser = object $ Person
<$> field "name" string
<*> addressParser
<*> (fromMaybe Unassigned <$> optField "gender" genderParser)
view raw Contacts.hs hosted with ❤ by GitHub

That should do it. Now onto the address. As you can see in the input.yaml, the address information has been split up into two parts. Let’s assume we don’t have the clout to effect a better structuring of this data in our input, but we still want to be able to use a better structure in our program. We want an Address type object that is constructed from the street and house_number fields of each record.

data Address = Address
Text -- street
Int -- house_number
deriving Show
data Person = Person
Text -- name
Address
Gender
deriving Show
data Organization = Organization
Text -- name
Address
deriving Show
view raw Contacts.hs hosted with ❤ by GitHub

We’ll stick an addressParser in each of the functions personParser and organizationParser.

personParser :: Parser Person
personParser = object $ Person
<$> field "name" string
<*> addressParser
<*> (fromMaybe Unassigned <$> optField "gender" genderParser)
organizationParser :: Parser Organization
organizationParser = object $ Organization
<$> field "name" string
<*> addressParser
view raw Contacts.hs hosted with ❤ by GitHub

Now, this adressParser must have the type FieldParser Address and it must gobble up the street field and the house_number field.

addressParser :: FieldParser Address
addressParser = Address
<$> field "street" string
<*> field "house_number" integer
view raw Contacts.hs hosted with ❤ by GitHub

There it is. The Address constructor takes a Text and an Int and produces an Address.

Address :: Text -> Int -> Address

This is mapped onto a FieldParser that want’s the two fields street and street-number. The result is a FieldParser Address. The process can be followed in ghci:

> :t  field “street” string
field “street” string :: FieldParser Text
> :t Address <$> field “street” string
Address <$> field “street” string :: FieldParser (Int -> Address)
> :t Address <$> field “street” string <*> field “street_number” integer
Address <$> field “street” string <*> field “street_number” integer
:: FieldParser Address
> :t (object (Address <$> field “street” string <*> field “street_number” integer))
(object (Address <$> field “street” string <*> field “street_number” integer))
:: Parser Address
> parse (object (Address <$> field “street” string <*> field “street_number” integer)) “{street: ‘North La Brea Avenue’, street_number: 709}”
Right (Address “North La Brea Avenue” 709)

We can now build and run our finished parser:

> contacts-parser input.yaml
Contacts [Person “Coco Lenoix” (Address “Havenhurst Drive” 1612) Unassigned,Person “Diane Selwyn” (Address “Griffith Park Boulevard” 2900) Female,Person “Adam Kersher” (Address “Spring Street” 634) Male] [Organization “StudioCanal” (Address “Place du Spectacle” 1),Organization “ABC Studios” (Address “West 66th Street” 77)]

For now, this concludes this short series of posts on Data.Yaml.Combinators. I hope they will prove useful to some of my fellow Haskellers. I know it was useful for me to write them as it forced me to understand how the different parts work together.

If you have suggestions for how to follow this up, and if I have the skills necessary to implement those same suggestions, I may consider more installments in the series.

Series Navigation<< Parsing with Data.Yaml.Combinators, part 2

Leave a Reply

Your email address will not be published.