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, unchanged in 0.9. Leave the types off — just fun bulk threshold xs = … — and Morel emits an "unresolved flex record" error,
because row polymorphism isn't in its inference and it can't guess what
shape of record the query expects. The workaround is the annotation
above: define a type alias for your row and put it on every query
function's parameter.
Variations
Compose three deep. Read the argument stack as a pipeline: filter south, then bulk, then sum:
totalQty (bulk 20 (inRegion "south" orders));
The same pieces can be wired up as one from query with two filters,
for the same answer. Neither form is faster; pick whichever reads
better where you're writing it.
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.