moonknob

precedence · for moonbit

Two settings.
One answer.

A function that takes a settings record often takes a few flat arguments beside it. This decides which of the two wins, and what to say when they disagree.

moon add moonbitstack/moonknob

01

The order

The more specific a setting, the later it applies.

i

Library preset

What the function does when nobody says otherwise, written once beside it.

ii

The record

What the caller passes as the settings record, usually one built from that preset.

iii

This call's argument

The one field this call needs different. It is the most specific, so it wins.

An argument beats the record unless the function says wins=Base, which is for a record that is an authority rather than a default: the header a signature is about to be made under, the limits a server enforces.

Nothing is overridden in silence. pick keeps the name of every setting that disagreed, and settle hands those names to the clash policy on the way out. The same value twice is one answer, not a disagreement.

02

When they disagree

Four answers. Each function documents the one it defaults to.

ignore

Take what wins chose and say nothing about it.

what most libraries do
warn

Take it, and pass one line to a function the caller supplied: its logger.

the library itself never prints
panic

Abort. Nothing after it runs; the message names the settings.

a mistake in the program
handle

Pass the names, both sides and the merge; take back what the caller decides.

a rule only the caller knows
03

Use it

A record, its flat twin, and one call.

Start with the record

The settings a function has anyway. The preset belongs in the default of the record parameter, so a caller who passes nothing gets it.

struct Options {
  timeout : Int32
  retries : Int32
}

Lay the arguments over it

One pick per setting worth overriding, then settle. wins and clash are the two knobs; the default is in the signature, where a reader finds it.

fn connect(
  options~ : Options = { timeout: 30, retries: 3 },
  timeout? : Int32,
  wins~ : @moonknob.Wins = Extra,
  clash~ : @moonknob.OnClash[Options] = Ignore,
) -> Options {
  let merge = @moonknob.Merge::new(wins)
  let picked = merge.pick("timeout", options.timeout, timeout)
  let merged = { ..options, timeout: picked }
  merge.settle(clash, options, merged, "connect: options and arguments")
}

Call it, and choose what a clash costs

The argument wins by default. Base turns that round. Warn hands the line to a logger; the line names the function and the settings that disagreed.

let mine = { timeout: 5, retries: 3 }
assert_eq(connect(options=mine, timeout=10).timeout, 10)
assert_eq(connect(options=mine, timeout=10, wins=Base).timeout, 5)

let said : Array[String] = []
let warned = connect(options=mine, timeout=10, clash=Warn(l => said.push(l)))
assert_eq(warned.timeout, 10)
assert_eq(said, [
  "connect: options and arguments: timeout given twice with different values",
])

Two maps, same rule

Headers, labels, environment maps: union merges key by key with the same two knobs, and keeps the first side's order.

let merge = @moonknob.Merge::new(Extra)
let base = { "alg": "none", "typ": "JWT" }
let extra = { "typ": "JOSE", "kid": "k1" }
let heads = merge.union(base, extra)
assert_eq(heads.to_array(), [
  ("alg", "none"),
  ("typ", "JOSE"),
  ("kid", "k1"),
])