Problem
I've caught myself typing the same expression — real ord.quantity * p.price — in four different queries. If I change the formula I have
to change it everywhere. Give me one place to define it.
Setup
val orders = [
{ id = 1, productId = 101, quantity = 12 },
{ id = 2, productId = 204, quantity = 30 },
{ id = 4, productId = 204, quantity = 50 },
{ id = 8, productId = 102, quantity = 25 }
];
val products = [
{ id = 101, price = 18.50 },
{ id = 102, price = 24.00 },
{ id = 204, price = 28.00 }
];
Example
Define the metric once as a function. Its type signature — int * real -> real — is the contract; Morel checks every call site against it:
fun lineTotal (q : int, price : real) : real = real q * price;
from ord in orders, p in products
where ord.productId = p.id andalso lineTotal (ord.quantity, p.price) > 500.0
yield { ord.id, total = lineTotal (ord.quantity, p.price) };
val it = [{id=2,total=840.0},{id=4,total=1400.0},{id=8,total=600.0}]
: {id:int, total:real} list
What's happening
fun lineTotal (q, price) = … is a plain function definition. Morel
infers types from the body, but when the function is a piece of
shared vocabulary it's worth annotating — (q : int, price : real) : real — so that callers get a clear error if they hand it the wrong
shape. The function value lives in the session just like a binding
from val, and every subsequent query can call it.
This is the compositional payoff over SQL. A SQL derived column lives
inside a single query; you can't pass it into the next one without a
CTE or a view. A Morel function is a first-class value, so you write
the definition once and refer to it anywhere — in a where clause,
in a yield, on the right-hand side of a sum over …. When the
formula changes, you change one place and the type checker tells you
about every call site that doesn't line up.
The small ceremony is explicit types. Morel's inference is strong, so you can usually omit them and things still work. But for metrics — numbers you want to mean the same thing on every page — spell the types out. They're documentation the compiler reads.
Variations
Same metric, used as a sort key. Define it once, sort by it:
from ord in orders, p in products
where ord.productId = p.id
yield { ord.id, total = lineTotal (ord.quantity, p.price) }
order DESC total;
Same metric, used as the input to an aggregate. yield promotes it
to a column; sum over aggregates it:
from ord in orders, p in products
where ord.productId = p.id
yield { total = lineTotal (ord.quantity, p.price) }
group {}
compute { revenue = sum over total };
See also
- Recipe 18 — Higher-order functions on data — pass
lineTotalintomapandfoldonce you've got it as a value. - Recipe 19 — Compose queries from small pieces — wrap a
fromexpression in a function and reuse it like a table. - Recipe 10 — Group and aggregate — where named metrics earn their keep on the
overside of an aggregate.