Morel Cookbook

Problem

A big analysis is six small queries in a trench coat. I want three named pieces — "filter to bulk orders", "filter to a region", "sum the quantities" — and I want to chain them without copy-pasting the predicates.

Setup

A type alias up front so the function signatures are crisp. Morel's inference will otherwise flag "unresolved flex record" when a query function's argument type isn't pinned down.

type orderRow = { id : int, quantity : int, region : string };

val orders : orderRow list = [
  { id = 1, quantity = 12, region = "north" },
  { id = 2, quantity = 30, region = "south" },
  { id = 3, quantity = 8,  region = "north" },
  { id = 4, quantity = 50, region = "west"  },
  { id = 5, quantity = 6,  region = "east"  },
  { id = 6, quantity = 20, region = "south" }
];

Example

Three functions, each a small query. Each takes and returns a collection, which is what makes them composable:

fun bulk (threshold : int) (xs : orderRow list) : orderRow list =
  from r in xs where r.quantity >= threshold yield r;

fun inRegion (region : string) (xs : orderRow list) : orderRow list =
  from r in xs where r.region = region yield r;

fun totalQty (xs : orderRow list) : int =
  List.foldl (op +) 0 (from r in xs yield r.quantity);

bulk 20 (inRegion "west" orders);
val it = [{id=4,quantity=50,region="west"}]
  : {id:int, quantity:int, region:string} list

What's happening

Each function takes a list and returns a list, so the output of one can be the input of the next. bulk 20 (inRegion "west" orders) is read inside-out: filter to the west, then keep the bulk ones. That's the same left-to-right intuition as from r in orders where r.region = "west" andalso r.quantity >= 20 yield r, but split into named pieces each of which can be tested and reused.

The type signatures are load-bearing. bulk : int -> orderRow list -> orderRow list says exactly what goes in and what comes out. When you feed a string list in, the compiler says so before you even run the query. That's the composition safety you don't get from stringly-typed SQL fragments.

One wrinkle for 0.8. If you leave the types off — just fun bulk threshold xs = … — Morel emits an "unresolved flex record" error because it can't infer what shape of records the query expects. Row polymorphism isn't in 0.8's inference. The workaround is the annotation above: define a type alias for your row and put it on every query function's parameter. A little ceremony, a lot of readability.

Variations

Compose three deep. Read the argument stack as a pipeline: filter south, then bulk, then sum:

totalQty (bulk 20 (inRegion "south" orders));

You can also wire the same pieces in with the from-query equivalence — one query with two filters — and get the same answer. The point isn't one form is faster; it's that you pick whichever reads better in the place you're writing.

See also