www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Thinking about nothing: Solving the weirdness of the `void` type

reply Quirin Schroll <qs.il.paperinik gmail.com> writes:
**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
next sibling parent reply Timon Gehr <timon.gehr gmx.ch> writes:
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
parent Basile B. <b2.temp gmx.com> writes:
On Thursday, 13 July 2023 at 18:56:06 UTC, Timon Gehr wrote:
 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.**
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
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
Jul 14 2023
prev sibling parent Paul Backus <snarwin gmail.com> writes:
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