Extensible Record in Haskell using Vinyl for Noob
Tags: haskell, vinyl, record
Published: 2021-02-07 08:14:05

In this tutorial, I'm going to show how to use `vinyl` to create an extensible record within haskell.

This tutorials aim for those who area already familiar with haskell, but cannot figure out how to use `vinyl`.

We are going to use javascript object/typescript interface as comparison.

## Note
- We are using stack lts-17.1

# Setting up the project

- Using `stack new learn-vinyl` to create a new project. 

- Add these library to your `package.yaml`: `vinyl`, `microlens`

- In `Lib.hs`, add this language extensions

```haskell
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeOperators #-} 
```

- Add these imports:

```haskell
import Data.Vinyl
import Data.Vinyl.Syntax ()
import Lens.Micro
import Data.Text
```


# Defining a Record

Let's define a record `Person`:
```haskell
type Person = FieldRec
  '[ "name" ::: Text
   , "age" ::: Int
   , "is_single" ::: Bool
   ]
```

this is equivalent to this typescript version:
```ts
type Person = {
  name: string,
  age: number,
  is_single: boolean
}
```


## Constructing the record

To construct a record of type `Person`, we do this:
```haskell
marcus :: Person
marcus =
      #name =:= "Marcus"
  <+> #age =:= 58
  <+> #is_single =:= False
```

Typescript version:

```ts
let marcus : Person = 
  {
    name: "Marcus", 
    age: 58, 
    is_single: false
  }
```

__Warning__: `vinyl` record field takes order into the matter, so if you put the wrong order, it will causes the type error.

Example:
```Haskell
bad_record :: Person
bad_record =
      #age =:= 58
  <+> #name =:= "Marcus"
  <+> #is_single =:= False
```

This is not good, luckily we have a function to re-order the field for us, by using `rcast`:

```haskell
marcus2 :: Person
marcus2 = rcast $
      #age =:= (58 :: Int)
  <+> #name =:= ("Marcus" :: Text)
  <+> #is_single =:= False

```
One bad thing is that, you have to explicitly add the type for some fields.

## Accessing the field

To access the field of a record, we can use `(^.)` operators in microlens:

```haskell
run :: IO ()
run = do
  print (marcus ^. #name)
```

TS version:
```ts
console.log(marcus.name)
```

## Setting the field of a record

```haskell
run :: IO ()
run = do
  let updated_marcus = marcus & #name .~ "Aurelius"
  print (updated_marcus ^. #name)
```

Typescript version:
```ts
marcus.name = "Aurelius"
console.log(marcus)
```
## Nested Record

### Constructing

To construct a nested record, we add this:
```haskell
type Empire = FieldRec
  '[ "king" ::: Person
   , "country" ::: Text
   ]

rome :: Empire
rome = 
      #king =:= marcus
  <+> #country =:= "Italy"
```

### Accessing nested field

```haskell
run :: IO ()
run = do
  print (rome ^. #king . #name)
```

Typescript version:
```ts
console.log(rome.king.name)
```

### Setting nested field

```haskell
run :: IO ()
run = do
  let updated_rome = rome & #king . #name .~ "Aurelius"
  print (updated_rome ^. #king . #name)
```

```ts
rome.king.name = "Aurelius"
console.log(rome)
```

# Conclusion

`vinyl` is a very powerful library, yet it lags a simple tutorial. That is the reason why I create this tutorial. 

In the future, we will talk about record subset, combining record, and more.

Full source code:
```haskell
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeOperators #-}

module Lib
    ( run
    ) where

import Data.Vinyl
import Data.Vinyl.Syntax ()
import Lens.Micro
import Data.Text

type Person = FieldRec
  '[ "name" ::: Text
   , "age" ::: Int
   , "is_single" ::: Bool
   ]

type Empire = FieldRec
  '[ "king" ::: Person
   , "country" ::: Text
   ]

marcus :: Person
marcus =
      #name =:= "Marcus"
  <+> #age =:= 58
  <+> #is_single =:= False

-- bad_record :: Person
-- bad_record =
--       #age =:= 58
--   <+> #name =:= "Marcus"
--   <+> #is_single =:= False

marcus2 :: Person
marcus2 = rcast $
      #age =:= (58 :: Int)
  <+> #name =:= ("Marcus" :: Text)
  <+> #is_single =:= False

rome :: Empire
rome =
      #king =:= marcus
  <+> #country =:= "Italy"


run :: IO ()
run = do
  print (marcus ^. #name)

  let updated_marcus = marcus & #name .~ "Aurelius"
  print (updated_marcus ^. #name)

  -- Nested
  print (rome ^. #king . #name)

  let updated_rome = rome & #king . #name .~ "Aurelius"
  print (updated_rome ^. #king . #name)

```