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
- Recipe 17 — Define a metric once — scalar metrics are a simpler version of this same pattern.
- Recipe 18 — Higher-order functions on data —
mapandfilteras primitive compositional pieces. - Recipe 11 — Join two tables — multi-scan queries are the next step up from composing single-scan ones.