Article Series

This article series discuss more than 30 different programming languages. Please read overview before you read any of the details.

Playing with Records Related Articles.

Where to Discuss?

Local Group

Preface

Goal: A practical case to collect unique record fields using ReasonML.

Reference Reading

Source Examples

You can obtain source examples here:


Common Use Case

Task: Get the unique tag string

Please read overview for more detail.

Prepopulated Data

Songs and Poetry

module Songs = {
  type tl_tags  = list(string)
  type tl_song  = { title: string, ltags: option(tl_tags) }
  type tl_songs = list(tl_song) 

  let lsongs: tl_songs = [
    { title : "Cantaloupe Island",
      ltags : Some(["60s", "jazz"])
    },
    { title : "Let it Be",
      ltags : Some(["60s", "rock"])
    },
    { title : "Knockin' on Heaven's Door",
      ltags : Some(["70s", "rock"])
    },
    { title : "Emotion",
      ltags : Some(["70s", "pop"])
    },
    { title : "The River",
      ltags : None
    }
  ]
}

ReasonML Solution

The Answer

There might be many ways to do things in reasonML. One of them is this oneliner as below:

L.map (Songs.lsongs, (lsong) => 
    O.getWithDefault(lsong.Songs.ltags, [])
) |> S.flatten 
  |> Tags.unique 
  |> Array.of_list
  |> Js.log

That simple, and that is easy to be read. In fact, after using Haskell and Purescript, ReasonML is an easy language to adapt with.

Enough with introduction, at this point we should go straight to coding.

Environment

The whole source placed at /my-first-app/src folder. Then you can compile them at once:

$ cd ~/Documents/songs/reasonml/my-first-app
$ bsb -make-world -w
Dependency Finished
>>>> Start compiling 
bsb: [3/3] src/T07_tags-MyFirstApp.cmj
>>>> Finish compiling 1118 mseconds

ReasonML: bucklescript make world watch

The compilation is also happened very fast.


1: Data Type

Before building a complex records, I begin with simple datatype.

Array or List?

You can use either List or Array in ReasonML. It is more comfortable to work with List in ReasonML, but Array looks better to be dumped in JS.log. Moreover, the compiled source code in javascript, also looks better in Array.

let tags: list(string) = ["rock", "jazz", "rock", "pop", "pop"]

Js.log(tags |> Array.of_list)

With the result as below array (not list):

$ node src/T01_tags.bs.js
[ 'rock', 'jazz', 'rock', 'pop', 'pop' ]

ReasonML: List to Array

Type Alias

We can use type alias to define a record:

type tags = list(string)
type song = { title: string, tags: option(tags) }

let song: song = {
  title : "Cantaloupe Island",
  tags  : Some(["60s", "jazz"])
}

let {title, tags: mytags} = song

mytags->Js.log

With the result as below list (not array):

$ node src/T02_song.bs
{ hd: '60s', tl: { hd: 'jazz', tl: 0 } }

Wrapping in Options for Nullability

I also add option so tags can accept None value, instead of just empty list.

type song = { title: string, tags: option(tags) }

let song: song = {
  title : "Cantaloupe Island",
  tags  : Some(["60s", "jazz"])
}

This is why we have this Some […] for each tags. We need, a not too simple, case example.

Converting List to Array

That is clear that, we are going to work with List. But since we want to express the result in array, I would like to introduce of using both types, in one reasonml script.

type tl_tags = list(string)
type tl_song = { title: string, ltags: option(tl_tags) }

type ta_tags = array(string)
type ta_song = { title: string, atags: option(ta_tags) }

let lsong: tl_song = {
  title : "Cantaloupe Island",
  ltags : Some(["60s", "jazz"])
}

let lsongToArray = (lsong) => {
  let {title: mytitle, ltags: mytags} = lsong;
  let ltags = Belt.Option.getWithDefault(mytags, [])
  let asong: ta_song = {
    title : mytitle,
    atags : Some(ltags |> Array.of_list)
  }
  asong
}

Js.log(lsongToArray(lsong))

With the result as below records (in array):

$ node src/T03_map.bs.js
{ title: 'Cantaloupe Island', atags: [ '60s', 'jazz' ] }

Fat Arrow

Javascript users should feel like home with this Fat Arrow.

let lsongToArray = (lsong) => {
  
}

Deconstruction

Just like ecmascript, this reasonml also has deconstruction.

  let {title: mytitle, ltags: mytags} = lsong;
  let ltags = Belt.Option.getWithDefault(mytags, [])

Just beware of namespace between modules as I will explain later.


2: Data Structure

Meet The Songs Record

From just karaoke, we can go pro with recording studio.

From simple data, we can build a structure to solve our task.

Record in ReasonML

Consider to make a new module, named MySongs.re. We are working with list, for this module.

module Songs = {
  type tl_tags  = list(string)
  type tl_song  = { title: string, ltags: option(tl_tags) }
  type tl_songs = list(tl_song) 

  let lsongs: tl_songs = [
    { title : "Cantaloupe Island",
      ltags : Some(["60s", "jazz"])
    },
    { title : "Let it Be",
      ltags : Some(["60s", "rock"])
    },
    { title : "Knockin' on Heaven's Door",
      ltags : Some(["70s", "rock"])
    },
    { title : "Emotion",
      ltags : Some(["70s", "pop"])
    },
    { title : "The River",
      ltags : None
    }
  ]
}

ReasonML: Song Data Structure in Module

Using Songs Module

At this point we can utilize the module, as shown in this very simple script below:

open MySongs

Js.log(Songs.lsongs |> Array.of_list)

With the result as below records (in array, then in list):

$ node src/T04_songs.bs.js
[
  { title: 'Cantaloupe Island', ltags: { hd: '60s', tl: [Object] } },
  { title: 'Let it Be', ltags: { hd: '60s', tl: [Object] } },
  {
    title: "Knockin' on Heaven's Door",
    ltags: { hd: '70s', tl: [Object] }
  },
  { title: 'Emotion', ltags: { hd: '70s', tl: [Object] } },
  { title: 'The River', ltags: undefined }
]

ReasonML: Using The Module

Simplified the Output

In short: Make the code more complex!

As above preparation, we can applied with many records.

module L = Belt.List
module O = Belt.Option
open MySongs

type ta_tags  = array(string)
type ta_song  = { title: string, atags: option(ta_tags) }
type ta_songs = array(ta_song)

let lsongs = Songs.lsongs

let lsongToArray = (lsong) => {
  let {title: mytitle, Songs.ltags: mytags} = lsong;
  let ltags = O.getWithDefault(mytags, [])
  let asong: ta_song = {
    title : mytitle,
    atags : Some(ltags |> Array.of_list)
  }
  asong
}

let asongs: ta_songs = L.map
  (lsongs, lsongToArray) |> Array.of_list

Js.log(asongs)

With the result as below records (in array, then in array):

$ node src/T05_songs.bs.js
[
  { title: 'Cantaloupe Island', atags: [ '60s', 'jazz' ] },
  { title: 'Let it Be', atags: [ '60s', 'rock' ] },
  { title: "Knockin' on Heaven's Door", atags: [ '70s', 'rock' ] },
  { title: 'Emotion', atags: [ '70s', 'pop' ] },
  { title: 'The River', atags: [] }
]

Field Namespace

Instead of using lsong.tag directly, field of record must using module name. So we have tao access with lsong.Songs.ltags.

open MySongs

let lsongs = Songs.lsongs

let lsongToArray = (lsong) => {
  let {title: mytitle, Songs.ltags: mytags} = lsong;
  
}

This feels strange for beginner like me. But it makes sense later.

Iteration Using Map

The record iteration processed by using map:

module L = Belt.List
let asongs: ta_songs = L.map
  (lsongs, lsongToArray) |> Array.of_list

3: Approach in Solving Unique

Consider going back to simple data. We need to discuss about solving unique list.

x:xs Pattern Matching

As a beginner, I cannot find any reference on, how to solve unique list in reasonml. So I lookup 99 reasonml, and find switch.

Then I combine with x:xs pattern matching from Haskell. The original custom made code from haskell shown as below:

exclude :: String -> ([String] -> [String])
exclude tag = filter((/=) tag)

unique :: [String] -> [String]
unique [] = []
unique (tag:tags) = tag:unique(exclude tag tags)

tags :: [String]
tags = ["rock", "jazz", "rock", "pop", "pop"]

main = print (unique tags)

With the result of

["rock","jazz","pop"]

This x:xs pattern is made of two functions:

  1. Exclude
  2. Unique

Switch for Exclude

We are using recursive function with [x, ...xs] pattern matching. This is what I have got:

let tags: list(string) = ["rock", "jazz", "rock", "pop", "pop"]

let rec exclude = (_val, lst) =>
  switch lst {
  | [] => []
  | [x] when x === _val => []
  | [x] when x !== _val => [x]
  | [x, ...xs] when x === _val => exclude(_val, [...xs])
  | [x, ...xs] when x !== _val => [x, ...exclude(_val, [...xs])]
  | [x, ...xs] => [x, ...xs] // surpress warning
};

Js.log(tags |> exclude("rock") |> Array.of_list)

With the result similar as below array:

 node src/T06_tags.bs.js
[ 'jazz', 'pop', 'pop' ]

Filter for Exclude

It is easier to rewrite those switch above using List.filter.

let tags: list(string) = ["rock", "jazz", "rock", "pop", "pop"]
let exclude = (_val, lst) => List.filter( x => (x!==_val), lst );

Js.log(tags |> exclude("rock") |> Array.of_list)

With the result exactly like above.

Switch for Unique

Then we are going to put this exclude function, inside another recursive pattern matching.

It will be a good idea to put this complexity in separate module. This is what I have got so far:

module Tags = {
  let rec exclude = (_val, lst) =>
    
  };

  let rec unique = (lst) =>
    switch lst {
    | [] => []
    | [x] => [x]
    | [x, ...xs] => [x, ...unique(exclude(x, [...xs]))]
  };
}

ReasonML: Unique Pattern Matching in Module

Using Tags Module

At this point we can utilize the module, as shown in this very simple script below:

open MyTags

let tags: list(string) =
  ["rock", "jazz", "rock", "pop", "pop"]

Js.log(tags |> Tags.unique |> Array.of_list)

With the result as below array:

$ node src/T07_tags.bs.js
[ 'rock', 'jazz', 'pop' ]

ReasonML: Using Tags Module


4: Extracting Records

We are going to flatten the array. The array taken from list in each record.

Brief Overview

Four steps:

  1. Gather tags, also unwrap option.

  2. Flatten the tags.

  3. Select distinct from array of tags.

  4. Simplified using oneliner.

Gather Tags

We are using our previous map function. But the map here is simpler, since we can just let the data in list form anyway.

module L = Belt.List
open MySongs

let lsongs = Songs.lsongs

let lsongToTags = (lsong) => {
  let {Songs.ltags: mytags} = lsong;
  Belt.Option.getWithDefault(mytags, [])
}

let lo_tags = L.map (lsongs, lsongToTags)

Js.log(lo_tags |> Array.of_list)

With the result as below list:

$ node src/T09_songs.bs.js
[
  { hd: '60s', tl: { hd: 'jazz', tl: 0 } },
  { hd: '60s', tl: { hd: 'rock', tl: 0 } },
  { hd: '70s', tl: { hd: 'rock', tl: 0 } },
  { hd: '70s', tl: { hd: 'pop', tl: 0 } },
  0
]

We are going to convert to array, after flatten.

Flatten Tags

The flatten proces is just using flatten function.

module L = Belt.List
module O = Belt.Option
module S = StdLabels.List
open MySongs

let lsongToTags = (lsong) => {
  O.getWithDefault(lsong.Songs.ltags, [])
}

let l_tags = L.map (Songs.lsongs, lsongToTags) 

Js.log(l_tags |> S.flatten |> Array.of_list)

With the result as below array:

$ node src/T10_songs.bs.js
[
  '60s', 'jazz',
  '60s', 'rock',
  '70s', 'rock',
  '70s', 'pop'
]

Accessing Field from Record Using Module

Just beware the syntax, of the lsong.Songs.ltags, instead of lsong.ltags.

et lsongToTags = (lsong) => {
  O.getWithDefault(lsong.Songs.ltags, [])
}

To make it sense, just compare with previous code:

let lsongToTags = (lsong) => {
  let {Songs.ltags: mytags} = lsong;
  Belt.Option.getWithDefault(mytags, [])
}

Distinct Tags

Now we can directly apply our custom made unique function.

module L = Belt.List
module O = Belt.Option
module S = StdLabels.List

open MySongs
open MyTags

let l_tags = L.map (Songs.lsongs, (lsong) => 
    O.getWithDefault(lsong.Songs.ltags, [])
  ) 

Js.log(l_tags |> S.flatten |> Tags.unique |> Array.of_list)

With the result as below array:

 node src/T11_distinct.bs.js
[ '60s', 'jazz', 'rock', '70s', 'pop' ]

Voila… Almost finished.

Finally Oneliner

However, we can use this ReasonML fashion in writing code, to make it even cooler.

module L = Belt.List
module O = Belt.Option
module S = StdLabels.List

open MySongs
open MyTags

L.map (Songs.lsongs, (lsong) => 
    O.getWithDefault(lsong.Songs.ltags, [])
) |> S.flatten 
  |> Tags.unique 
  |> Array.of_list
  |> Js.log

With the result as below array:

 node src/T12_oneliner.bs.js
[ '60s', 'jazz', 'rock', '70s', 'pop' ]

ReasonML: Finally Oneliner

Finally Oneliner

However, we can use this ReasonML fashion in writing code, to make it even cooler.

module L = Belt.List
module O = Belt.Option
module S = StdLabels.List

open MySongs
open MyTags

L.map (Songs.lsongs, (lsong) => 
    O.getWithDefault(lsong.Songs.ltags, [])
) |> S.flatten 
  |> Tags.unique 
  |> Array.of_list
  |> Js.log

With the result as below array:

 node src/T12_oneliner.bs.js
[ '60s', 'jazz', 'rock', '70s', 'pop' ]

![ReasonML: Finally Oneliner]


5: More About Function

Dive Deeper with Functional Programming

Function Composition

We can rewrite the code above with function composition. This article below show a very nice trick, that you can use of in ReasonML

The complete code is as below:

module L = Belt.List
module O = Belt.Option
module S = StdLabels.List

open MySongs
open MyTags

let (>>) = (f, g, x) => g(f(x));
let (<<) = (f, g, x) => f(g(x));

L.map (Songs.lsongs, (lsong) => 
    [] |> (O.getWithDefault @@ lsong.Songs.ltags)
) |> (S.flatten >> Tags.unique >> Array.of_list)
  |> Js.log

With the result as below array:

❯ node src/T13_composition.bs.js
[ '60s', 'jazz', 'rock', '70s', 'pop' ]

ReasonML: Function Composition

Function composition is pretty common in functional world. We can make it in ReasonML with only two lines of code.

let (>>) = (f, g, x) => g(f(x));
let (<<) = (f, g, x) => f(g(x));

This is the end of our ReasonML journey in this article. We shall meet again in other article.


What is Next 🤔?

Consider continue reading [ OCaml - Playing with Records - Part One ].