Morel Cookbook

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