Skip to content

explicit self parameter for resource types? #334

Closed

Description

@serephus

We wish to design a builder API with wit, however this is impossble now as I understand it.

Let say we want something like this:

resource example {};

resource example-builder {
  new: static func() -> example-builder;
  set_xxx: func(xxx: u64) -> example-builder;
  // some other builder functions
  build: func() -> example;
}

this won't work because set_xxx implicit takes a borrow<T> and returns an owned<T>. (breaks unchained calls)

this won't work either because there is no lifetime annotation in wit

set_xxx: func(xxx: u64) -> borrow<example-builder>;

so wit-bindgen won't be able to generate bindings for language like Rust.

And we can't make resource method implicit takes an owned<T> either.

also, this is not what we want either:

set_xxx: static func(self: owned<example-builder>, xxx: u64) -> owned<example-builder>;

The problem here IMO is the implicit self argument like in C++. C++ has to add a lot method specifier like const/&/&&/static to specify what this parameter a method took. And C++23 approved a feature called "deducing this" to fix this problem. I really wish this wasn't the case here.

Related Issue: #226

Activity

  1. lukewagner commented on Apr 8, 2024

    @lukewagner
    Member

    It seems like the idiom of returning the self parameter is a rather language-specific syntactic workaround. That is, since the returned value is assumed to be exactly the same as the passed parameter value, you don't actually need to be passing an extra runtime handle value from callee to caller as a return value; the caller knows exactly what to use as the parameter to the next method call in the chain; it's just a matter of getting the nice syntax to do so.

    Thus, I wonder if what we want here is a [builder] name prefix (as a further refinement to the existing [method] name prefix) that hints to the language bindings generator that this method should be callable in a builder style which, for some languages, means tacking on the self-parameter as a return value. This would both be more efficient (esp. with caller-side inlining) and would potentially allow better syntactic integration in a wider variety of languages by stating the interface author's real intent.

    WDYT?

  2. serephus commented on Apr 9, 2024

    @serephus
    Author

    Sorry I don't know if I understand your comment correctly.

    What im saying here is I think the rust way is the correct way to go.

    In Rust, the self parameter of a method is explicit and can take several form.

    1. &self
    2. &mut self -> borrow<T>
    3. self
    4. mut self -> own<T> / consume
    5. ...other valid form

    the return type can mix and match with self parameter. for example: both (&mut self) -> &mut Self and (mut self) -> Self allows chained builder calls.

    from an user perspective, I would like to be able to express what I want to pass as self instead of an implicit borrow<T>.

    As a side note: consume in issue #226 would be fine as a solution, although I would prefer explicit.
    Side note 2: perhaps we need borrow<T> and mut-borrow<T> or perhaps (shared<T>/exclusive<T>/own<T>)?

  3. serephus commented on Apr 9, 2024

    @serephus
    Author

    Anyway, this is just a thought for your consideration, I'm not very familiar with wasm/wit. I hope it make sense.

  4. FrankReh commented on Apr 9, 2024

    @FrankReh
    Contributor

    I've run into nice builder patterns in Rust, around Transport Layer Security (TLS) I think, where the builder pattern implemented a state machine, so methods didn't necessarily return Self, but a new, or "next", builder type, with the next builder type being slightly different, to represent the progression through the building state machine.

    I've also run into builder patterns where I felt forced to store the builder to a local and then call mutating methods on it. Didn't feel as natural but I realized I just had to write the code once and then I could forget it so it didn't matter much.

    Also some builder methods return a Result, so it was nice to be able to have the function return an Error, even from a chained list.

    And being able to throw in a conditional test in the middle of builder calls is useful. So sometimes wanting to store the intermediate builder value in a local so the conditional block can be run or not on it, before continuing with the finalizing builder steps.

    So there are benefits to supporting fn(&mut self, ...) functions and fn(self, ...) -> Self, and fn(self, ...) -> NextBuilder patterns.

    But should the Component Model have to spell out something special for the builder pattern? (I know I'm not able to take many other language constraints into account so maybe the answer is yes but it is surprising.)

    But getting back to the original question or issue. Naively, it is surprising a Resource can't be consumed in some form of a signature. The difficulty must be on the callee side, in doing something with the resource instantiation that should now no longer exist. An interesting problem. Seems like the API around Resources will be super important.

  5. lukewagner commented on Apr 9, 2024

    @lukewagner
    Member

    FWIW, the linearly-typed chaining you're describing is currently expressible via, e.g.:

    resource builder-step1;
    resource builder-step2;
    resource finished-thing;
    build-thing: func(v: step1-value) -> builder-step1;
    set-step2-value: func(b: builder-step1, v: step2-value) -> builder-step2;
    finish-thing: func(b: builder-step2, v: final-value) -> finished-thing;

    What's missing is the nice syntactic sugar that lets you write a nice terse method call chain.

  6. oovm commented on May 12, 2024

    @oovm

    If I understand the builder pattern correctly, the steps can be reordered, so you need to generate as many permutations of the step-builder-wrapper as possible.

    To express the builder pattern at the type level, there is even a special design pattern called State Type (or Generic as Type Class Pattern).

    This is a very high-level type application, and I think it is more realistic to directly support move semantics(self: own<T> in question).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions