Skip to content

Switch (back) to multi-return, remove unit, s/expected/result/ #69

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Aug 4, 2022

Conversation

lukewagner
Copy link
Member

@lukewagner lukewagner commented Jul 14, 2022

This PR switches back to multi-return and, for the reasons outlined in #41, makes several associated changes:

  • unit is removed (empty return can be used instead)
  • variant and expected case payloads are changed to lists of valuescontain either 0 or 1 valtype
  • expected is renamed to result
  • parameter/result lists can either be all-named or have a single unnamed type

@lukewagner
Copy link
Member Author

lukewagner commented Jul 18, 2022

I just rebased this PR on top of #70 and made the analogous changes to WIT.md (s/unit// s/expected/result/). Three subjective choices that might be wrong and are thus worth calling out and discussing are:

  • For result, instead of requiring _ to be manually written for empty payloads (e.g., result<_,_>), I added a few syntactic cases: result<T,E>, result<T>, result<_,E> and result. IMO, this is much nicer-looking and I hope has an obvious-enough meaning.
  • For functions, I made the param/result tuples symmetric, matching the AST changes, so that they can both be a single type or a parenthesized named tuple, e.g.: func u32 -> u32, func (x:u32) -> u32, func u32 -> (y:u32), func (x:u32) -> (y:u32).
  • The previous bullet interacts poorly with the previous syntactic sugar of allowing -> () to be elided, since it lets you have x : func u32 which, without the parens around u32, may look confusing. The two options seemed to be: (1) make params asymmetric and require parens around params always, (2) remove the optional--> () sugar, so that you'll have x : func u32 -> (). IMO, removing the sugar is more clear (this is a function; what's its return type?) and language-neutral (many languages always require an explicit return type) and most functions should be returning some sort of result anyways, so it's not like a case we need to syntactically optimize for, so I currently went with that, but I'd be interested to hear other opinions. [Edit:] Another option is to always parenthesize params/results.

@theduke
Copy link

theduke commented Jul 19, 2022

I'm curios: what is the motivation for removing unit and switching back to "implicit" unit types via omission?

Is it related to subtyping, simplifying the type model or language mapping constraints?

I quite appreciated having an explicit unit type.

How would one represent a union with a potential unit value?

For example, in Assemblyscript: i64 | f64 | null, or Scala: Unit | String, ...

Without a dedicated unit type there is quite a bit of ambiguity there.

For example, one possible mapping is using (option T) on the WIT side.
So i64 | f64 |null could be (option (union i64 float64)).
But sticking with the Scala example: Scala has an Option[T] type, so distinguishing between Unit|String and Option[String] is a matter of interpretation / preference.

There are probably other examples of where this ambiguity could potentially be problematic.

@theduke
Copy link

theduke commented Jul 19, 2022

Of course I can understand that having a unit type is also problematic, because not every language has a clear notion of such a type. Would unit be null or undefined in Javascript? C++ 17 added std::monostate, but what about C? etc

Either way it would be nice if the docs contained a reasoning and motivation for some of the design choices.

@lukewagner
Copy link
Member Author

lukewagner commented Jul 19, 2022

unit was added when multi-return was originally removed (as the alternative way to express "no meaningful return value"), so we're basically just reverting that change here so that we don't end up with two ways to express the same idea (forcing API designers to ask which is better and make varying choices). As you said, unit introduces per-language binding questions that are nice to avoid altogether (e.g.). Also, for cases where one might want to use a unit to implicitly convey "a case without a payload", probably union is best avoided and a variant (with a case with an empty payload) will be more clear for everyone.

@sunfishcode
Copy link
Member

* For functions, I made the param/result tuples symmetric, matching the AST changes, so that they can both be a single type or a parenthesized named tuple, e.g.: `func u32 -> u32`, `func (x:u32) -> u32`, `func u32 -> (y:u32)`, `func (x:u32) -> (y:u32)`.

I suggest embracing the asymmetry in Wit. Wit syntax is all about human readability and convenience. While there is a mechanical symmetry, there is also an overwhelming practical asymmetry. In particular, result is almost exclusively used asymmetrically, as a result and not as a param, and will be used pervasively.

From that perspective, I suggest omitting the func u32 form, and making the syntax forms for 0, 1, and 2 return values look like this:

aaa: func(x: u32)
bbb: func(x: u32) -> u32
ccc: func(x: u32) -> (u32, u32)

This implies special-case syntax for 0 and 1.

The forms with -> () and -> (u32) for 0 and 1 might make sense to preserve, for the convenience of people machine-generating wit files.

@lukewagner
Copy link
Member Author

I'm in the 40-60% range on this question, so no strong opinions here, but just to dig into the reasoning for the asymmetry a bit:

In particular, result is almost exclusively used asymmetrically, as a result and not as a param, and will be used pervasively.

I agree that the vast majority of functions should be returning a result (or a future<result> or something), but I thought this implied the opposite conclusion: why have the extra syntactic sugar for leaving off the -> () if noone is almost ever writing it? It seems like you'd almost want to highlight it because maybe someone just forgot a result.

@skreborn
Copy link

For what it's worth, I'm all for explicitness. I've come to appreciate having to type a little more to satisfy a compiler versus accidentally leaving off important details. I would much rather unambiguously specify an empty result set than have the compiler figure it out. In fact, I'd prefer if the compiler straight-up told me that I'm missing a return type instead of quietly rolling over it.

@sunfishcode
Copy link
Member

I'm in the 40-60% range on this question, so no strong opinions here, but just to dig into the reasoning for the asymmetry a bit:

In particular, result is almost exclusively used asymmetrically, as a result and not as a param, and will be used pervasively.

I agree that the vast majority of functions should be returning a result (or a future<result> or something), but I thought this implied the opposite conclusion: why have the extra syntactic sugar for leaving off the -> () if noone is almost ever writing it? It seems like you'd almost want to highlight it because maybe someone just forgot a result.

WASI itself is expecting to have functions with no return values, in the upcoming logging API. It's much less common, but does happen. That said, -> () wouldn't be the worst, but it does seem less familiar (Typescript, Rust).

My main suggestion here is that we don't need the special cases for 0 or 1 parameters. Those would risk confusion and uninteresting variety, which is a more important concern than syntactic symmetry here. And consequently, we don't need whitespace between func and (.

@lukewagner
Copy link
Member Author

lukewagner commented Jul 29, 2022

If we remove the "implicit -> ()" syntactic sugar, then there is no special-case for for 0 params/results, only for 1 param/result. I also don't like special cases, and so I also wanted to always require parens; the thing that made me consider otherwise is that, independently of parenthesization, when it comes to naming we already have this design choice from #41 wherein you can leave off the name of a single param/result, making "one unnamed parameter" a special case anyhow. Thus, it seemed kindof naturally aligned to say that ( marks the beginning of 0..N <name>: <valtype> pairs whereas non-( means just <valtype> -- that's basically what the underlying component-model AST is saying anyway.

@sunfishcode
Copy link
Member

The circumstances that make a single unnamed return value a useful special case don't seem to apply to parameters in the same way, so my suggestion is to deemphasize this special case in the wit syntax for parameters.

Also, omitting the parens in a param list gives the appearance of a different kind of function.

I suggest the syntax func(u32) for a function with a single unnamed parameter.

@lukewagner
Copy link
Member Author

The circumstances that make a single unnamed return value a useful special case don't seem to apply to parameters in the same way, so my suggestion is to deemphasize this special case in the wit syntax for parameters.

I suppose it's a matter of perspective, but in the presence of multi-return, it does feel like the same circumstances apply to both: if we say that we always want to have parens around params, then it seems surprising to have a different rationale applied to results. (It made sense for single-return, of course; but the context here is updating WIT to stay in lock-step with multi-return in the component model.)

Copy link
Member

@sunfishcode sunfishcode left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me!

@lukewagner
Copy link
Member Author

Thanks for the review!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants