Preface
Goal: A practical case to collect unique record fields using OCaml.
After using reasonml
for a while, it make sense to step into ocaml
.
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
type tl_tags = string list;;
type tl_song = {
title : string; tags : tl_tags option;
};;
type tl_songs = tl_song list;;
let songs: tl_songs = [
{ title = "Cantaloupe Island";
tags = Some ["60s"; "jazz"]
};
{ title = "Let It Be";
tags = Some ["60s"; "rock"]
};
{ title = "Knockin' on Heaven's Door";
tags = Some ["70s"; "rock"]
};
{ title = "Emotion";
tags = Some ["70s"; "pop"]
};
{ title = "The River";
tags = None
}
];;
OCaml Solution
The Answer
There might be many ways to do things in OCaml
.
One of them is this oneliner as below:
let () = print_endline (
join_str (unique (flatten (all_tags songs)))
)
Of course there are hidden details beneath each functions. I will discuss about this step by step.
Enough with introduction, at this point we should go straight to coding.
Environment
I compile using ocaml
directly without dune
or any opam
.
$ cd ~/Documents/songs/ocaml
$ ocaml 10-songs-unique.ml
[60s, jazz, rock, 70s, pop]
1: Data Type
Before building a complex records, I begin with simple datatype.
Using List.
There are a some useful data type in ocaml
.
List
is comfortable to work with.
open Printf;;
let tags : string list =
["rock"; "jazz"; "rock"; "pop"; "pop"];;
let () = List.iter (printf "%s, ") tags;;
With the result as below list
:
❯ ocaml 01-tags.ml
rock, jazz, rock, pop, pop, %
This output doesn’t even end with \n
endline.
Custom Pretty Print
Since we need nice output through put this article.
I create a helper join_str
function,
to make a custom pretty print.
let tags : string list =
["rock"; "jazz"; "rock"; "pop"; "pop"];;
let join_str : string =
"[" ^ (String.concat ", " tags) ^ "]";;
let () = print_endline join_str;;
With the result as below list:
$ ocaml 02-assign.ml
[rock, jazz, rock, pop, pop]
Type Alias
We can use user defined type to define a record:
type tl_tags = string list;;
type tl_song = { title : string; tags : tl_tags; };;
let song : tl_song = {
title = "Cantaloupe Island";
tags = ["60s"; "jazz"]
};;
let join_str my_tags : string =
"[" ^ (String.concat ", " my_tags) ^ "]";;
let () = print_endline (join_str song.tags);;
With the result as below list:
$ ocaml 03-type.ml
[60s, jazz]
Wrapping in Options for Nullability
I also add option
so tags
can accept None
value,
instead of just empty list.
tags : tl_tags option;
The complete code is as below:
type tl_tags = string list;;
type tl_song = {
title : string;
tags : tl_tags option;
};;
let song : tl_song = {
title = "Cantaloupe Island";
tags = Some ["60s"; "jazz"]
};;
let unwrap my_option : tl_tags =
match my_option with
| Some x -> x
| None -> [];;
let join_str_r my_record : string =
"[" ^ (String.concat ","
(unwrap my_record.tags)) ^ "]\n";;
let () = print_string (join_str_r song);;
With the result as below list:
$ ocaml 04-type-option.ml
[60s,jazz]
We need, a not too simple, case example.
This is why we have this Some […]
.
For unwrapping
, we can utilize this code below:
let unwrap my_option : tl_tags =
match my_option with
| Some x -> x
| None -> [];;
2: Approach in Solving Unique
We need to discuss about solving unique list, before using it later in our final code.
Copy Paste
There is a code from rosetta code website. The code below is working well. I know because I have tested it.
let uniq lst =
let unique_set = Hashtbl.create (List.length lst) in
List.iter (fun x -> Hashtbl.replace unique_set x ()) lst;
Hashtbl.fold (fun x () xs -> x :: xs) unique_set []
let _ =
uniq [1;2;3;2;3;4]
As beginner, I avoid to many library, and I use simple algorithm instead. You can utilize this code for your daily use, but I decide to use my own custom code for this session.
head::tail Pattern Matching
I cannot find any other reference on,
how to solve unique
list in ocaml
.
So I borrow the algorithm from 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
Filter for Exclude
It is easier to rewrite those haskell
above using List.filter
.
let exclude my_val my_tags : string list =
List.filter (
fun tag -> (tag <> my_val)
) my_tags;;
The complete code is as below:
let tags : string list =
["rock"; "jazz"; "rock"; "pop"; "pop"];;
let join_str my_tags : string =
"[" ^ (String.concat ", " my_tags) ^ "]";;
let exclude my_val my_tags : string list =
List.filter (
fun tag -> (tag <> my_val)
) my_tags;;
let () = print_endline (
join_str (exclude "rock" tags)
);;
With the result as below list:
$ ocaml 05-tags-exclude.ml
[jazz, pop, pop]
Beware of the use of <>
and !=
in ocaml
.
Since the behaviour is different is ocaml
.
Match for Unique
Then we are going to put this exclude
function,
inside another recursive pattern matching.
This is working by accumulating list using @
operator recursively.
let rec unique my_tags : string list =
match my_tags with
| [] -> []
| [last] -> [last]
| (head::tail) -> [head] @
(unique (exclude head tail));;
The complete code is as below:
let tags : string list =
["rock"; "jazz"; "rock"; "pop"; "pop"];;
let join_str my_tags : string =
"[" ^ (String.concat ", " my_tags) ^ "]";;
let exclude my_val my_tags : string list =
List.filter (
fun tag -> (tag <> my_val)
) my_tags;;
let rec unique my_tags : string list =
match my_tags with
| [] -> []
| [last] -> [last]
| (head::tail) -> [head] @
(unique (exclude head tail));;
let () = print_endline (
join_str (unique tags)
);;
With the result as below list:
$ ocaml 06-tags-unique.ml
[rock, jazz, pop]
The Tags File
It will be a good idea to put this complexity in separate file. This is what I have got so far:
let join_str my_tags : string =
"[" ^ (String.concat ", " my_tags) ^ "]";;
let exclude my_val my_tags : string list =
List.filter (
fun tag -> (tag <> my_val)
) my_tags;;
let rec unique my_tags : string list =
match my_tags with
| [] -> []
| [last] -> [last]
| (head::tail) -> [head] @
(unique (exclude head tail));;
Using Tags File
At this point we can rewritethe previuous code, into this code below. With exactly the same result.
#use "mytags.ml";;
let tags : string list =
["rock"; "jazz"; "rock"; "pop"; "pop"];;
let () = print_endline (
join_str (unique tags)
);;
The #use
keyword is different with using module.
This is just simply copy paste other file content into the file.
So the main code looks shorter.
3: 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 OCaml
It is easy to arrange a bunch of record in a list.
Compared to other programming language it is a matter of syntax.
The only adaptation we need is, ocaml
utilize ;
instead of ,
.
type tl_tags = string list;;
type tl_song = {
title : string; tags : tl_tags;
};;
type tl_songs = tl_song list;;
let songs: tl_songs = [
{ title = "Cantaloupe Island";
tags = ["60s"; "jazz"]
};
{ title = "Let It Be";
tags = ["60s"; "rock"]
};
{ title = "Knockin' on Heaven's Door";
tags = ["70s"; "rock"]
};
{ title = "Emotion";
tags = ["70s"; "pop"]
};
{ title = "The River";
tags = []
}
];;
The issue comes when it comes to processing. But it is also easy to adapt for simple structure.
let join_str my_tags : string =
"[" ^ (String.concat ", " my_tags) ^ "]";;
let all_tags my_songs : string list = List.map (
fun my_song -> (join_str my_song.tags)
) my_songs;;
let join_all my_all : string =
String.concat "\n" my_all;;
let () = print_endline (join_all (all_tags songs));;
With the result as below list of list:
$ ocaml 07-songs.ml
[60s, jazz]
[60s, rock]
[70s, rock]
[70s, pop]
[]
The Song File
Consider make this data stucture more complex,
with adding nullability option
:
type tl_tags = string list;;
type tl_song = {
title : string; tags : tl_tags option;
};;
type tl_songs = tl_song list;;
Since we are going to reuse the songs
records multiple times,
consider to make a new file, named mysongs.ml
,
with complete code as below:
type tl_tags = string list;;
type tl_song = {
title : string; tags : tl_tags option;
};;
type tl_songs = tl_song list;;
let songs: tl_songs = [
{ title = "Cantaloupe Island";
tags = Some ["60s"; "jazz"]
};
{ title = "Let It Be";
tags = Some ["60s"; "rock"]
};
{ title = "Knockin' on Heaven's Door";
tags = Some ["70s"; "rock"]
};
{ title = "Emotion";
tags = Some ["70s"; "pop"]
};
{ title = "The River";
tags = None
}
];;
Using Songs File
At this point we can utilize the separate file above, as shown in this very simple script below:
#use "mysongs.ml";;
…
And so on.
4: Extracting Records
We are going to flatten the list
.
Brief Overview
Four steps:
-
Gather
tags
, also unwrapoption
. -
Flatten the
tags
. -
Select
distinct
fromlist
oftags
. -
Simplified using oneliner.
Instead of working directly with records, we can simplified things using mockup.
Flatten Using Mockup
This just basically prepare a simpler data structure.
Just a list
of a list
with option
.
let all_tags_test: string list option list =[
Some ["60s"; "jazz"]; Some ["60s"; "rock"];
Some ["70s"; "rock"]; Some ["70s"; "pop"];
None];;
Now we should be ready with data processing.
Let me introduce flatten
function using fold_left
:
let flatten my_list : string list = List.fold_left (
fun acc element -> acc @ (unwrap element)
) [] my_list;;
And use the function as below:
(flatten all_tags_test)
Complete Mockup
There is nothing new here, we already discuss about the other function:
join_str
, andunwrap
The complete code shown as below:
type tl_tags = string list;;
type tl_song = {
title : string; tags : tl_tags option;
};;
type tl_songs = tl_song list;;
let all_tags_test: string list option list =[
Some ["60s"; "jazz"]; Some ["60s"; "rock"];
Some ["70s"; "rock"]; Some ["70s"; "pop"];
None];;
let join_str my_tags : string =
"[" ^ (String.concat ", " my_tags) ^ "]";;
let unwrap my_option : tl_tags =
match my_option with Some x -> x | None -> [];;
let flatten my_list : string list = List.fold_left (
fun acc element -> acc @ (unwrap element)
) [] my_list;;
let () = print_endline (join_str (flatten all_tags_test))
With the result as below list:
$ ocaml 08-mockup.ml
[60s, jazz, 60s, rock, 70s, rock, 70s, pop]
Gather Tags
We are using mysongs
file.
Since the structure is a little bit,
different we must alter the code a bit.
We need to unwrap
in the first place.
let all_tags my_songs: tl_tags list = List.map (
fun my_song -> (unwrap my_song.tags)
) my_songs;;
And use the call function as:
(all_tags songs)
Flatten Tags
The flatten process is just using flatten
function.
Without unwrap
inside.
let flatten my_list : string list = List.fold_left (
fun acc element -> acc @ element
) [] my_list;;
And use the call function as:
(flatten (all_tags songs))
Complete Flatten Code
The complete code is as below:
#use "mysongs.ml";;
let join_str my_tags : string =
"[" ^ (String.concat ", " my_tags) ^ "]";;
let unwrap my_option : tl_tags =
match my_option with Some x -> x | None -> [];;
let flatten my_list : string list = List.fold_left (
fun acc element -> acc @ element
) [] my_list;;
let all_tags my_songs: tl_tags list = List.map (
fun my_song -> (unwrap my_song.tags)
) my_songs;;
let () = print_endline (
join_str (flatten (all_tags songs))
)
With the result as below list:
$ ocaml 09-songs.ml
[60s, jazz, 60s, rock, 70s, rock, 70s, pop]
Distinct Tags
Now we can directly apply our custom made unique
function.
(unique (flatten (all_tags songs)))
Finally Oneliner
However, we can use this OCaml
fashion in writing code,
to make it even cooler.
let () = print_endline (
join_str (unique (flatten (all_tags songs)))
)
The complete code is as below:
#use "mysongs.ml";;
#use "mytags.ml";;
let unwrap my_option : tl_tags =
match my_option with Some x -> x | None -> [];;
let flatten my_list : string list = List.fold_left (
fun acc element -> acc @ element
) [] my_list;;
let all_tags my_songs: tl_tags list = List.map (
fun my_song -> (unwrap my_song.tags)
) my_songs;;
let () = print_endline (
join_str (unique (flatten (all_tags songs)))
)
With the result as below list
:
$ ocaml 10-songs-unique.ml
[60s, jazz, rock, 70s, pop]
5: Alternative Solution
Of course there are many ways to solve flattening task, using idiomatic functional libraries.
List Library: Filter Concat
We can make above code simpler.
#use "mysongs.ml";;
let join_str my_tags : string =
"[" ^ (String.concat ", " my_tags) ^ "]";;
let unwrap = function
| Some x -> x
| None -> [];;
let all_tags my_songs: string list
= List.concat_map (
fun my_song -> (unwrap my_song.tags)
) my_songs;;
let () = print_endline (
join_str (all_tags songs)
)
With the result as below list
:
$ ocaml 09-filter-concat.ml
[60s, jazz, 60s, rock, 70s, rock, 70s, pop]
List Library: Filter Map
We can make above code even simpler,
using filter_map
, and flatten with concat
later.
#use "mysongs.ml";;
let join_str my_tags : string =
"[" ^ (String.concat ", " my_tags) ^ "]";;
let all_tags my_songs: tl_tags list
= List.filter_map (
fun my_song -> my_song.tags
) my_songs;;
let () = print_endline (
join_str (List.concat (all_tags songs))
)
With the result as below list
:
$ ocaml 09-filter-map.ml
[60s, jazz, 60s, rock, 70s, rock, 70s, pop]
Text Book: Unique Alternative
There is also this unique
algorithm using textbook.
And also in chapter: Terser and Faster Patterns.
Text Book: Exclude Alternative
There is also this exclude
algorithm using textbook, using recursive.
6: More About Function
Dive Deeper with Functional Programming
Standard Pipe
We can rewrite above function with:
let () = print_endline @@ join_str
@@ unique @@ flatten @@ all_tags @@ songs;;
Custom Pipe
Or even write your own operator:
let (|>) v f = f v;;
let () = songs
|> all_tags
|> flatten
|> unique
|> join_str
|> print_endline;;
Function Composition
But if you want a more standarized version,
you can utilized batteries
library.
$ opam install batteries
The following actions will be performed:
∗ install num 1.4 [required by batteries]
∗ install batteries 3.2.0
===== ∗ 2 =====
Do you want to continue? [Y/n] y
Then run this to make sure it loaded.
$ eval `opam config env`
Now we can rewrite the code above with function composition.
let () = songs
|> (unique % flatten % all_tags)
|> (print_endline % join_str);;
The complete code is as below:
#use "topfind";;
#require "batteries";;
open Batteries;;
#use "mysongs.ml";;
#use "mytags.ml";;
let unwrap my_option =
match my_option with | Some x -> x | None -> [];;
let flatten my_list = List.fold_left (
fun acc element -> acc @ element
) [] my_list;;
let all_tags my_songs = List.map (
fun my_song -> unwrap @@ my_song.tags
) my_songs;;
let () = songs
|> (unique % flatten % all_tags)
|> (print_endline % join_str);;
With the result as below list
:
$ ocaml 13-composition.ml
[60s, jazz, rock, 70s, pop]
Function composition is pretty common in functional world. We need to load battery first.
#use "topfind";;
#require "batteries";;
open Batteries;;
What is Next 🤔?
Consider continue reading [ OCaml - Playing with Records - Part Two ].