digitalmars.D - Thinking about nothing: Solving the weirdness of the `void` type
- Quirin Schroll (140/140) Jul 13 2023 **Key idea: Make `void` an alias to `typeof(null)` when used as a
- Timon Gehr (20/30) Jul 13 2023 Some issues with that:
- Basile B. (22/30) Jul 14 2023 That's correct but `typeof(null)` is actually more simply the
- Paul Backus (10/13) Jul 13 2023 This proposal seems strictly worse in every respect than
**Key idea: Make `void` an alias to `typeof(null)` when used as a return type, with the goal of deprecating `void` return types altogether.** If this works, we have a three-step plan to transition the language into a state in which `void` has a clear meaning: It is the invalid type, the not-a-type type as in the not-a-number floating-point number. Another neat thing about this, the following steps are not needed for the previous ones to make sense. Here are the steps: 1. Make `void` an alias to `typeof(null)` when used as a return type. This opens a transition path to remove `void` as a return type without (much) breakage. Almost all of the post is about this. 2. Remove concessions needed so that Step 1 had little breakage. 3. Finally, make `void` as a return type invalid and make `void*`, `void[n]` and `void[]` basic types. Step 1 brings the language in a position so that for the breakage that is intentional in Step 2, there is a transition path. The language after Step 1 admits mixing “new and good” code with “old and bad” code. The last step is essentially closing the door to the past. After Step 3, `void*`, `void[n]` and `void[]` need the special-casing that they deserve. You might think that a template like ```d auto f(T)(T[] values); ``` should work fine with `void[]`, but that depends on the template. Generally speaking, it’s likely that if the algorithm `f` implements special-cases `void[]` or (unintentionally) does not work with it anyways. The simple reason is that there are loads of things one can do with `T[]` (and `T` values) if and only if `T` isn’t `void`. I’m not suggesting we change the syntax of `void[]` to reflect that it’s not a slice of `void` (one reason is that it’s additional and unnecessary breakage and another is that a `void[]` still is a slice, `void[n]` still is an array and `void*` still is a pointer, at last sort of), but I do suggest that one cannot create the `void[]` type by stitching together `void` and `[]`. In the example above, I suggest one cannot instantiate `f!void`. (If `f` wants to support `void[]`, the author should supply an overload.) This is exactly like you cannot stitch together void initialization: One has to use the `void` keyword: ```d alias V = void; int x = V; // error: type `void` has no value ``` --- We all know that `void` is weird. Depending on usage, it’s not even a type, e.g. in void initialization where it serves as a keyword; this makes it weirder than in C++, but *that* actually isn’t the problem. The problem is that the `void` is weird as a type. You can have `void*`, `void[n]` and `void[]` and you can seemingly return a `void` object or get one via a function call, but you can’t have a `void` typed local variable or parameter. For the first two, there’s a “fix”: Consider `void*`, `void[n]` and `void[]` as basic types (not as per the grammar, just conceptually) since really a `void[]` (or `void[n]`) isn’t a slice/array of actual `void` objects. The `void` return is a different beast. The language kind of pretends that `void` values exist when it comes to `return` and function calling, e.g. this works: ```d void f(); void g() { return f(); } ``` This is already a concession to the design of `void`. The code would, of course, work for `int` (in place of `void`) because `int` does have values, but – maybe surprisingly – it also works for `noreturn`, which does not have values. The problem is not the supposed concession, it’s the concession’s limitations: The difference between `void` and any other type is that the transformation of going through a variable works for any type except `void`: ```d void f(); void g() { auto x = f(); // type `void` is inferred from initializer `f()`, and variables cannot be of type `void` return x; // cannot return non-void from `void` function } ``` Part of the plan is to fix this weirdness without special-casing `void` even more. **Note: Because `typeof(null)` appears quite often in the remainder of this post, I’ll assume `null_t` is an alias of `typeof(null)`.** As a return type, `void` is a unit-type. There were proposals to give `void` unit-type semantics, but that’s not possible, because then `void[]` and friends (especially unintentionally formed) would break. It occurred to me: With `null_t`, doesn’t D already have perfectly good unit type, one whose slices aren’t anything special (apart from being quite useless), one that admits being the type of a parameter, local variable, data member, etc.? So, why not use it? My idea was to make `void`, when in the place of a return type, an alias for `null_t`. Ideally, that’s it. Maybe we need to make concessions and find places where `void` return types shouldn’t be an alias of `null_t`. An example that came to my mind immediately is the explicit and implicit drop-out-of-function `return` statement: If the return type is `void`, a function returns implicitly at the end of its scope and a return statement can be without value. For `null_t`, maybe this should not be allowed. And vice-versa, `return null;` maybe shouldn’t be allowed for a `void` function. On the other hand, there’s no real the damage to just allowing all of them. ```d null_t f() { } // Allow it? null_t f() { return; } // Allow it? null_t f() { return null; } // Definitely good! void f() { } // Definitely good! void f() { return; } // Definitely good! void f() { return null; } // Allow it? ``` I don’t know if (and where) “return type `void` actually is `null_t`” runs into real-world issues, but maybe `void` could become a type that represents “not really a valid type” as in: What’s the common type between `int[]` and `bool`? It’s `void`, i.e. there is none; almost like pun of the “numbers” in `float`/`double` that are “not-a-number,” `void` is a type that’s “not-a-type.” We do have the issue that templates query things like `is(typeof(f()) == void)` and these must continue to work. My best attempt would be to make `void` be an alias of `null_t` in this context as well, that is, special-case the `is` query to interpret the pattern `is(typeof(CallExpression) == void)` as if it were `is(typeof(CallExpression) == null_t)`; this can be done because after making `void` return types an alias of `null_t` there really isn’t a way for a well-formed CallExpression to return actual `void`. To test for mere well-formedness, one uses `is(typeof())` without equality check (currently and with this change as well). If we made `typeof(f())` result in `void` if the function is specified with `void`, but then there would be (subtle) differences between specifying `void` and `null_t` as the return type, and this is something that we should really avoid.
Jul 13 2023
On 7/13/23 19:58, Quirin Schroll wrote:**Key idea: Make `void` an alias to `typeof(null)` when used as a return type, with the goal of deprecating `void` return types altogether.** If this works, we have a three-step plan to transition the language into a state in which `void` has a clear meaning: It is the invalid type, the not-a-type type as in the not-a-number floating-point number. Another neat thing about this, the following steps are not needed for the previous ones to make sense. ...Some issues with that: 1. deprecation of language features has been deprecated 2. `void` is compatible with C and everyone has built muscle memory for typing it. Also, it is true that `void[]` is semantically completely unrelated to `void`, but it's still a special case of `T[]` for `T=void`. 3. `typeof(null)` is not a canonical unit type. It has a subtyping relationship with class references and pointers. This means you would sometimes actually need to reserve a full machine word for passing a `typeof(null)` across a virtual function boundary, while ideally a unit type can be passed without any dedicated code being executed. ```d class C{ void foo(typeof(null) x){} } class D:C{ void foo(C c){} } ``` There's probably more issues, this is just off the top of my head.
Jul 13 2023
On Thursday, 13 July 2023 at 18:56:06 UTC, Timon Gehr wrote:On 7/13/23 19:58, Quirin Schroll wrote:That's correct but `typeof(null)` is actually more simply the type of the `null` expression [1]. Then by implicit conversion it indeed converts to all reference tpyes and pointers types. That implicit cast happens **99.99%** of the time, but not always e.g: ```d void* v() { auto error() // the internal type null { things(); return null; } if (auto s = stuff()) return s; return error(); // cast(void*) error() } ``` So the proposed change cannot work. [1]: https://dlang.org/spec/expression.html#null**Key idea: Make `void` an alias to `typeof(null)` when used as a return type, with the goal of deprecating `void` return types altogether.**3. `typeof(null)` is not a canonical unit type. It has a subtyping relationship with class references and pointers. This means you would sometimes actually need to reserve a full
Jul 14 2023
On Thursday, 13 July 2023 at 17:58:27 UTC, Quirin Schroll wrote:**Key idea: Make `void` an alias to `typeof(null)` when used as a return type, with the goal of deprecating `void` return types altogether.**This proposal seems strictly worse in every respect than [Dennis's proposal to make `void` a unit type with a size of 1.][1] It will break approximately 100% of D projects (even "Hello world" uses `void main()`), and in return we get...even more special cases than we started with? A type that can be used as an array member but not as a return value? I appreciate the effort you've put into thinking this through, but, as the kids say, this ain't it. [1]: https://github.com/dlang/DIPs/pull/173
Jul 13 2023