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
distinctfromlistoftags. -
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] yThen 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 ].