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.logThat 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
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' ]

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.logWith 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
}
]
}
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 }
]

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_list3: 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:
- Exclude
- 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]))]
};
}
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' ]

4: Extracting Records
We are going to flatten the array.
The array taken from list in each record.
Brief Overview
Four steps:
-
Gather
tags, also unwrapoption. -
Flatten the
tags. -
Select
distinctfromarrayoftags. -
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.logWith the result as below array:
❯ node src/T12_oneliner.bs.js
[ '60s', 'jazz', 'rock', '70s', 'pop' ]

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.logWith 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.logWith the result as below array:
❯ node src/T13_composition.bs.js
[ '60s', 'jazz', 'rock', '70s', 'pop' ]
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 ].