January 16, 2019
On Wednesday, 16 January 2019 at 18:52:17 UTC, Johannes Loher wrote:
> No, I actually meant it exactly as I wrote it. The reasoning is the following: If we were to make `void` a proper unit type, it needs to be a type with exactly one value. We could choose any type we want for this, as long as it only has one value. E.g. `struct A {}` could be a candidate for such a type as any two instances of it are equal. However, `Tbottom*` is also a type with only one value: `null`. To me it seemed like the canonical choice for defining `void`.

An interesting find! I cannot immediately see any good argument against it. Deserves a closer look IMO.

January 16, 2019
On Wednesday, 16 January 2019 at 11:23:18 UTC, Johan Engelen wrote:
> This is just another example of using the bottom type to signify `noreturn`, which is trivially done with an attribute and doesn't need a new type. I'd like to see _other_ practical uses of the bottom type.
>
> -Johan

Here's an example I ran into while working on my `expectations` library. [1]

Internally, an Expected!(T, E) is represented as a discriminated union containing either an "expected" value of type T or an error value of type E. I would like to implement a property method, `value`, that either returns the expected value if there is one, or throws an exception if there isn't (similar to Rust's `unwrap`).

The obvious implementation looks like this:

struct Expected(T, E) {
    private SumType!(T, E) data;

    @property T value() {
        return data.match!(
            (T val) => val,
            (E err) { throw new Exception(err.to!string); }
        );
    }
}

However, this will not compile, because the second lambda is inferred to have a return type of void, and void is not implicitly convertible to T. Instead, I have to resort to the following ugly workaround:

@property T value() {
    try {
        return data.tryMatch!(
            (T val) => val
        );
    } catch (MatchException _) {
        data.tryMatch!(
            (E err) => throw new Exception(err.to!string)
        );
    }
}

If the second lambda were inferred to have a return type of Tbottom, none of this would be necessary. The first version would Just Work™.

[1] http://expectations.dub.pm
January 16, 2019
On Wednesday, 16 January 2019 at 22:32:33 UTC, Walter Bright wrote:
> An example of an awkward corner case caused by lack of a bottom type:
>
>    @noreturn int betty();
>
> How can it return an int yet not return? It makes no sense. And if one was doing type calculus on the return value of betty(), it would show up as 'int' and botch everything up. It'd be like substituting '5' in calculations that need a '0', but have no notion of '0'.
>
> The trouble with 'void' as a bottom type is it is only half done. For example, 'void' functions are procedures, and certainly do return. A void* is certainly a pointer to something, not nothing. 'void' is the one with awkward corner cases, it's just we're so used to them we don't see them, like few C++ people recognized the identity function problem.
>
> My trouble explaining the immediate value of a bottom type stems from my poor knowledge of type theory. Other languages aren't necessarily good role models here, as few language designers seem to know much about type calculus, either. I was hoping that someone who does understand it would help out!
>
> (I.e. it's not just about functions that don't return. That's just an obvious benefit.)

So you want to add something you don't fully understand and aren't sure if there is even any benefit at all. You just presume there to be a benefit? I don't think you should think of void and void* as the same thing. Pointers are their own type, if all you have is a void* and no other information, you might as well be pointing at nothing. I suggest you figure out what it is you actually want to implement, this is no better than cowboy programming. Hell might even be worse than a pilot wearing a blindfold with someone whispering in his ear that he is going the right direction. Don't implement something you obviously don't know enough about. I can only imagine what kind of disaster this is going to be in comparison to auto-encoding.

January 16, 2019
On Wed, 16 Jan 2019 14:32:33 -0800, Walter Bright wrote:
> * top type (represents every value, like D's "Object" root type)

D doesn't have a top type. Object is the top of the hierarchy of non-COM, non-C++ classes. std.variant.Variant is the closest thing D has to a top type.

> An example of an awkward corner case caused by lack of a bottom type:
> 
>     @noreturn int betty();
> 
> How can it return an int yet not return? It makes no sense.

Any empty type would suffice here. Bottom is just one canonical empty type.

> My trouble explaining the immediate value of a bottom type stems from my poor knowledge of type theory. Other languages aren't necessarily good role models here, as few language designers seem to know much about type calculus, either. I was hoping that someone who does understand it would help out!

I'm sorry that I missed your post asking for help on this DIP's background research while it was being drafted.

> (I.e. it's not just about functions that don't return. That's just an
> obvious benefit.)

This seems like a very good way to get the details wrong and eliminate the potential benefits that a different implementation, one better informed by theory or real-world examples from other programming languages, could give.
January 16, 2019
On Wednesday, 16 January 2019 at 23:08:38 UTC, Paul Backus wrote:
> On Wednesday, 16 January 2019 at 11:23:18 UTC, Johan Engelen wrote:
>> This is just another example of using the bottom type to signify `noreturn`, which is trivially done with an attribute and doesn't need a new type. I'd like to see _other_ practical uses of the bottom type.
>>
>> -Johan
>
> Here's an example I ran into while working on my `expectations` library. [1]
>
> Internally, an Expected!(T, E) is represented as a discriminated union containing either an "expected" value of type T or an error value of type E. I would like to implement a property method, `value`, that either returns the expected value if there is one, or throws an exception if there isn't (similar to Rust's `unwrap`).
>
> The obvious implementation looks like this:
>
> struct Expected(T, E) {
>     private SumType!(T, E) data;
>
>     @property T value() {
>         return data.match!(
>             (T val) => val,
>             (E err) { throw new Exception(err.to!string); }
>         );
>     }
> }
>
> However, this will not compile, because the second lambda is inferred to have a return type of void, and void is not implicitly convertible to T. Instead, I have to resort to the following ugly workaround:
>
> @property T value() {
>     try {
>         return data.tryMatch!(
>             (T val) => val
>         );
>     } catch (MatchException _) {
>         data.tryMatch!(
>             (E err) => throw new Exception(err.to!string)
>         );
>     }
> }
>
> If the second lambda were inferred to have a return type of Tbottom, none of this would be necessary. The first version would Just Work™.
>
> [1] http://expectations.dub.pm

As far as the DIP goes, it does not appear to make any mention of it working this way. It does say that the bottom type is implicitly convertible to any type, but being implicitly convertible does not mean that function types are also implicitly convertible. Just as float and int may be implicitly convertible, their functions are not. The only function types that appear to be implicitly convertible are those of class and their derived classes.

This would mean that the bottom type function would need to be ABI compatible with every single function type. I'm not sure if that's possible, but I feel that would need to addressed in the DIP.



January 16, 2019
On Wednesday, 16 January 2019 at 23:08:38 UTC, Paul Backus wrote:
> On Wednesday, 16 January 2019 at 11:23:18 UTC, Johan Engelen wrote:
>> This is just another example of using the bottom type to signify `noreturn`, which is trivially done with an attribute and doesn't need a new type. I'd like to see _other_ practical uses of the bottom type.
>>
>> -Johan
>
> Here's an example I ran into while working on my `expectations` library. [1]
>
> Internally, an Expected!(T, E) is represented as a discriminated union containing either an "expected" value of type T or an error value of type E. I would like to implement a property method, `value`, that either returns the expected value if there is one, or throws an exception if there isn't (similar to Rust's `unwrap`).
>
> The obvious implementation looks like this:
>
> struct Expected(T, E) {
>     private SumType!(T, E) data;
>
>     @property T value() {
>         return data.match!(
>             (T val) => val,
>             (E err) { throw new Exception(err.to!string); }
>         );
>     }
> }
>
> However, this will not compile, because the second lambda is inferred to have a return type of void, and void is not implicitly convertible to T. Instead, I have to resort to the following ugly workaround:
>
> @property T value() {
>     try {
>         return data.tryMatch!(
>             (T val) => val
>         );
>     } catch (MatchException _) {
>         data.tryMatch!(
>             (E err) => throw new Exception(err.to!string)
>         );
>     }
> }
>
> If the second lambda were inferred to have a return type of Tbottom, none of this would be necessary. The first version would Just Work™.
>
> [1] http://expectations.dub.pm

Also a simpler work around to your problem:

struct Expected(T, E) {
    private SumType!(T, E) data;
    @property T value() {
        return data.match!(
            (T val) => val,
            (E err) { throw new Exception(err.to!string); return T.init; }
        );
    }
}

Or depending on what you are doing even just using an if statement if you only have 2 types like that.
January 16, 2019
On Wednesday, 16 January 2019 at 23:32:22 UTC, luckoverthere wrote:
> On Wednesday, 16 January 2019 at 23:08:38 UTC, Paul Backus wrote:
>> Here's an example I ran into while working on my `expectations` library. [1]
>>
>> [...]
>
> As far as the DIP goes, it does not appear to make any mention of it working this way. It does say that the bottom type is implicitly convertible to any type, but being implicitly convertible does not mean that function types are also implicitly convertible. Just as float and int may be implicitly convertible, their functions are not. The only function types that appear to be implicitly convertible are those of class and their derived classes.
>
> This would mean that the bottom type function would need to be ABI compatible with every single function type. I'm not sure if that's possible, but I feel that would need to addressed in the DIP.

`match` is a template function whose return type is inferred from the lambdas passed to it (i.e., it returns `auto`).

If an auto-returning function has multiple return statements that return values of different types, then the function's return type is inferred to be the common type to which all of those different types are implicitly convertible. You can see this for yourself by compiling the following example:

auto fun(bool b) {
    if (b) return int.init;
    else return double.init;
}

import std.traits;
pragma(msg, ReturnType!fun); // double

It follows that if `match` is called with a lambda that returns Tbottom, and a lambda that returns T, its return type will be inferred to be T.
January 16, 2019
On Wednesday, 16 January 2019 at 23:40:03 UTC, luckoverthere wrote:
> Also a simpler work around to your problem:
>
> struct Expected(T, E) {
>     private SumType!(T, E) data;
>     @property T value() {
>         return data.match!(
>             (T val) => val,
>             (E err) { throw new Exception(err.to!string); return T.init; }
>         );
>     }
> }

Yes, this would also work. But having to insert a spurious return statement just to placate the type system is still unsatisfactory compared to the original version. In fact, it's the exact sort of special-case hackery that Walter expressed concern about in his post.
January 16, 2019
On Wed, Jan 16, 2019 at 02:32:33PM -0800, Walter Bright via Digitalmars-d wrote: [...]
> The trouble with 'void' as a bottom type is it is only half done. For example, 'void' functions are procedures, and certainly do return. A void* is certainly a pointer to something, not nothing. 'void' is the one with awkward corner cases, it's just we're so used to them we don't see them, like few C++ people recognized the identity function problem.
[...]

Actually, the *real* problem with `void` is that it has been overloaded to mean multiple, context-dependent things that are somewhat but not really the same, and aren't really compatible with each other.

1) When specified as a function return type, `void` signifies that the function does not return a meaningful value. In this sense, one might argue that it's a unit type (that requires no representation because it always holds the same value).

2) When `void` is specified as the type of a variable, it is an error, the justification being that you cannot have a variable that holds nothing.  However, this is incompatible with being a unit type: if it were *really* a unit type, we'd be allowed to declare as many variables as we want of that type. They would take up zero storage space (because there's no need to store a value that's the only value that can exist in that type), and every variable of this type would vacuously equal every other variable, because they can only hold the same value, so comparisons with == will always be vacuously true. You'd be able to pass it as a function parameter, though that'd be pretty useless since it conveys no information, and hence no code actually needs to be generated for it (there's no need to actually pass anything at the machine code level).  None of this is permitted for `void`, thereby making `void` not a unit type.  In fact, `void` in this usage seems closer to being a bottom type -- a type that holds no value, and therefore it makes no sense to instantiate a variable of that type.  So this isn't really consistent with (1).

3) The pointer type derived from `void`, that is, `void*`, has an ad hoc meaning that is not consistent with either (1) or (2): void* behaves like a top pointer type, i.e., every pointer implicitly casts to it. But that means that the `void` in `void*` does not mean the same thing as the `void` in (1) or (2) above, because if (1) were true, then it makes no sense to be able to construct a pointer to it. You cannot construct a pointer to nothing!  The closest thing you could get to constructing a pointer to nothing is null, however, typeof(null) != void* (even though null is implicitly convertible to void* due to void* behaving like a pointer to a top type).  Furthermore, if (2) were to hold, then `void*` couldn't possibly point to any other type, because a pointer to a unit type can only have one value (a "magic" pointer value that points to the non-physical storage location of the only value of the unit type).  So `void*` is inconsistent with (2).  What `void*` actually means is NOT a pointer to `void`, but rather a pointer to a top type that can represent any value.  However, even here, the language is not consistent: you cannot dereference a void* without casting it into something else first. If indeed it were a true top type, then dereferencing void* ought to give us a value of the top type (i.e., any value that can be represented in the language).  So (3) is not consistent with (1) and (2), and is closer to being a pointer to a top type, but not really because you can never dereference it.


So you see, `void` is a rats' nest of special cases and ad hoc monkey-patched semantics that's inconsistent with itself.  Sometimes it behaves like a unit type, sometimes it behaves like a bottom type, and sometimes it behaves like a top type, and none of these usages are consistent with each other, nor are they truly consistent with themselves either.  It's like working with a number system that has no concept of zero, negative numbers, or infinity, and then patching in a new number (let's call it X) that sometimes behaves like zero, sometimes like a negative number, and sometimes like infinity.  Good luck doing calculations involving X.

The only reason our brains haven't exploded yet from these inconsistencies is because we have become acclimatized to interpreting `void` in different ways depending on the context.  But because of that, trying to make sense of `void` as a consistent entity in itself is an exercise in futility.

And now we're proposing to add a Tbottom type to try to fix the missing corner cases in the type system... Just *how* is it going to interact with the existing inconsistent meanings of `void`?  I just can't wait to see the glorious chaos that will surely result. :-/


T

-- 
Genius may have its limitations, but stupidity is not thus handicapped. -- Elbert Hubbard
January 17, 2019
On Thursday, 17 January 2019 at 00:09:50 UTC, H. S. Teoh wrote:
> On Wed, Jan 16, 2019 at 02:32:33PM -0800, Walter Bright via Digitalmars-d wrote: [...]
>> [...]
> [...]
>
> Actually, the *real* problem with `void` is that it has been overloaded to mean multiple, context-dependent things that are somewhat but not really the same, and aren't really compatible with each other.
>
> [...]

A good summary of the issues with void. Maybe adding a bottom type enables some new clever semantics, but I would venture to guess that adding a real unit type would be even more helpful than a bottom type. I've had cases where being able to use void as a function parameter or a field would have made some of my templates much cleaner. Maybe we should focus on making void a real unit type before we try to add a bottom type?