Skip to content
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

Clarify the relationship between Void and () #443

Closed
geoffromer opened this issue Apr 8, 2021 · 9 comments
Closed

Clarify the relationship between Void and () #443

geoffromer opened this issue Apr 8, 2021 · 9 comments
Labels
leads question A question for the leads team

Comments

@geoffromer
Copy link
Contributor

The placeholder design currently states that Carbon will have a primitive type Void, which it defines as "a type with only one possible value: empty". It also states that Carbon will have tuple types, and in the absence of any statement to the contrary, it is natural to assume that this will include (), the arity-0 tuple type. In other words, our placeholder design calls for Carbon to have two built-in unit types, with no evident difference in meaning between them. This raises the question of what the relationship between these two types is. I can see a few possible answers:

  • Void and () are distinct types that can be used in all the same ways. This would imply that ()->() and ()->Void are distinct function types, and so even if I know a function doesn't return anything, I still need to know which kind of nothing it returns before I can e.g. wrap it in the equivalent of a std::function. This seems very unlikely to lead to good ergonomic outcomes.
  • Void is an alias for (). This avoids the problems of the previous option, but does still force programmers to make an arbitrary choice between the two canonical spellings for this type, and force readers to internalize that the conspicuous syntactic difference between the two has no semantic significance. It could also cause confusion for programmers coming from C++, where void behaves very differently from std::tuple<>.
  • Void and () are distinct types, and support different usages. For example, we could follow C++ by saying that Void can be the return type of a function, but not the type of a variable or constant. This might help mitigate the problems of the first option, by pushing programmers toward a consistent way of choosing between the two types. However, the restrictions that C++ imposes on void have proven to have substantial drawbacks, with few offsetting benefits (if any).

Alternatively, we could avoid this question by removing one of the two types from Carbon. Naturally, this could take one of two forms:

  • We could remove (), and require tuples to have at least one element. I see this as roughly analogous to working in a number system that lacks zero, and I expect it to be about as unpleasant. To name just one consequence, this would require us to invent a novel syntax for defining and calling functions that take no parameters (perhaps f(Void)?).
  • We could remove Void. There are probably drawbacks to this option, but I'm honestly struggling to come up with any.
@austern
Copy link
Contributor

austern commented Apr 8, 2021

I don't have specific thoughts about these three options, but I do want to observe that Carbon's type(s) in this space probably should behave differently from C++'s void. The trouble is that there really is more than one kind of nothing in language design.

  • The unit type, a type that has only one possible value. In a lot of languages, including Haskell and Rust, both the type and its value are spelled (). The notation makes sense since one can think of it as an empty product type.
  • The bottom type, a type with no values. It's sometimes called or bottom or ! or the never type. One can think of it as an empty sum type. The bottom type is a little more esoteric than the unit type, but still sometimes useful. Some languages use it to represent the return type of a function that never returns.

In C++, void isn't quite the unit type and isn't quite the bottom type. It's a somewhat incoherent mixture of the two, even before you get into its other use where void* is the universal pointer. I don't think we want to replicate C++'s complicated void in Carbon, even if we could.

@josh11b
Copy link
Contributor

josh11b commented Apr 8, 2021

I would advocate for the unit type to be spelled (), since I would expect () to be allowed as part of the tuple syntax. I would not have a Void since we won't have something that matches C++'s void's weird semantics.

@zygoloid
Copy link
Contributor

zygoloid commented Apr 9, 2021

I think there is substantial value in having n-ary tuples with uniform behavior for arbitrary n, and that should include the n=0 and n=1 cases, so I think we want a () type.

As for C++ void, I think we should consider each weird behavior of void in turn and see which of them we think are use cases that we should address. Specifically:

  • void return types: these permit a return; (with no value). This seems useful.
  • void parameter types. Obsolete.
  • void* universal pointer. This seems useful, but is neither a bottom type nor a unit type; this is a "pointer to top".
  • Raw storage pointer (for example, returned by operator new). Again this seems useful, but we probably want a type that explicitly represents raw storage rather than void.
  • Cast to void to forcibly discard a value. Having a syntax for explicitly discarding a value seems useful.
  • dynamic_cast to void*. This is intricately bound up in C++'s virtual dispatch mechanism, and almost never used, because it's a one-way conversion. However, it is used in some C++ object registration systems. May be unnecessary if we don't support multiple implementation inheritance.
  • Subexpressions of type void are valid and sometimes useful in conditional expressions and comma expressions, but these use cases could equally well be served by (), or (for throw expressions) by a bottom type.
  • For coroutines, the distinction between return_value and return_void. This distinction is harmful to generality and should be removed.
  • void provides a standard name for an incomplete type (well, except that it doesn't behave like an incomplete type in some contexts) or for an absent type. For example, less<> being less<void> only worked because void was incomplete. This does not seem incredibly valuable; a custom name might in fact work better.

I don't find any motivation in that list to specifically retain a void type in Carbon that is distinct from (). The use cases that seem worth supporting all seem like things we could support better (or no worse) with either () or with a feature designed specifically for them, and having two different built-in unit types with no clear semantic distinction between them seems unhelpful.

Let's not have a distinct Void type in addition to ().

[Edit: Previously I suggested removing Void, but I did not intend to suggest that I prefer "remove Void" over "make Void an alias for ()". Both options seem acceptable.]

@chandlerc
Copy link
Contributor

chandlerc commented Apr 9, 2021

I'm not advocating for a distinct Void unit type (or bottom type, or type replicating the oddities of C++'s void).

But I think there is value in an alias of () that is more friendly and recognizable to programmers looking for a unit type outside of any tuple context. My intent in the overview was to have such an alias spelled Void.

I don't think the downsides of having the alias are too bad. I think from a teaching perspective there is an easy suggestion: in a context where a tuple might be relevant, expected, or more obvious, use (). In contexts where tuples have no particular role, use Void. Because they are aliases, this is purely a matter of human preference, either one is always fine.

@geoffromer
Copy link
Contributor Author

The alias option may be a good test case for the mooted principle that Carbon should minimize the need for a style guide, because experience from Swift makes it very clear that if we define such an alias, people will make style rules and even lint rules about it (and those rules will probably be less nuanced than the one @chandlerc suggests). Note that those examples aren't cherry-picked; the first three links are to the top three Google results for "swift style guide" (and the only actual style guides on the first results page, apart from a book that I don't have access to, but that does mention "Returning void" in its table of contents), and the last link is to what appears to be the canonical Swift linting tool.

All of the problems with such a type alias will be compounded by the fact that (unlike in Swift) it will also be a value alias: if let () x = (); is valid Carbon, then so are let Void x = ();, let () x = Void;, and let Void x = Void;. So for example, maybe we can make the typechecker pretty smart about using context to decide whether spell it Void or () in an error message, but print(()); and print(Void); are going to have to produce identical string outputs.

@jonmeow
Copy link
Contributor

jonmeow commented Apr 9, 2021

Agreed with @geoffromer, if we want Void and () to be the same thing, we should start by not providing Void and see if it causes issues. This echoes a broader philosophy to start with the minimum, and see what's required based on issues. It may be that Void isn't really needed because where it would be used (e.g., fn Foo() -> Void;), we can simply omit it for most users (still allowing fn Foo() -> () to address template/generic flexibility).

@zygoloid
Copy link
Contributor

All of the problems with such a type alias will be compounded by the fact that (unlike in Swift) it will also be a value alias

I don't think that necessarily follows, but the polytypic nature of () certainly seems like an interesting aspect to consider. In particular, I would expect Void to be specifically a type, rather than inheriting the polymorphic behavior of () -- I think we'd want let Type Void = ();, rather than alias Void = (); (or perhaps let () Void = ();). Under that approach, I'd expect these to be valid:

let Void x = ();
let () x = ();
let Type x = Void;
let Type x = ();

... and these to be invalid:

let Void x = Void; // error: cannot initialize 'x' of type 'Void' from value of type 'Type'
let () x = Void; // error: cannot initialize 'x' of type '()' from value of type 'Type'

Making Void and () be different in this way -- where () is general and Void is specifically a type -- seems like an argument in favor of having both. Nonetheless, I lean towards initially not providing Void -- I don't think we want the idiomatic way of writing a 3-tuple type to be (eg) (Int, Float, String) but the idiomatic way of writing a 0-tuple type to be Void.

Regarding the question of whether we permit return; and what it means, I'd suggest we tie that to the presence or absence of a return type in the function declaration. That is, if the function is introduced as:

fn Foo() -> T {

... then return expr; is valid and return; is invalid, even if T is (). And if the function is introduced as:

fn Foo() {

... then return; is valid and return expr; is invalid, even if expr is of type ().

@chandlerc
Copy link
Contributor

I'm not at all sure about the restricted model of Void as a type-only part of @zygoloid's summary.

But I'm down with the suggestion from @jonmeow and the second half of @zygoloid's post about how to start minimally and how to handle return;. I especially like the idea of trying to completely eliminate the () when using that syntax might be confusing (return types, return statements, etc).

So maybe we have a good consensus initial position here?

@jonmeow jonmeow mentioned this issue May 4, 2021
@zygoloid
Copy link
Contributor

No concerns raised with @chandlerc's suggestion that we have consensus.

Decision: We will initially not have Void, neither as a distinct type nor as an alias for (). The canonical unit type is ().

My suggestion for avoiding writing () in functions that return nothing is captured in #518 and not covered by this decision.

@geoffromer geoffromer mentioned this issue May 17, 2021
geoffromer added a commit that referenced this issue Jun 1, 2021
Implements resolution of #443.
chandlerc pushed a commit that referenced this issue Jun 28, 2022
Implements resolution of #443.
@jonmeow jonmeow added the leads question A question for the leads team label Aug 10, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
leads question A question for the leads team
Projects
None yet
Development

No branches or pull requests

6 participants