Thread overview
Built-in sumtypes, case and noreturn: why block enders should be expressions.
Oct 24, 2022
FeepingCreature
Oct 24, 2022
ryuukk_
Oct 25, 2022
FeepingCreature
Oct 24, 2022
rikki cattermole
Oct 25, 2022
FeepingCreature
Oct 25, 2022
Dukc
Oct 25, 2022
FeepingCreature
Oct 25, 2022
Dukc
Oct 26, 2022
FeepingCreature
Oct 26, 2022
Dukc
October 24, 2022

I finally added bottom types to neat (https://neat-lang.github.io , but it's not on a released tag yet). For context, bottom types are called noreturn in D. The main motivation for me was that it enables a very neat idiom for built-in sumtypes. Since someone raised the idea of making sumtypes built-in to Walter, I want to preemptively explain how this works:

(int | string) foo;
...
int i = foo.case(int i: i, string s: return null);

Now, this used to be a built-in syntax. It seems intuitively obvious that you should be able to discard a branch of the case by just returning out of the function entirely. But the way that it actually works now, is that all block enders, ie. return, break and continue, instead of statements are just - expressions of type bottom. And bottom unifies with every type :) so these expressions don't add any type of their own to the case expression type - except if they are the only expression in the case, in which case the type of the whole case expression is bottom, as it should be.

The reason I love this so much is because it's an easy way in the typesystem to capture our intuition about these statements; which is that we can ignore them because they don't ever evaluate to an actual value. And in one stroke, it also enables other idioms, like foo.case(string s: exit(1)), which we would expect to work the same, for the same reason.

So, you know, we already have noreturn, so if you ever implement built-in sumtypes in D, keep this in mind. :)

PS: hilariously, with this change the following is entirely valid code:

int foo() { return return return 3; }
October 24, 2022

On Monday, 24 October 2022 at 06:20:15 UTC, FeepingCreature wrote:

>

I finally added bottom types to neat (https://neat-lang.github.io , but it's not on a released tag yet). For context, bottom types are called noreturn in D. The main motivation for me was that it enables a very neat idiom for built-in sumtypes. Since someone raised the idea of making sumtypes built-in to Walter, I want to preemptively explain how this works:

(int | string) foo;
...
int i = foo.case(int i: i, string s: return null);

Now, this used to be a built-in syntax. It seems intuitively obvious that you should be able to discard a branch of the case by just returning out of the function entirely. But the way that it actually works now, is that all block enders, ie. return, break and continue, instead of statements are just - expressions of type bottom. And bottom unifies with every type :) so these expressions don't add any type of their own to the case expression type - except if they are the only expression in the case, in which case the type of the whole case expression is bottom, as it should be.

The reason I love this so much is because it's an easy way in the typesystem to capture our intuition about these statements; which is that we can ignore them because they don't ever evaluate to an actual value. And in one stroke, it also enables other idioms, like foo.case(string s: exit(1)), which we would expect to work the same, for the same reason.

So, you know, we already have noreturn, so if you ever implement built-in sumtypes in D, keep this in mind. :)

PS: hilariously, with this change the following is entirely valid code:

int foo() { return return return 3; }

I'm personally not a fan of this syntax at all, in fact i'm not sure how to properly ready it, the one on the twitter post i linked in that thread is more interesting to me as it is consistent with everything else in the language

https://twitter.com/seanbax/status/1583654796791140352

It's no different than using a Struct, a function, or a switch, it blends perfectly in the language, that should be the goal with D

What ever difficulty in the compiler side of things is, it shouldn't be an excuse

End user experience and tooling should be taken into account

October 25, 2022
On 24/10/2022 7:20 PM, FeepingCreature wrote:
> 
> ```
> (int | string) foo;
> ...
> int i = foo.case(int i: i, string s: return null);
> ```

That is kinda brilliant.

The first case sets what actually gets returned the others all return nothing and are inherited from it.

I assume if the other cases get hit that it sets to 0? (unless of course its boxed)
October 25, 2022
On Monday, 24 October 2022 at 17:45:17 UTC, rikki cattermole wrote:
> On 24/10/2022 7:20 PM, FeepingCreature wrote:
>> 
>> ```
>> (int | string) foo;
>> ...
>> int i = foo.case(int i: i, string s: return null);
>> ```
>
> That is kinda brilliant.
>
> The first case sets what actually gets returned the others all return nothing and are inherited from it.
>
> I assume if the other cases get hit that it sets to 0? (unless of course its boxed)

Well, it doesn't set it to anything, that's sort of the point. :) The other cases are `bottom`, which is to say they implconv to `int`, but only under the assurance that it'll never come up because any control flow that leaves `return null` is unreachable anyways. If anything, I'd put `assert(false)` in there, just to be safe.

Also, the order doesn't actually matter. `foo.case(string s: return null, int i: i)` would also work, because `bottom | int = int | bottom = int`.
October 25, 2022

On Monday, 24 October 2022 at 17:32:51 UTC, ryuukk_ wrote:

>

I'm personally not a fan of this syntax at all

The syntax is irrelevant. That's just how it looks in my language at the moment. The important part of the post is the semantics of noreturn cases.

October 25, 2022

On Monday, 24 October 2022 at 06:20:15 UTC, FeepingCreature wrote:

>

But the way that it actually works now, is that all block enders, ie. return, break and continue, instead of statements are just - expressions of type bottom.

You forgot the old good goto 😉.

>

PS: hilariously, with this change the following is entirely valid code:

int foo() { return return return 3; }

Not any more hilariously than that this should maybe compile according to current rules:

@safe void main()
{ throw throw throw new Exception("hello");
}

It does not though. The implementation does not currently convert the bottom type to throwable. If this is allowed, should it be allowed in nothrow? What about @safe? I tend to think that yes but not sure.

October 25, 2022

On Tuesday, 25 October 2022 at 10:04:50 UTC, Dukc wrote:

>

It does not though. The implementation does not currently convert the bottom type to throwable. If this is allowed, should it be allowed in nothrow? What about @safe? I tend to think that yes but not sure.

Wow.

extern(C) noreturn abort();
noreturn foo() nothrow { throw abort; }

That is truly evil.

October 25, 2022

On Tuesday, 25 October 2022 at 13:12:22 UTC, FeepingCreature wrote:

>

On Tuesday, 25 October 2022 at 10:04:50 UTC, Dukc wrote:

>

It does not though. The implementation does not currently convert the bottom type to throwable. If this is allowed, should it be allowed in nothrow? What about @safe? I tend to think that yes but not sure.

Wow.

extern(C) noreturn abort();
noreturn foo() nothrow { throw abort; }

That is truly evil.

Why it'd be evil? It aborts with the C function, it doesn't actually throw.

No more of a problem than

extern(C) Throwable abort();
noreturn foo() nothrow { throw abort; }

...which I believe currently works. Or is that a problem too?

October 26, 2022

On Tuesday, 25 October 2022 at 13:31:48 UTC, Dukc wrote:

>

On Tuesday, 25 October 2022 at 13:12:22 UTC, FeepingCreature wrote:

>

On Tuesday, 25 October 2022 at 10:04:50 UTC, Dukc wrote:

>

It does not though. The implementation does not currently convert the bottom type to throwable. If this is allowed, should it be allowed in nothrow? What about @safe? I tend to think that yes but not sure.

Wow.

extern(C) noreturn abort();
noreturn foo() nothrow { throw abort; }

That is truly evil.

Why it'd be evil? It aborts with the C function, it doesn't actually throw.

No more of a problem than

extern(C) Throwable abort();
noreturn foo() nothrow { throw abort; }

...which I believe currently works. Or is that a problem too?

That doesn't work. How would it? D has no idea that abort can never return if it's not typed noreturn. (Neither works, to be clear.)

October 26, 2022

On Wednesday, 26 October 2022 at 05:53:21 UTC, FeepingCreature wrote:

> >
extern(C) Throwable abort();
noreturn foo() nothrow { throw abort; }

...which I believe currently works. Or is that a problem too?

That doesn't work. How would it? D has no idea that abort can never return if it's not typed noreturn. (Neither works, to be clear.)

Just tested. Yes, it doesn't work. This does, though:

extern(C) Error abort();
noreturn foo() nothrow { throw abort; }

It works because a throw statement is always of type noreturn, no matter what is thrown. If abort indeed aborts, fine, obviously we don't return. However, if it did return an Error, we'd then throw that, which also prevents doing anything with "result" of throw. This works even in nothrow because we're throwing an unrecoverable error, not an exception. This is also why the snippet with Throwable does not compile - abort might potentially return an Exception, because Exceptions are Throwables. With an Error return type, that's not possible.

It makes an interesting question whether throw throw something (or throw assert(0)) should be allowed, and if, should it be @safe and/or nothrow? You could argue that the bottom type converts to Exception and thus you can't do it in nothrow, but can in @safe. Equally you could say the bottom type converts to Error, so throwing it should be ok in nothrow but not in @safe. Or you could say that since the execution will never actually reach where a bottom type is instantiated, it's ok to "throw" it anywhere.