Repository navigation
Default values #15
Copy link
Copy link
Open
Description
TLDR, these proposed changes:
- Allow function parameters, record fields and variant case payloads to have default values, as long as they're constant.
- When subtyping; allow variant cases to fall back on cases which have non-unit payload.
- Strip the
optiontype from its special status in various places.
Definition
literal-bool ::= 'false' | 'true'
literal-char ::= '"' <core:stringelem> '"'
constant ::= unit
| (bool <literal-bool>)
| (s8 <core:s8>) | (u8 <core:u8>)
| (s16 <core:s16>) | (u16 <core:u16>)
| (s32 <core:s32>) | (u32 <core:u32>)
| (s64 <core:s64>) | (u64 <core:u64>)
| (float32 <core:f32>) | (float64 <core:f64>)
| (char <literal-char>) | (string <core:string>)
| (record (field <name> <constant>)*)
| (variant (case <name> <constant>))
| (list <intertype> <constant>*)
Regarding the list constant: every provided <constant> must be a subtype of the list's <intertype>.
Aliases
false ↦ (bool false)
true ↦ (bool true)
(tuple <constant>*) ↦ (record (field "𝒊" <constant>)*) for 𝒊=0,1,...
(flags <name>*) ↦ (record (field <name> (bool true))*)
none ↦ (variant (case "none" unit))
(some <constant>) ↦ (variant (case "some" <constant>))
(ok <constant>) ↦ (variant (case "ok" <constant>))
(error <constant>) ↦ (variant (case "error" <constant>))
Types
Every constant expression has a fully deterministic interface type.
- Primitive constants map 1:1 to an interface type.
- List constants declare the element intertype in the constant expression itself.
- Record and Variant constants generate an ad-hoc interface type derived from the constant itself. Example:
Constant:
(record
(field "a" (s16 42))
(field "b" (string "Wasm"))
(field "c" (variant (case "ok" (bool true))))
(field "d" (list u8 (u8 3) (u8 5) (u8 8) (u8 13)))
)
Generated type:
(record
(field "a" s16)
(field "b" string)
(field "c" (variant (case "ok" bool)))
(field "d" (list u8))
)
Updates to existing definitions
- (record (field <name> <intertype>)*)
+ (record (field <name> <intertype> (default <constant>)?)*)
* <constant> must be a subtype of <intertype>
- (variant (case <name> <intertype> (refines <name>)?)*)
+ (variant (case <name> <intertype> (default <constant>)? (refines <name> <constant>?)?)*)
* The <constant> of the `default` clause must be a subtype of <intertype> from the same case.
* The <constant> of the `refines` clause must be a subtype of <intertype> from the case <name> refers to.
* The <constant> in the `refines` clause is optional when the case `<name>` refers to has a `default` value.
- (func <id>? (param <name>? <intertype>)* (result <intertype>))
+ (func <id>? (param <name>? <intertype> (default <constant>)?)* (result <intertype>))
* A param's <constant> must be a subtype of <intertype>
- (flags <name>*) ↦ (record (field <name> bool)*)
+ (flags <name>*) ↦ (record (field <name> bool (default (bool false)))*)
- (option <intertype>) ↦ (variant (case "none") (case "some" <intertype>))
+ (option <intertype>) ↦ (variant (case "none" unit (default unit)) (case "some" <intertype>))
- `record`: fields can be reordered; covariant field payload subtyping; superfluous fields can be ignored in the subtype; `option` fields can be ignored in the supertype
+ `record`: fields can be reordered; covariant field payload subtyping; superfluous fields can be ignored in the subtype; missing fields are set to the `default` value declared in the supertype.
- `func`: parameter names must match in order; contravariant parameter subtyping; superfluous parameters can be ignored in the subtype; `option` parameters can be ignored in the supertype; covariant result subtyping
+ `func`: parameter names must match in order; contravariant parameter subtyping; superfluous parameters can be ignored in the subtype; missing parameters are set to the `default` value declared in the supertype; covariant result subtypingThis is already more detailed than I intended it to be. Let me know if there is interest in pursuing this any further.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
lukewagner commentedon Apr 4, 2022
I've also wondered about having default values too, so this is a great issue to raise.
One reason for the current design with
optionis thatoptionconveys an extra bit of semantic information from the caller to the callee (distinguishing the cases of a caller not supplying a value vs. explicitly supplying what happens to be the default value). In most situations, this doesn't matter, but it seems like in some scenarios it's useful:optiongives the per-language bindings more flexibility for picking the language-idiomatic to deal with missing values. There's a lot of variability here when you dig into the fine details (e.g., witness the subtle distinctions betweennullvs.undefinedvs. when it comes to JavaScript in Web IDL). Withoption, the interface says "however your language expresses a missing parameter, do that, then communicate this to me as thenonecase.option, it's quite clear that the behavior of thenonecase is controlled by the callee.So all that made me lean toward the current
optiondesign; but maybe there are other reasons to prefer default values?One obvious one is that, when the
nonecase implies a default value (which is not always, but perhaps most of the time), it's more explicit to include that default value in the programmatic interface, rather than relying on documentation. At some point, I think we'll want to standardize a "documentation" custom section with structured contents (rather than free-form strings) and, in that context, a structured "default value" annotation could make sense and be programmatically available to, e.g., intellisense.badeend commentedon Apr 24, 2022
When posed as an either/or question, I'm ambivalent. I don't think default values are inherently better than option types or vice versa. They can both fill each other's role to some extent.
That said, I think the two concepts are orthogonal. Especially in languages that support both.
For instance, these three are not the same:
Having only option types at their disposal forces programmers to introduce an extra state in the system where this might not be appropriate. In the preceding example,
asHtmlended up with a third limbo state "None", besides true and false.Another example is the
flagsinterface type. Either it must be special cased in the subtyping rules or you can't add new flags in a backwards compatible way.lukewagner commentedon Apr 25, 2022
Yeah, I suppose I see your point; sometimes you don't want to add a new
Nonecase, but you also don't want folks to have to fill in every flag/field/parameter manually.badeend commentedon Jun 6, 2022
I think languages like Java, C#, etc. would most likely use
Optionfor their implicit nullability of all reference types. Leaving them no other way to communicate missing parameters. (Other than a nestedOption<Option<<...>>?)Could this be solved by requiring that, in case both the subtype and supertype declare a default value for a particular field/parameter, the values must be compatible (according to the subtyping rules) ? That way it wouldn't matter whether the caller or the callee is responsible for default values; the end result is effectively the same.
Though, with that restriction in place, I think it makes sense to make the caller responsible for providing the default values. See examples below.
If that distinction is truly needed, you can still use an Option type. No harm done.
I don't think the documentation/annotation/metadata direction is enough whenever the subtype contains a field/parameter with a default value the supertype is not aware of. See example 2 below. In this case the component model glue code has to be aware of default values.
I completely understand. I'll leave that tradeoff for you to make.
Examples
It would be awesome if, through some mechanism, the following examples would Just Work™.
lukewagner commentedon Jun 7, 2022
My expectation here is that the language bindings start with the interface type (which does not have implicit nullability, only
option<T>) and then coerces to and from that. So if the bindings accept a source-language reference for a non-nullable interface-typeT, then on source-languagenull, the bindings would eithertrap,throwor coerce in some language-bindings-defined manner. Thus,option<T>would not be forced to inherit the default-nullability of languages.That's an interesting idea! Yes, I suppose theoretically it could. It seems like this would put further requirements on this constant-valued interface-typed expression language, though.
Thanks for writing out all those examples! I hadn't even thought about the interaction between default values and function subtyping allowing extra optional parameters... some of those cases were rather surprising to me and it seems like we'll need to think long and hard about this.
guybedford commentedon Dec 16, 2022
Nice to see this fleshed out proposal. Having to deal with None v Some branches in all places where things are optional does create overhead that is nicer to avoid with default values.
Is the intention for the "default" default implementations of types to be defined? Can we extend the proposal semantics to the WIT definitions at this point?
MendyBerger commentedon May 29, 2026
This would also help out with evolving interfaces without making breaking changes.
Case in point: when requesting a device in WebGPU, you can specify limits of the device. The limits object keeps evolving and is expected to grow over time as new features are being added. Without default values, every new limit is a breaking change.
badeend commentedon May 29, 2026
In lieu of value subtyping, the next best thing right now is to instead define the limits as a resource type and use getter/setter methods for each option