www.digitalmars.com         C & C++   DMDScript  

digitalmars.dip.development - First Draft: opUnwrapIfTrue

reply Richard (Rikki) Andrew Cattermole <richard cattermole.co.nz> writes:
The DIP: 
https://gist.github.com/rikkimax/21242118e3bc1bf5f28024c2cdc33557

A new operator overload to allow if statements to automatically 
unwrap result types and ensure that the check is called prior to, 
similar to foreach statements support of ranges.

This solves a prolific issue that I have had to deal with result 
types and has been exhibited by ``Nullable`` in PhobosV2.

Full example:

```d
import core.attribute : mustuse;

 mustuse
struct Result(Type) {
     private {
         Type value;
         bool haveValue;
     }

     this(Type value) {
         this.value = value;
         this.haveValue = true;
     }

     bool opCast(T:bool)() => haveValue;

     Type opUnwrapIfTrue() {
         assert(haveValue);
         return value;
     }
}

Result!int result = Result!int(99);

if (int value = result) {
     // got a value!
     assert(value == 99);
} else {
     // oh noes an error or default init state
}
```
Feb 26
next sibling parent reply Juraj <junk vec4.xyz> writes:
On Thursday, 26 February 2026 at 13:53:41 UTC, Richard (Rikki) 
Andrew Cattermole wrote:
 The DIP: 
 https://gist.github.com/rikkimax/21242118e3bc1bf5f28024c2cdc33557

 A new operator overload to allow if statements to automatically 
 unwrap result types and ensure that the check is called prior 
 to, similar to foreach statements support of ranges.

 This solves a prolific issue that I have had to deal with 
 result types and has been exhibited by ``Nullable`` in PhobosV2.

 Full example:

 ```d
 import core.attribute : mustuse;

  mustuse
 struct Result(Type) {
     private {
         Type value;
         bool haveValue;
     }

     this(Type value) {
         this.value = value;
         this.haveValue = true;
     }

     bool opCast(T:bool)() => haveValue;

     Type opUnwrapIfTrue() {
         assert(haveValue);
         return value;
     }
 }

 Result!int result = Result!int(99);

 if (int value = result) {
     // got a value!
     assert(value == 99);
 } else {
     // oh noes an error or default init state
 }
 ```
Feels like this will be hard to understand (I have to check `Result` for this op), I would much rather see movement on something like this: <https://github.com/dlang/dmd/issues/20645> Allowing this pattern: ```d if (int value; result.tryGetValue(value)) { ... } ``` IIRC, C++ has this feature as **if statements with initializer**
Feb 26
next sibling parent "Richard (Rikki) Andrew Cattermole" <richard cattermole.co.nz> writes:
On 27/02/2026 4:28 AM, Juraj wrote:
 Feels like this will be hard to understand (I have to check `Result` for 
 this op), I would much rather see movement on something like this:
 <https://github.com/dlang/dmd/issues/20645>
 
 Allowing this pattern:
 
 ```d
 if (int value; result.tryGetValue(value)) {
    ...
 }
 ```
 
 IIRC, C++ has this feature as **if statements with initializer**
We talked a bit on Discord, and the point I've made is that this is a different feature. Same with matching for sum types. They will coexist with this feature, like they do in Rust and Swift.
Feb 26
prev sibling parent Kagamin <spam here.lot> writes:

```
if (result.tryGetValue(out int value)) {
   //use value
}
```
Mar 02
prev sibling next sibling parent Paul Backus <snarwin gmail.com> writes:
On Thursday, 26 February 2026 at 13:53:41 UTC, Richard (Rikki) 
Andrew Cattermole wrote:
 The DIP: 
 https://gist.github.com/rikkimax/21242118e3bc1bf5f28024c2cdc33557
The "Rationale" section begins as follows:
 In the authors codebase is a result type, with support for an 
 error. It goes to an extreme extent to require a check for a 
 given value of the result type to have been checked using 
 `opCast!bool`, however this is very easy to miss when you do 
 the `get` call which results in a runtime error. This was 
 exhibited over many years period as being problematic.
This seems like an entirely self-inflicted problem. You've designed an API that's a pain in the butt to use, and now you want to add a language feature to compensate for your bad design. A better solution would be to redesign your API to combine the check and the access into a single operation, either with a higher-order function like `Nullable`'s [`apply`][1], or with an `opApply` overload: ```d struct Result(T) { private T value; private bool hasValue; this(T value) { this.value = value; this.hasValue = true; } int opApply(Body)(scope Body body) { if (this.hasValue) return body(this.value); return 0; } } Result!int div(int n, int m) { if (m) return Result!int(n / m); else return Result!int.init; } void main() { import std.stdio; // prints 5 foreach (int result; div(10, 2)) writeln(result); // prints nothing foreach (int result; div(1, 0)) writeln(result); } ``` [1]: https://dlang.org/phobos/std_typecons.html#apply
Feb 26
prev sibling next sibling parent reply Dennis <dkorpel gmail.com> writes:
On Thursday, 26 February 2026 at 13:53:41 UTC, Richard (Rikki) 
Andrew Cattermole wrote:
 ```D
 if (int value = result) {
     // got a value!
     assert(value == 99);
 } else {
     // oh noes an error or default init state
 }
 ```
So after this DIP, the value in the if-condition can now be falsey. ```D if (int value = result) { // value can be 0 here assert(value); // can fail! } ``` This looks very confusing to me. Note that Rust's `if let Some(value) = result ` doesn't have this problem because the `Some()` makes it explicit that unwrapping is going on. (Also int/i32 doesn't implicitly convert to bool there). You list range primitives as prior work, but notably they require the dedicated foreach keyword instead of being an invisible overload of the old `for` statement. I think if we add unwrapping or pattern matching to D, it should have its own syntax.
Feb 26
parent Meta <jared771 gmail.com> writes:
On Thursday, 26 February 2026 at 18:03:31 UTC, Dennis wrote:
 On Thursday, 26 February 2026 at 13:53:41 UTC, Richard (Rikki) 
 Andrew Cattermole wrote:
 ```D
 if (int value = result) {
     // got a value!
     assert(value == 99);
 } else {
     // oh noes an error or default init state
 }
 ```
So after this DIP, the value in the if-condition can now be falsey. ```D if (int value = result) { // value can be 0 here assert(value); // can fail! } ``` This looks very confusing to me. Note that Rust's `if let Some(value) = result ` doesn't have this problem because the `Some()` makes it explicit that unwrapping is going on. (Also int/i32 doesn't implicitly convert to bool there). You list range primitives as prior work, but notably they require the dedicated foreach keyword instead of being an invisible overload of the old `for` statement. I think if we add unwrapping or pattern matching to D, it should have its own syntax.
Agreed; D already has limited pattern matching in template type lists and is-expressions anyway. If we're going to do something like this DIP, we should just instead add a new form of if statement or add pattern matching. There was a DIP years ago I think that proposed a very simple mechanism for adding custom unpacking functionality to types.
Feb 26
prev sibling next sibling parent monkyyy <crazymonkyyy gmail.com> writes:
On Thursday, 26 February 2026 at 13:53:41 UTC, Richard (Rikki) 
Andrew Cattermole wrote:
 The DIP: 
 https://gist.github.com/rikkimax/21242118e3bc1bf5f28024c2cdc33557

 A new operator overload to allow if statements to automatically 
 unwrap result types and ensure that the check is called prior 
 to, similar to foreach statements support of ranges.
So, one narrow opOverload, for one narrow type, for one narrow usage pattern? Id suggest `opLeftAssign` that lets nullable!int define how int parses the assignment would be a more general feature
Feb 26
prev sibling parent reply Per =?UTF-8?B?Tm9yZGzDtnc=?= <per.nordlow gmail.com> writes:
On Thursday, 26 February 2026 at 13:53:41 UTC, Richard (Rikki) 
Andrew Cattermole wrote:
 The DIP: 
 https://gist.github.com/rikkimax/21242118e3bc1bf5f28024c2cdc33557
Nice. Regarding the lowering ```d if (Result!int result2 = result, result2.opCast!bool) { scope(exit) result2.destroy; ... } else { result2.destroy; } ``` are ```d scope(exit) result2.destroy; result2.destroy; ``` eagerly elided when `__traits(needsDestruction, typeof(result2))` is false or are the injected the lower regardless and then later elided? Just checking to make sure the lowering doesn't inject AST unneccesary nodes.
Mar 04
parent "Richard (Rikki) Andrew Cattermole" <richard cattermole.co.nz> writes:
On 04/03/2026 9:50 PM, Per Nordlöw wrote:
 On Thursday, 26 February 2026 at 13:53:41 UTC, Richard (Rikki) Andrew 
 Cattermole wrote:
 The DIP: https://gist.github.com/ 
 rikkimax/21242118e3bc1bf5f28024c2cdc33557
Nice. Regarding the lowering ```d if (Result!int result2 = result, result2.opCast!bool) {     scope(exit) result2.destroy;     ... } else {     result2.destroy; } ``` are ```d     scope(exit) result2.destroy;     result2.destroy; ``` eagerly elided when `__traits(needsDestruction, typeof(result2))` is false or are the injected the lower regardless and then later elided? Just checking to make sure the lowering doesn't inject AST unneccesary nodes.
I didn't add this particular bit of code, its already present and operational in dmd. I've tried to make the design and implementation very minimal, and I have succeeded at this! See PR. It does check if dtor is needed.
Mar 04