January 17, 2019
On Thursday, 17 January 2019 at 13:36:07 UTC, Nicholas Wilson wrote:
> The progression of this DIP throughout the stages of review, in spite of a lack of due diligence of research and response to criticisms, ignores the opportunity cost of reviewing higher impact and review ready DIPs, like the copy constructor or template constraint DIPs or any other DIP that is _actually ready_ for review.

Agreed.

But with all that said, it's still more constructive to be polite.

I wouldn't say any specific paragraph of your post was blatantly rude, condescending or uncalled for, but you could have made the same points while using a less aggressive, scathing tone, and I think a lot of discussions on the D Forums would benefit from that.
January 17, 2019
On Wednesday, 16 January 2019 at 22:32:33 UTC, Walter Bright wrote:
> 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!

Get Bartosz in here :#)
January 17, 2019
On Tuesday, 15 January 2019 at 08:59:07 UTC, Mike Parker wrote:
> DIP 1017, "Add Bottom Type", is now ready for Final Review. This is the last chance for community feedback before the DIP is handed off to Walter and Andrei for the Formal Assessment. Please read the procedures document for details on what is expected in this review stage:
>
> https://github.com/dlang/DIPs/blob/master/PROCEDURE.md#final-review
>
> The current revision of the DIP for this review is located here:
>
> https://github.com/dlang/DIPs/blob/4716033a8cbc2ee024bfad8942b2ff6b63521f63/DIPs/DIP1017.md
>
> In it you'll find a link to and summary of the previous review round. This round of review will continue until 11:59 pm ET on January 30 unless I call it off before then.
>
> Thanks in advance for your participation.

An advantage of the bottom type over an attribute is, that it is part of the function type. A library allowing to set a custom error handler could specify that the error handler must not return:
alias ErrorHandler = TBottom function(string msg);

Here is another example for TBottom* == typeof(null): If a library type allows custom user data with a template and you don't need, you could specify TBottom.
struct LibraryType(UserData)
{
    // ...
    UserData* userData;
}
Since the compiler knows, that userData is always null, it could eliminate dead code. Note that it would not be possible to specify typeof(null) as the template parameter of LibraryType, because then userData would be of type typeof(null)*.

The rules TBottom* == typeof(null) and TBottom[] == typeof([]) can also be generalized to custom types:
struct CustomArray(T)
{
    T[] data;
    void opAssign(T2)(CustomArray!T2 a2) if(is(T2:T))
    {
        data.length = a2.data.length;
        foreach(i, x; a2.data)
            data[i] = x;
    }
}
auto literal(T...)(T params)
{
    import std.traits;
    static if(params.length == 0)
        return CustomArray!TBottom();
    else
        return CustomArray!(CommonType!T)([params]);
}
unittest
{
    CustomArray!int a = literal();
}
This could be simplified if CommonType!() == TBottom holds.
Note that this could also be implemented by replacing CustomArray!TBottom with a special EmptyCustomArray type.

January 17, 2019
On Thursday, 17 January 2019 at 13:15:03 UTC, H. S. Teoh wrote:
> On Thu, Jan 17, 2019 at 10:35:00AM +0000, Dukc via Digitalmars-d wrote: [...]
>> -Where no type is convertible from `typeof(assert(0))`, any type could
>> be made convertible from `void`, the only possible value of void
>> converting to target type's `.init` value.
>
> I think you got the conversion direction mixed up.  If typeof(assert(0))
> is supposed to be the bottom type, then it must implicitly convert to
> every other type (as Walter's DIP stipulates), but no other type could
> implicitly convert to it.

Oh yes. Stratch that.


January 17, 2019
On Thursday, 17 January 2019 at 13:20:20 UTC, Olivier FAURE wrote:
> On Thursday, 17 January 2019 at 12:40:18 UTC, H. S. Teoh wrote:
>> No! A unit type is one inhabited by a single value. A top type is a type that can represent *every* value.  Very crudely speaking, if types were numbers, then a unit type is 1, a bottom type is 0, and a top type is infinity. Or, to use a set analogy, a unit type corresponds with a singleton set, a bottom type to the empty set (the subtype of every type), and the top type to the universal set (the supertype of every type).
>
> I think I'm confused.
>
> Actually, I think I have a pretty good question. Let's imagine we implement two types in D, Unit and Top, so that they match their type theory concepts as best as they can.
>
> Given this code:
>
>     int foo(Unit u) {
>         ...
>     }
>
>     int bar(Top t) {
>         ...
>     }
>
> what could you do in `bar` that you couldn't do in `foo`?
>
> (I'd appreciate if you could answer with pseudocode examples, to help communication)

It's the other way around--there are things you can do in `foo` that you can't do in `bar`.

For example, you can compare two instances of Unit using `==` (and in fact such a comparison will always evaluate to true, since all instances of Unit have, by definition, the same value). However, you cannot compare two instances of Top using `==`, because there are types in D that have `@disable opEquals`, and Top is a supertype of those types.
January 17, 2019
On Thu, 17 Jan 2019 13:20:20 +0000, Olivier FAURE wrote:
> Actually, I think I have a pretty good question. Let's imagine we implement two types in D, Unit and Top, so that they match their type theory concepts as best as they can.
> 
> Given this code:
> 
>      int foo(Unit u) {
>          ...
>      }
> 
>      int bar(Top t) {
>          ...
>      }
> 
> what could you do in `bar` that you couldn't do in `foo`?

bar knows nothing about the value passed in. It's pretty much equivalent to taking a Variant. Or, if you're only dealing with OOP, it's like taking Object. It could do anything with that value, but it has to figure out what type it's dealing with first, then down-cast it.

foo knows the exact value that will be passed to it because there's only one possibility. It can't do anything with that value at runtime that it couldn't do at compile-time, so the whole function could be reduced to a constant (assuming it's pure), or the argument could be omitted. Kind of like taking a CheckedInt where min and max are both 0.
January 17, 2019
On Thu, Jan 17, 2019 at 01:20:20PM +0000, Olivier FAURE via Digitalmars-d wrote: [...]
> I think I'm confused.
> 
> Actually, I think I have a pretty good question. Let's imagine we implement two types in D, Unit and Top, so that they match their type theory concepts as best as they can.
> 
> Given this code:
> 
>     int foo(Unit u) {
>         ...
>     }
> 
>     int bar(Top t) {
>         ...
>     }
> 
> what could you do in `bar` that you couldn't do in `foo`?
> 
> (I'd appreciate if you could answer with pseudocode examples, to help
> communication)

For one thing, Top would be the supertype of every other type, so you could pass in values of any type to bar:

	bar(123);
	bar("abc");
	bar(null);
	bar(Unit.init);

Whereas the only type that foo will accept is the unique value of Unit:

	foo(Unit.init);

You can't pass anything else to foo, because the only value it accepts is the unique value of Unit, and nothing else.

As to what you can *do* inside the functions: since Unit has only one value, every operation on it is vacuous:

	int foo(Unit u) {
		assert(u == u); // always true
		auto v = u;	// basically no-op
		assert(is(typeof(v) == Unit));
		...
		return 0;	// N.B.: cannot return u because Unit
				// does not convert to int (Unit is not
				// a subtype of int).
	}

With Top, the value can literally be *any* type, which means there's also very little you can actually do with it, because you cannot assume anything about it -- the only assumptions you can make are those that are true for *all* types in the language.  You could take its address, or assign a new value to it (of any type):

	int bar(Top t) {
		Top* p = &t;
		t = 123;
		t = "abc";
		t = null;
		t = Unit.init;
		...
		return 0;	// again, cannot return t, because Top is not
				// a subtype of int (int is a subtype of
				// Top, though, but it doesn't go the
				// other way)
	}

But as to actually operating on the value of t, there's really not much that can be done, since it encompasses too much, and there are very few operations that are common to *all* types.

A Top* would be the same as today's void*, in that any pointer implicitly converts to it:

	int i = 123;
	float x = 1.0;
	string s = "abc";
	Unit u;
	Top* ptr;

	ptr = &i;
	ptr = &x;
	ptr = &s;
	ptr = &u;

Dereferencing ptr would give you a value of type Top, but then, barring further language support, you wouldn't know which underlying type it is, so there wouldn't be very much you could meaningfully do with it. This means that you can't meaningfully assign anything via ptr, for example:

	*ptr = 123; // error: not every type supports opAssign(int)
	*ptr = "abc"; // error: not every type supports opAssign(string)
	... // etc.

Which ultimately makes sense, because we could have gotten ptr from taking the address of a float, for example, and it would be UB to assign a string to *ptr.

You could cast the Top* to something else, but that gets into the realm of implementation-defined behaviour (it could be just UB), and no longer something under the auspices of type theory.

Basically, Top behaves more-or-less like std.variant.Variant, maybe excepting some corner cases.


T

-- 
People walk. Computers run.
January 17, 2019
On Thu, 17 Jan 2019 08:27:36 +0000, FeepingCreature wrote:
> On Wednesday, 16 January 2019 at 08:47:48 UTC, Johannes Loher wrote:
>> By the way, Kotlin does basically the same thing. Kotlin does not have pointers, but it has optional types and the type `Nothing?` is a type which can hold exactly one value: `null`. This also means that it is basically a unit type. While it is not actually Kotlin's `Unit` type, it could have been. Similarly, should we decide to go down that road and add proper top, bottom and unit types, we could define
>>
>> ```
>> alias Tbottom = typeof(assert(0));
>> alias void = Tbottom*;
>> ```
> 
> That is amazing. I love it.

Unfortunately, it would mean void.sizeof is (void*).sizeof and incrementing a void* would increase it by four or eight instead of one, which would be a breaking change.

If D had distinct unit and raw-memory types, then that would be a valid way to define unit, but it would reserve four or eight bytes per variable instead of zero, which is suboptimal.
January 17, 2019
On Thu, 17 Jan 2019 17:17:45 +0000, Tim wrote:
> An advantage of the bottom type over an attribute is, that it is part of
> the function type. A library allowing to set a custom error handler
> could specify that the error handler must not return:
> alias ErrorHandler = TBottom function(string msg);

It was unclear to me, however, whether you could implicitly convert a TBottom function(string msg) to an int function(string msg). It's much more common to have a bunch of handlers, most of which should complete properly and only one of which aborts the program every time.
January 17, 2019
On Thu, Jan 17, 2019 at 02:39:18AM -0800, H. S. Teoh via Digitalmars-d wrote:
> On Thu, Jan 17, 2019 at 02:59:09AM +0000, Meta via Digitalmars-d wrote: [...]
> > As an aside, the empty enum:
> > 
> > enum Empty
> > {
> > }
> > 
> > Is an uninhabited type similar to Bottom, but is not implicitly convertible to any other type, unlike Bottom.
> 
> Unfortunately, you get a compile error for this declaration.
> 
> But assuming it were allowed, then it would be possible to declare multiple distinct empty enums that do not interconvert with each other, which would mean that there is not one, but arbitrarily many bottom types of this kind.

Further thoughts on empty enums in D: according to the spec, an enum always has an underlying base type, the default of which is int.  So an empty enum as declared above, if we were to hypothetically allow it, would be a subtype of int (a bottom int subtype).  Which also means we can declare:

	enum BottomStr : string { }

which, AFAIK, would be treated as something distinct from the bottom int type.  So it would represent a different bottom type.

You'd need a "true" bottom type to get the direct equivalent of the Bottom of type theory.

Of course, we could stipulate that all empty enums are interpreted as Tbottom, which would clear up this mess -- but this is yet another area where the DIP in question failed to address.


> > The empty struct:
> > 
> > struct Unit
> > {
> > }
> > 
> > Unit u;
> > writeln(u); // Prints Unit()
> 
> Oddly enough, Unit.sizeof == 1, as a hack for generating distinct addresses when you declare multiple instances of Unit. One would have expected .sizeof == 0 for a unit type (and NaN or an error if you attempted to take .sizeof of an empty enum, if empty enums were allowed).
[...]

And furthermore, as somebody has already pointed out, Unit as declared above would be distinct from empty structs of any other name that you might declare.  So there'd be multiple incompatible unit types. Which again is a mess, since two functions that don't return values ("procedures") could ostensibly return distinct unit types (let's say one returns void and the other returns Unit, or one returns Unit and the other returns another empty struct of a different name), and you'd have a messy incompatibility situation.


T

-- 
Give a man a fish, and he eats once. Teach a man to fish, and he will sit forever.