specify args multiple() times through the combinator on options - #32
Conversation
commit: |
b98cc03 to
239d5be
Compare
239d5be to
5190b1c
Compare
|
robo assisted feedback:
through: param.multiple ? undefined : horizon?.indexConsider a child introduced after a dynamic phase: Before The division of responsibility should be:
That preserves dynamic route boundaries, earliest-claim arbitration, and automatic support for custom readers without requiring every reader to understand a Two other concerns surfaced:
Regression coverage was pushed in
The added malformed-occurrence, source-precedence, default-schema, and numeric-looking-string cases already pass. |
1b7d8f5 to
7cd5232
Compare
5a90f9f to
78067e5
Compare
|
I had a pretty extensive conversation with the robot about how to fix the combinatoric explosion potential with multiple decoders (lazily iterating candidates with a generator helps, but it is still susceptible to the cases where you iterate all the way to the end of the huge list) I think I ended up with a way to both make the decoding system simpler, and also eliminate it as a possibility so that the complexity of decoding is linear with the number of options to decode. The TLDR is that by changing the primitive to be less lossy, we can make the combinators on top of it more powerful, while still conforming to the thin Most of the implementation you have here stays, it's that how decoding is represented internally to be more combinatorially robust. A transcript of the entire conversation is here for context: https://gist.github.com/cowboyd/eb8ce14699d03a1f933f62b928f1d4ce Here is the recommendation we arrived at: Multiple decodingStatusProposed design for repeated CLI parameter representations. MotivationA parameter marked with Those representations must be collected without looking through unresolved route The decoder currently removes failed alternatives and returns only successful decode("abc"); // ["abc"]
decode("123"); // [123, "123"]This loses the correspondence between a candidate and the decoder alternative [1, 2, 3][1, 2, "003"][1, "002", 3] // ...
["001", "002", "003"];With DecisionDecoder candidate positions are stable. A decoder represents a failed decode("abc"); // [Nothing, Just("abc")]
decode("123"); // [Just(123), Just("123")]Candidate slot
Each decoder alternative is applied consistently to the complete collection. decode("001"); // [Just(1), Just("001")]
decode("002"); // [Just(2), Just("002")]the lifted decoder returns: multiple(decode)(["001", "002"]);
// [
// Just([1, 2]),
// Just(["001", "002"]),
// ]If an alternative cannot decode every representation, its lifted slot is multiple(decode)(["abc", "123"]);
// [
// Nothing,
// Just(["abc", "123"]),
// ]The number of aggregate candidates is therefore bounded by the number of decoder Decoder representationThe decoder output becomes a fixed-width array of type Decoder<T = unknown> = (
value: string,
) => readonly Maybe<T>[];Every invocation of a decoder must return the same number of slots in the same The built-in decoders become: const number: Decoder<number> = (value) => {
if (!numeric.test(value)) {
return [Nothing()];
}
let decoded = Number(value);
return Number.isFinite(decoded) ? [Just(decoded)] : [Nothing()];
};
const text: Decoder<string> = (value) => [Just(value)];
const scalar: Decoder = (value) => [
...number(value),
...text(value),
];
const boolean: Decoder<boolean> = (value) => [
value === "true" ? Just(true) : value === "false" ? Just(false) : Nothing(),
];No public decoder identifier or brand is required. Positional correspondence is Decoder liftingThe multiple decoder transposes scalar candidate slots and sequences each slot: function multiple<T>(
decode: Decoder<T>,
): (values: readonly string[]) => readonly Maybe<readonly T[]>[] {
return (values) => {
let rows = values.map(decode);
let width = rows[0]?.length ?? 0;
return Array.from({ length: width }, (_, index) => {
let column = rows.map((row) => row[index]);
if (column.every(present)) {
return Just(column.map((candidate) => candidate.value));
}
return Nothing();
});
};
}The implementation must treat unequal row widths as a violated decoder invariant An empty representation collection is absence, not a candidate array. It does Binding lifecycleCapture is incremental; interpretation happens during finalization. A multiple parameter cannot be decoded, validated, or settled until its current "Closed" refers to the current phase's CLI binding scope, not necessarily the The current phase may capture Phase binding
interface Capture {
readonly values: unknown[];
readonly issues: Issue[];
readonly exists: boolean;
readonly failed: boolean;
}During CLI arbitration:
After the CLI fixed point: for (let param of params) {
let capture = captures.get(param.name);
if (!capture?.exists) {
// Try Env, Values, and finally undefined.
} else if (capture.failed) {
// Return all accumulated capture issues.
} else if (param.multiple) {
// Decode the complete representation collection.
} else {
// Decode the single representation.
}
}If one repeated occurrence fails after earlier successful occurrences, all Candidate validationSingular and multiple decoders both return fixed-width function decode<T>(
param: Param<string, T>,
representation: unknown,
candidates: Iterable<Maybe<unknown>>,
path: string[],
): Result<T> {
let issues: readonly Issue[] | undefined;
let found = false;
for (let candidate of candidates) {
if (!candidate.exists) {
continue;
}
found = true;
let result = validate(param, candidate.value, path);
if (result.ok) {
return result;
}
issues = issues ?? result.issues;
}
return found
? { ok: false, issues: issues ?? [] }
: unableToDecode(representation, path);
}The first candidate accepted by the schema wins, preserving existing decoder Source semantics
One or more CLI occurrences make the CLI source present. Invalid CLI capture, Values remain already-interpreted JavaScript values and go directly to schema For non-text CLI representations, the policy must be explicit. The simplest // Singular already-interpreted representation
[Just(value)] // Multiple already-interpreted representations
[Just(values)];Alternatively, Model typingThe schema output remains authoritative. option(
name("config"),
multiple(),
schema(type("string[]")),
);
// model.config: string[]A transforming schema may accept an array candidate and return another model option(
name("config"),
multiple(),
schema(arrayToSet),
);
// model.config: Set<string>Implementation changes
|
367f7c6 to
ba05db5
Compare
Motivation
We want to support
cli --config inputs1.yml --config inputs2.ymland be able to merge them.Approach
multiplereadtakes that as an arg so all read logic is still contained there