Description
Motivation
Let's say I'd like to build a component that consumes 3 configuration values a
, b
and c
(which in a 12-factor app I'd take as 3 environment variables). I could define a component with type:
(component
(import "get-config" (func (param "key" string) (result (option string))))
...
)
However, this loses the fact that my component specifically wants a
, b
and c
. If I'm using or deploying this component, I have to learn about these values from the docs or observe the behavior of get-config
at runtime. If I make a mistake, the error will only show up at runtime.
If instead I give my component this type:
(component
(import "config" (instance
(export "a" (func (result string)))
(export "b" (func (result string)))
(export "c" (func (result string)))
))
...
)
then there's a lot more declarative information that a host can leverage to provide a better developer experience. E.g., at deployment time, the host can check that there are indeed configuration values a
, b
and c
available and give a deployment-time error if not. There are also new optimizations made available at runtime: a
, b
and c
can be held in a dense array accessed by internally-computed static index, thereby avoiding the hash-table lookups at runtime (which can really matter when the target instantiation-time is in microseconds).
This example is in the domain of configuration, but you can also find analogous examples in:
- metrics (with one (inlineable)
(func (param "increment" u32))
import per metric) - logging (supporting multiple logging backends)
- HTTP upstreams (with one import of
wasi:http/outgoing-handler
per upstream. - KV/Blob stores (with one import per distinct storage backend)
In all these cases, the workaround for supporting multiple instances of the same interface inevitably involves using a dynamic string
parameter which loses the otherwise-declarative dependency information. (To be clear, some use cases do really need a runtime-dynamic string
name; we're talking here about the cases where the string would otherwise be a magic constant in the code.)
So given that we'd like to write components with the above type, how do we capture all these varying types in Wit? Of course we can write the Wit for any single component; for example, a Wit world
that supports the above component is:
world my-world {
import config: interface {
a: func() -> string
b: func() -> string
c: func() -> string
}
...
}
The challenge is writing a single world
that captures this component and all the other components, each with their own varying set of configuration values.
The proposal is to allow Wit to express not just a single interface
or world
, but a family of interface
s/world
s produced by substituting various parameters. Concretely, for the above I'd like to write (roughly; the exact syntax here is open for debate, of course):
world my-world {
import config: interface {
*: func() -> string
}
...
}
Using this, it would be natural for WASI standards to use *
in standardized interfaces such as:
default interface "wasi:config/values" {
*: func() -> string
}
which would then allow my-world
to be rewritten as:
world my-world {
import config: "wasi:config/values"
...
}
and leverage standard implementations of wasi:config/values
.
Sketch
While in general I think we'll want to allow putting parameters everywhere in Wit (types, names, parameters, results, fields, cases, imports, exports, ...), as a first step, I'd like to keep things scoped to just the case I showed above where a *
can show up in the name of the lone field of an interface
. While this won't be easy (there are a number of interesting producer/consumer design questions to work out), I think it'll be much easier than parameters in types, and so a good starting point. For terminology, I've been calling the general feature "Wit templates", and I think this first milestone could be called "variable imports and exports", but open to hearing alternatives.
From a Wit grammar perspective, the addition is fairly tiny, defining a:
variable-id ::= id | '*'
and then using variable-id
in func-item
and typedef-item
. As an additional validation-time constraint, *
would only be allowed inside an interface
block as the only item. (Or we could capture this constraint in the grammar; but I think we'll want to loosen this constraint over time.)
To allow encoding Wit documents as .wasm
binaries, we'll also need to extend Binary.md to support *
in names. One thing to be clear about here is that a concrete component won't be allowed to have any *
s in its imports/exports; only Wit documents. In a far-post-MVP future, one could imagine generalizing components with a staged-compilation model that did allow components to talk about *
names, so while we don't need to design how that all would work, it would be good to pick an encoding of *
that could be retconned into this far-post-MVP future if needed.
Significant design work will be needed per-language-toolchain around how to allow the developer to fill in the *
s in the world they are targeting. Riffing on an idea from @dicej and @fibonacci1729, this could come from buildconfig, with each line adding a field. E.g., in the context of cargo component, I could write:
[package.metadata.component.target]
path = "my-world.wit"
[package.metadata.component.parameters]
config.a = "string"
config.b = "string"
config.c = "string"
which would declare 3 imported configuration values a
, b
and c
, looking ahead to a future milestone where non-string
types could be allowed. More design work is probably necessary here to think through how to express all the not-so-simple cases. But the idea is that, from this language-specific buildconfig, tooling could derive a language-agnostic substitution which would then be applied to the target world to produce a "monomorphized" world with no *
s that is fed into bindings generation, keeping the rest of the build pipeline working like normal.
This is just a sketch, and more work is needed to flesh out the design, but I thought it'd be useful to put up this much now since the interface design questions above are coming up in a number of places at the moment. @dicej and @fibonacci1729 have also done a lot of thinking about this, so I'd invite them to drop in whatever they're thinking too.