'How to pattern match a "string list * string list list" in Ocaml?

a newbie in Ocaml here!

I have "string list * string list list" looking like this:

val employee : string list * string list list = 
(["Name"; "Salary"; "department"],
["James"; "52 000"; "A1"];
["Sarah"; "50 000"; "A1"];
["Peter"; "76 000"; "C3"];
["Jessica"; "93 000"; "D2"]])

I want to create a function that takes "employee", a list containing various combinations of ["Name"; "Salary"; "department"] and returns a string with all the elements for that attribute. I.e. #employeeInfo (["Name"], employee) should return ["Name"; "James"; "Sarah"; "Peter"; "Jessica"]. However, I am not sure how to pattern match a string list * string list list and I couldn't find anything online.



Solution 1:[1]

It's perhaps helpful to understand how a list in OCaml is built in order to understand how it's pattern-matched. So a quick and dirty list definition for demonstration purposes.

type 'a demo_list = Empty | List of 'a * 'a demo_list

A list is a recursive data type. It is either empty, or a value plus some other list which might either be empty or... well, the same. That can go on as long as we want it to.

What that means for pattern-matching a list is that you can match either an empty list, or the first element in a list and the rest.

match some_list with
| [] -> ...
| first::rest -> ...

Now, call first and rest whatever you want. x and xs are really common. There's nothing special about those names. They're just names.

When pattern-matching on a list, the [] pattern is a natural exit point for recursion. Typically with first::rest or x::xs we're doing something with that first value, and then recursively calling a function on rest or xs.

We can pattern match on more than the first and rest, but we can't pattern-match all elements in a list, because we can't know how long a list will be at compile-time as it can be of any length.

In your problem, that means pattern-matching on your employee tuples is going to be impossible, since we aren't interested in one at a time, but in parallel components of each.

But we can pattern match on the headers list.

I don't want to just give you code, but here's some ASCII diagrams. We'll start out with the type of table you've essentially described.

+-----+-----+-----+
| "A" | "B" | "C" |
+-----+-----+-----+
| "a" | "s" | "7" |
| "g" | "y" | "9" |
| "x" | "d" | "1" |
+-----+-----+-----+

If we consider the first row the headers, and it's a list. We run a lookup function that takes the header name we're looking for and the data table.

lookup "C" data

It matches on that first header list.

         +-----+          +-----+-----+
first -> | "A" |  rest -> | "B" | "C" |
         +-----+          +-----+-----+

But first does not equal "C" so we call the function recursively with rest and with mapping List.tl to the data.

         +-----+          +-----+
first -> | "B" |  rest -> | "C" |
         +-----+          +-----+

Data:

+-----+-----+
| "s" | "7" |
| "y" | "9" |
| "d" | "1" |
+-----+-----+

Still not what we're looking for because "B" is not equal to "C". Let's try this again.

         +-----+          +-+
first -> | "C" |  rest -> | |
         +-----+          +-+

Data:

+-----+
| "7" |
| "9" |
| "1" |
+-----+

Now "C" is equal to "C" so we can return the result of mapping List.hd to the data and we'd get: ["7"; "9"; "1"].

Of course, if that last iteration hadn't worked, and we'd called lookup with an empty headers list, we'd know the field we're looking for isn't there, and a reasonable course of action would either be to raise an exception or refactor your function to use the option type and return None.

Solution 2:[2]

A pattern in OCaml generally looks like the value it matches. A pattern that matches pairs (x, y) looks like (p, q). So if you match your value employee against the pattern (tags, values), tags will match ["Name"; "Salary"; "department"] and values will match the list of lists.

You can combine patterns into larger ones. So the pattern (tag :: tags, values) matches a pair whose first element is a list. If you match this pattern against your value employee, tag will match "Name", tags will match ["Salary; "department"] and values will match the list of lists.

You can continue in this way to build up whatever pattern you like. However, there's not going to be a single pattern match that calculates the function you want. It seems to me you will need some code that maps between the position of the tag and the pieces of the list of lists.

Solution 3:[3]

Here is a solution for your problem

let rec position name = function
  | [] -> failwith "No column with that name"
  | hd::_ when hd = name -> 0
  | _::tail -> 1 + position name tail

let employee_info (headers, fields) attribute =
  let pos = position attribute headers in
  List.map (fun l -> List.nth l pos) fields

To be used like

utop # employee_info employees "Name"
- : string list = ["James"; "Sarah"; "Peter"; "Jessica"]
utop #

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Chris
Solution 2 Jeffrey Scofield
Solution 3 BlackBeans