oCamlCase
The challenge
Build a playlist with filters and formatters
Easy

A track is a tuple of title, artist, duration in seconds, and genre. Build a library of functions that work with a list of such tracks: compute total duration, filter by genre or artist, extract titles, and format tracks for display.

  • 01Define a genre variant type with Rock, Jazz, Classical, and Pop constructors.
  • 02Create at least 5 track tuples of type string * string * int * genre and collect them in a list.
  • 03Write duration that takes a track and returns its duration in seconds.
  • 04Write total_duration that takes a track list and returns the sum of all durations using recursion.
  • 05Write filter_by_genre and filter_by_artist using List.filter.
  • 06Write track_titles using List.map to extract just the title from each track.
  • 07Write long_tracks that returns only tracks longer than a given threshold in seconds.
  • 08Write format_track that returns a formatted string, for example "Bohemian Rhapsody by Queen (5:54)".

Starter code

playlist.ml
type genre = Rock | Jazz | Classical | Pop

(* A track: (title, artist, duration_sec, genre) *)
let track1 = ("Bohemian Rhapsody", "Queen", 354, Rock)
let track2 = ("So What", "Miles Davis", 562, Jazz)
(* add more tracks... *)

let playlist = [track1; track2 (* ... *)]

let duration track =
  (* TODO: extract the third element *)
  0

let rec total_duration = function
  | [] -> 0
  | track :: rest -> (* TODO *) 0

let filter_by_genre g tracks =
  List.filter (fun _ -> (* TODO *) false) tracks

let track_titles tracks =
  List.map (fun _ -> (* TODO *) "") tracks

let format_track (title, artist, d, _) =
  (* TODO: use Printf.sprintf
     minutes = d / 60, seconds = d mod 60 *)
  ""

Hints

For duration, use a wildcard pattern to destructure the tuple: let duration (_, _, d, _) = d. The same approach works in any function that only needs certain fields.

For filter_by_genre, the lambda needs to extract the fourth element and compare it to g. Notice that filter_by_artist follows the exact same pattern, just extracting the second element instead. This repetition hints that partial application is at work: both functions are specializations of a more general filtering idea.

For format_track, use Printf.sprintf "%s by %s (%d:%02d)". The %02d format pads the seconds with a leading zero when needed.

Notice the pattern

filter_by_genre and filter_by_artist are structurally identical. The only thing that changes is which field is compared and to what. This is the same idea as partial application: parameterize the behavior instead of duplicating it.

+ Show solution try it yourself first
playlist_solution.ml
type genre = Rock | Jazz | Classical | Pop

let track1 = ("Bohemian Rhapsody", "Queen",           354, Rock)
let track2 = ("So What",           "Miles Davis",     562, Jazz)
let track3 = ("Moonlight Sonata",  "Beethoven",       375, Classical)
let track4 = ("Billie Jean",       "Michael Jackson", 294, Pop)
let track5 = ("Comfortably Numb",  "Pink Floyd",      382, Rock)

let playlist = [track1; track2; track3; track4; track5]

let duration (_, _, d, _) = d

let rec total_duration = function
  | [] -> 0
  | track :: rest -> duration track + total_duration rest

let filter_by_genre g tracks =
  List.filter (fun (_, _, _, tg) -> tg = g) tracks

let filter_by_artist artist tracks =
  List.filter (fun (_, a, _, _) -> a = artist) tracks

let track_titles tracks =
  List.map (fun (title, _, _, _) -> title) tracks

let long_tracks min_sec tracks =
  List.filter (fun track -> duration track > min_sec) tracks

let format_track (title, artist, d, _) =
  Printf.sprintf "%s by %s (%d:%02d)"
    title artist (d / 60) (d mod 60)

let print_playlist tracks =
  List.iter (fun t -> print_endline (format_track t)) tracks

let () =
  Printf.printf "Total: %d:%02d\n"
    (total_duration playlist / 60)
    (total_duration playlist mod 60);
  print_endline "Rock tracks:";
  print_playlist (filter_by_genre Rock playlist);
  print_endline "Long tracks (5+ min):";
  print_playlist (long_tracks 300 playlist)

Walkthrough

Tuple destructuring. The wildcard pattern (_, _, d, _) unpacks only what is needed and discards the rest. This is the idiomatic way to access fields in a tuple: no index syntax and no boilerplate.

filter_by_genre vs filter_by_artist. Both are one-liners that differ only in which field they compare. This similarity is intentional. In a real codebase you might abstract them into a single filter_by_field function that accepts a field extractor, but naming them explicitly is clearer here.

format_track. The %02d format specifier pads the value with a leading zero so that 5 minutes and 4 seconds prints as 5:04 instead of 5:4. This is the same as the C printf format, which OCaml inherits.

print_playlist. List.iter applies a function to every element for its side effects. Unlike List.map, it returns unit. Using it here keeps the printing logic separate from the formatting logic.

Going further

Once the basic version works, try these extensions:

  • Write sort_by_duration using List.sort with a custom comparator.
  • Group tracks by genre into a list of (genre * track list) pairs.
  • Write shuffle using List.sort with Random.int as the comparator.