Morel Cookbook

Problem

Some of my rows have a missing value — NONE in Morel, effectively NULL in SQL, NaN in pandas. I want to count how many are missing, drop them, default them, or unwrap them safely.

Setup

val orders = [
  { id = 1,  quantity = 12, status = SOME "fulfilled" },
  { id = 2,  quantity = 30, status = SOME "fulfilled" },
  { id = 3,  quantity = 8,  status = SOME "cancelled" },
  { id = 5,  quantity = 6,  status = NONE },
  { id = 6,  quantity = 20, status = SOME "fulfilled" },
  { id = 11, quantity = 16, status = NONE }
];

Example

Keep the rows where status is known, and unwrap it so downstream steps see a plain string:

from ord in orders
  where isSome ord.status
  yield { ord.id, status = valOf ord.status };
val it =
  [{id=1,status="fulfilled"},{id=2,status="fulfilled"},
   {id=3,status="cancelled"},{id=6,status="fulfilled"}]
  : {id:int, status:string} list

What's happening

status has type string option, which Morel reads as "a string, or nothing". That's the whole missing-value story — there is no null, and there is no NaN. Either the value is SOME "fulfilled" or it's NONE, and the type system makes you handle both. That's the payoff over SQL's tri-valued logic, where status = NULL silently evaluates to "unknown" and ghosts through your WHERE clauses.

isSome asks "is this SOME _?", valOf pulls the value out, and together they do the moral equivalent of "filter nulls, then unwrap." If you reach for valOf on a NONE, Morel raises Option at runtime — that's why the isSome guard goes first. Pattern matching (see recipe 12) is a tidier spelling of the same thing when you want to handle both branches in one pass.

Two patterns worth remembering. getOpt (x, default) unwraps with a fallback in one call — the Morel equivalent of COALESCE. And for an "ignore nulls, just use the real ones" pipeline, a where isSome … step up front narrows the type so the rest of the query can stop worrying.

Variations

Count the missing rows — a group {} with no key produces one row, and the compute clause does the counting:

from ord in orders
  where not (isSome ord.status)
  group {} compute { missing = count over () };

Default the missing ones rather than drop them — getOpt takes the option and a fallback, returns a plain string:

from ord in orders
  yield { ord.id, status = getOpt (ord.status, "unknown") };

See also