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 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, %

OCaml: Simple List of Tags

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 […].

OCaml: Wrapping in Options for Nullability

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:

  1. Exclude
  2. 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]

OCaml: Approach in Solving Unique

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));;

OCaml: Unique Pattern Matching in Separate File

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
    }
];;

OCaml: Song Data Structure in Separate File

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:

  1. Gather tags, also unwrap option.

  2. Flatten the tags.

  3. Select distinct from list of tags.

  4. 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, and
  • unwrap

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]

OCaml: Finally Oneliner


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]

OCaml: Function Composition

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