Thinking about nothing: Solving the weirdness of the `void` type
Quirin Schroll
qs.il.paperinik at gmail.com
Thu Jul 13 17:58:27 UTC 2023
**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.
More information about the Digitalmars-d
mailing list