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
The order
The more specific a setting, the later it applies.
Library preset
What the function does when nobody says otherwise, written once beside it.
The record
What the caller passes as the settings record, usually one built from that preset.
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.
When they disagree
Four answers. Each function documents the one it defaults to.
Take what wins chose and say nothing about it.
Take it, and pass one line to a function the caller supplied: its logger.
the library itself never printsAbort. Nothing after it runs; the message names the settings.
a mistake in the programPass the names, both sides and the merge; take back what the caller decides.
a rule only the caller knowsUse 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"),
])