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
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.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
}
]
}
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_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:
- 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
distinct
fromarray
oftags
. -
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' ]
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' ]
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 ].