Modeling WebAssembly Components with Wit
If you've ever defined a service interface using protocol buffers or you've written schemas using Avro, or any of a dozen other service and schema definition DSLs (like Amazon's Smithy or JSON Schema), then you should feel relatively at ease writing component specifications in wit
.
As you go through the examples here, keep in mind that all identifiers in wit
are lowercase kebab-cased. Whether or not we like kebab casing, it does provide for a level of consistency and predictability that many other DSLs don't provide. Once you get used to it it's actually pretty intuitive.
Records
Records in wit serve the same purpose as structs
in other languages and tools like wit-bindgen
actually convert wit records into those structs in target languages like Rust. Records are defined using the record
keyword and contain a block of field definitions.
record position {
latitude: f64,
longitude: f64,
altitude: f64,
}
record customer {
id: string,
name: string,
address: string,
phone: string,
}
A good rule of thumb when you're writing wit
that helps with remembering kebab case is that you only ever need to use the shift key for special characters like parentheses, colons, and brackets.
Enumerated Types
Many interface and schema definition languages have little to no support for enums and variant types. This is why it's such a pleasant surprise that wit
has incredibly robust support for enums. Enums in wit
come in two varieties: variant
, enum
.
enum sensor-type {
temperature
humidity
pressure
}
variant sensor-reading {
none,
temperature(list<f64>),
humidity(f64),
altitude(f64),
accelerometer(accelerometer-data)
}
(Bit) Flags
The flags
type, also known as a "bag of booleans", comes in handy when you need to keep track of a number of boolean flags and you want to use a nice compact memory footprint that packs booleans into the bit values of a single integer.
flags quest-completion {
has-sword
has-shield
has-boots
has-armor
is-hero
did-turn-knob
}
It's worth pointing out that none of the booleans in a flags type are related to each other. They can all be set/unset independently.
Functions
Functions can be defined inside interfaces, but they can also be included directly in world definitions via the import
or export
keyword. Whether you import/export individual functions in a world or whole interfaces is up to you and your own modeling preferences.
interface amazing {
fib: func(n: u32) -> u32
do-a-thing: func() -> result<string>
// we can even return multiple values from a function if they're named:
fancy: func(input: string) -> (output: string, acknowledged: bool)
// of course we can use records as well
fancy-record: func(input: fancy-data) -> result<not-so-fancy-data>
}
For those of us familiar with languages where keywords like fun
, func
, def
, or fn
precede the function name, wit's name-first notation can take a little getting used to.
Type Aliases
Type aliases are used to make reading and writing your wit
files a little easier. You can provide an alias (so long as it's a valid identifier) for any other data type: primitive or otherwise.
type okless-result = result<_, errno> // no "ok" type
type errless-result = result<string> // no "err" type
type char-result = result<char, errno> // both types specified
type row-list = list<row-data>
type why-would-we-do-this = result<option<list<string>>, option<list<bool>>>
Next let's take a look at how we can turn these wit
definitions into code!