www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Built-in sumtypes, case and noreturn: why block enders should be

reply FeepingCreature <feepingcreature gmail.com> writes:
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; }
```
Oct 23 2022
next sibling parent reply ryuukk_ <ryuukk.dev gmail.com> writes:
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
Oct 24 2022
parent FeepingCreature <feepingcreature gmail.com> writes:
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.
Oct 24 2022
prev sibling next sibling parent reply rikki cattermole <rikki cattermole.co.nz> writes:
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)
Oct 24 2022
parent FeepingCreature <feepingcreature gmail.com> writes:
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`.
Oct 24 2022
prev sibling parent reply Dukc <ajieskola gmail.com> writes:
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: ```D 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.
Oct 25 2022
parent reply FeepingCreature <feepingcreature gmail.com> writes:
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.
Oct 25 2022
parent reply Dukc <ajieskola gmail.com> writes:
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 ```D extern(C) Throwable abort(); noreturn foo() nothrow { throw abort; } ``` ...which I believe currently works. Or is that a problem too?
Oct 25 2022
parent reply FeepingCreature <feepingcreature gmail.com> writes:
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 ```D 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.)
Oct 25 2022
parent Dukc <ajieskola gmail.com> writes:
On Wednesday, 26 October 2022 at 05:53:21 UTC, FeepingCreature 
wrote:
 ```D
 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: ```D 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 `Exception`s are `Throwable`s. 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.
Oct 26 2022