component-model icon indicating copy to clipboard operation
component-model copied to clipboard

Allow consuming methods

Open badeend opened this issue 2 years ago • 8 comments

Currently the spec states:

Validation of [method] names requires the first parameter of the function to be (param "self" (borrow $R)), where $R is the resource labeled r.

Are there any plans to allow methods that consume their self argument?

My specific use case would be; to define a specialized destructor for a resource, which takes additional arguments and is async.

badeend avatar Aug 14 '23 07:08 badeend

Great question! I was also wondering about this. A starting idea is to add a [destructor] prefix to <name> that implies a self parameter of type (own $R) that is expressed in Wit syntax as destructor(...args...) (symmetric to constructor). There's an interesting wrinkle for how to bind this to languages where destructors are nullary and thus where there's no good way to pass args (e.g., to a C++/Rust destructor, C# (and maybe JS?) using, or Python with); I was thinking maybe the validation rules require any ...args... to be option<T> for some T, and that's what gets called by those abovementioned language constructs, but you're also able to explicitly call the destructor earlier (with some specially-designated name) and pass arguments?

But how to make this "async" in the Preview 2 timeframe is tricky. Usually we express an async operation as a "pseudo-future" which is a resource that has a listen method that returns a pollable, so naively an async destructor would return a pseudo-future, which doesn't sound great. But instead perhaps we can put the listen method on the resource itself and say that if you want to asynchronously wait for the resource to be ready to be destroyed, you wait on its listen method, and then you're guaranteed that calling the destructor will be non-blocking. (And if you don't, then the destructor will either block or detach.)

lukewagner avatar Aug 14 '23 20:08 lukewagner

I think it might be practical to separate the general case (arbitrary consuming methods) from the special destructor case. Precisely because of what you said; many languages have built-in mechanisms or otherwise well-known idioms on how to handle these destructors, and have other (or no) vocabulary for dealing with arbitrary methods that consume their this parameter.

Destructors

For interoperability, I don't think they should be allowed to have parameters other than self. Also, they must return () or future<()> If we have this, does this effectively supersede resource.drop ?

Arbitrary consuming methods

These are just regular methods like any other. Ie. the can take any kind of parameters and return any kind of value. Except that they consume themselves.

WIT can already express these functions as static methods:

resource my-res {
	build: static func(%self: my-res) -> u32
}

So the only change would be in the generated name; from %[static]my-res.build to %[method]my-res.build

Examples:

resource database-transaction {
	commit: consume func() -> future<_>
	rollback: consume func() -> future<_>
}
resource url-builder { // Mutable
	set-scheme: func(scheme: string)

	// ...
	
	build: consume func() -> url
}

resource url { // Immutable
	scheme: func() -> string
	// etc...
}

badeend avatar Aug 16 '23 19:08 badeend

That's a really cool idea. consume is also more general than destructor in that it doesn't necessarily "destroy" the resource, it may just change the resource's state and return a new handle with a new type to reflect the new state and set of available operations (emulating "typestate"). So yeah, maybe it's consume we want; I'd like to noodle on it a bit more and hear what others think.

As for the question about whether destructors supersede resource.drop: I think it's useful to ensure that every resource type implementation is always able to add a destructor as a private impl detail (i.e., without changing the public interface). This effectively means that, from a public interface pov, every resource must have a dtor and so, if we're not wanting to allow the signature to vary (as I was considering above), there's no point in requiring the destructor to be written in Wit or explicitly imported, and then that's basically resource.drop :)

lukewagner avatar Aug 16 '23 23:08 lukewagner

That's a really cool idea. consume is also more general than destructor in that it doesn't necessarily "destroy" the resource, it may just change the resource's state and return a new handle with a new type to reflect the new state and set of available operations (emulating "typestate"). So yeah, maybe it's consume we want; I'd like to noodle on it a bit more and hear what others think.

I also like the more general design of a consume method, since they may just modify the resource (but require ownership for this) or actually consume them. When experimenting with wit-bindgen, I noticed that taking an owned resource handle currently provides no way of fully consuming the resource into an owned value of the type implementing the resource (think Rust's into_inner methods). Perhaps resources could gain a new drop function, which invalidates the owned resource handle but does not invoke its destructor, allowing the contained value to be retrieved?

juntyr avatar Aug 16 '23 23:08 juntyr

if we're not wanting to allow the signature to vary (as I was considering above), there's no point in requiring the destructor to be written in Wit

Almost. The way I sketched it above, the destructor was able to be async. The signature could change in its return type (() or future<()>).

I think it's useful to ensure that every resource type implementation is always able to add a destructor as a private impl detail

If destructors are only synchronous, then; Yes, that would indeed be nice. If destructors are allowed to be asynchronous, then it wouldn't a private implementation detail anymore.

badeend avatar Aug 17 '23 04:08 badeend

@badeend Great points! Given that it seems like one should always be able to drop any own handle one is holding (even if you haven't imported any other functions for it), it seems like this "destruction is sync/async" distinction needs to be on the resource type itself (to be clear: in a Preview 3 timeframe), so that you can always drop and also always know whether dropping is a sync or async operation.

lukewagner avatar Aug 17 '23 23:08 lukewagner

FYI, just came across some examples in wasi-http that could benefit from this:

resource response-outparam {
  set: static func(param: response-outparam, response: result<outgoing-response, error>);
}

resource incoming-body {
  finish: static func(this: incoming-body) -> future-trailers;
}

resource outgoing-body {
  finish: static func(this: outgoing-body, trailers: option<trailers>);
}

badeend avatar Oct 08 '23 08:10 badeend

Yep, those are great examples! I think we probably don't have time to add this to Preview 2, but I'm definitely interested to consider it for inclusion in Preview 3 next year.

lukewagner avatar Oct 16 '23 19:10 lukewagner