Morel Cookbook

Problem

I want to model a domain where events of different shapes share a life — an order, a payment against it, a refund. In SQL I'd have three tables and hope the foreign keys hold. In Morel I can have one list of events and a type system that makes "payment for an order that doesn't exist" unrepresentable. Plus I want to check a business rule — refunds never exceed payments — and have the answer land as a table I can look at.

Setup

datatype event =
    Order of { id : int, amount : real }
  | Payment of { orderId : int, amount : real }
  | Refund of { orderId : int, amount : real };

val events = [
  Order { id = 1, amount = 222.00 },
  Order { id = 2, amount = 840.00 },
  Payment { orderId = 1, amount = 222.00 },
  Payment { orderId = 2, amount = 500.00 },
  Refund { orderId = 2, amount = 100.00 },
  Order { id = 3, amount = 90.00 },
  Payment { orderId = 3, amount = 90.00 }
];

Example

Three projections — kind, amount, and a group-sum — as one pipeline:

fun kind (Order _)   = "order"
  | kind (Payment _) = "payment"
  | kind (Refund _)  = "refund";

fun amount (Order { id = _, amount = a })        = a
  | amount (Payment { orderId = _, amount = a }) = a
  | amount (Refund { orderId = _, amount = a })  = a;

from e in events
  yield { kind = kind e, amount = amount e }
  group kind
    compute { total = sum over amount };
val it =
  [{kind="order",total=1152},{kind="payment",total=812},
   {kind="refund",total=100}] : {kind:string, total:real} list

What's happening

datatype event = Order of … | Payment of … | Refund of … is an algebraic data type — a sum of product types. Each case is a constructor with its own payload, and the compiler enforces exhaustiveness when you pattern-match. Add a fourth event kind and every fun matching event must gain a clause for it. The shape is the schema, and the schema is the code.

Two helpers — kind and amount — turn the typed events back into uniform fields the query operators understand. That's the bridge: define the data as precisely as you can, then project to the flat shape your aggregates need. The query reads like SQL, the type definition reads like Haskell, and they share a file.

Small gotcha, still there in 0.9. Partial record patterns inside a constructor payload — Order { id, ... } — hit the same bug as recipe 02. Bind every field with =, using a wildcard where you don't care (id = _, amount = a).

Variations

The business invariant as a query. Per order ID, sum payments and refunds and assert one doesn't exceed the other. The ok column is the test — any false is a bug:

fun orderId (Order { id = i, amount = _ })        = i
  | orderId (Payment { orderId = i, amount = _ }) = i
  | orderId (Refund { orderId = i, amount = _ })  = i;

fun isPayment (Payment _) = true | isPayment _ = false;
fun isRefund  (Refund _)  = true | isRefund  _ = false;

fun paidFor id =
  List.foldl (op +) 0.0
    (from e in events where isPayment e andalso orderId e = id yield amount e);

fun refundedFor id =
  List.foldl (op +) 0.0
    (from e in events where isRefund e andalso orderId e = id yield amount e);

val ids = from e in events group orderId e;

from id in ids
  yield { id, paid = paidFor id, refunded = refundedFor id,
          ok = refundedFor id <= paidFor id };
val it =
  [{id=1,ok=true,paid=222,refunded=0},{id=2,ok=true,paid=500,refunded=100},
   {id=3,ok=true,paid=90,refunded=0}]
  : {id:int, ok:bool, paid:real, refunded:real} list

This is what testing looks like in Morel: queries and assertions share an evaluator. No mocks, no separate harness. Write the invariant, render it as a table, read the ok column.

See also