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 next 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
prev sibling next sibling parent reply Ogion <ogion.art gmail.com> writes:
On Thursday, 26 February 2026 at 13:53:41 UTC, Richard (Rikki) 
Andrew Cattermole wrote:
 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.
We have a proposed tuple unpacking syntax[1] in this form: ```D Tuple!int result = tuple(99); (int value) = result; assert(value == 99); ``` We could use the same syntax for unwrapping optional types. This requires some additional rules: 1. If the type has a boolean value, it is checked before unpacking. If the check fails: 1.1. If the unpacking declaration is in `if` statement, execution takes the `else` branch (if exists). 1.2. If the unpacking declaration is in `foreach` statement, execution continues to the next iteration. 1.3. Otherwise, an Error is thrown. ```D mustuse struct Result(Type) { private { Type value; bool haveValue; } this(Type value) { this.value = value; this.haveValue = true; } bool opCast(T:bool)() => haveValue; (Type) = value; // TupleDeclarator (see DIP1053) } Result!int result = Result!int(99); (int x) = result; // throws an Error if `result` is false. if ((int y) = result) { assert(y == 99); } ``` Looping over Results: ```D foreach ((int x); getManyResults()) { writeln(x); } ``` It Just Works™: Result that holds a tuple: ```D Result!(Tuple!(int, string)) tupleResult = tuple(69, "nice"); if ((auto num, auto str) = tupleResult) { assert(num == 69); assert(str == "nice"); } ``` We could even apply the same syntax to pointers: ```D int[string] map = ["key": 69]; if ((int value) = "key" in map) { assert(value == 69); } int*[] arr = getManyPointers(); foreach ((int x); arr) { writeln(x); } /* Same as: foreach (p; arr) { if (p) { writeln(*p); } } */ ``` 1\. https://github.com/dlang/DIPs/blob/master/DIPs/accepted/DIP1053.md
Mar 12
next sibling parent "Richard (Rikki) Andrew Cattermole" <richard cattermole.co.nz> writes:
On 13/03/2026 3:18 AM, Ogion wrote:
 On Thursday, 26 February 2026 at 13:53:41 UTC, Richard (Rikki) Andrew 
 Cattermole wrote:
 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.
We have a proposed tuple unpacking syntax[1] in this form: ```D Tuple!int result = tuple(99); (int value) = result; assert(value == 99); ``` We could use the same syntax for unwrapping optional types. This requires some additional rules: 1. If the type has a boolean value, it is checked before unpacking. If the check fails: 1.1. If the unpacking declaration is in `if` statement, execution takes the `else` branch (if exists). 1.2. If the unpacking declaration is in `foreach` statement, execution continues to the next iteration. 1.3. Otherwise, an Error is thrown. ```D mustuse struct Result(Type) {     private {         Type value;         bool haveValue;     }     this(Type value) {         this.value = value;         this.haveValue = true;     }     bool opCast(T:bool)() => haveValue;     (Type) = value; // TupleDeclarator (see DIP1053)
DIP1053 did not provide this "TupleDeclarator". Right now that isn't a thing. I'm inclined to say that with ``opUnwrapIfTrue`` it could return a tuple, that then is supported in the condition. ``If`` statements in grammar have their own rules for var declarations so some more work is required for it to work.
Mar 12
prev sibling parent user1234 <user1234 12.de> writes:
On Thursday, 12 March 2026 at 14:18:00 UTC, Ogion wrote:
 On Thursday, 26 February 2026 at 13:53:41 UTC, Richard (Rikki) 
 Andrew Cattermole wrote:
 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.
I think that is not a good design. ``` assert(result); ``` Problems: 1. you are not sure the result is really checked ("-release" has for effect to disable assertions) 2. you dont create a branch for if the result is okay
Mar 20
prev sibling next sibling parent reply Walter Bright <newshound2 digitalmars.com> writes:
Thank you for taking the time to develop this.

This is a bit simpler and doesn't require language changes:
```d
struct Result(T)
{
     bool hasValue;
     T value;

     bool get(out T x)
     {
         if (hasValue)
         {
             x = value;
             return true;
         }
         return false;
     }
}

void bar(int);

void foo()
{
     Result!int r;
     int x;
     if (r.get(x)) { bar(x); }
}
```
It also can be embedded in an expression and other constructs:
```d
int x;
switch (r.get(x) ? x : x.init)
{
     ...
}
```

To channel Andrei, destroy!
Mar 13
next sibling parent reply "Richard (Rikki) Andrew Cattermole" <richard cattermole.co.nz> writes:
On 14/03/2026 8:14 AM, Walter Bright wrote:
 Thank you for taking the time to develop this.
 
 This is a bit simpler and doesn't require language changes:
 ```d
 struct Result(T)
 {
      bool hasValue;
      T value;
 
      bool get(out T x)
      {
          if (hasValue)
          {
              x = value;
              return true;
          }
          return false;
      }
 }
 
 void bar(int);
 
 void foo()
 {
      Result!int r;
      int x;
      if (r.get(x)) { bar(x); }
 }
 ```
 It also can be embedded in an expression and other constructs:
 ```d
 int x;
 switch (r.get(x) ? x : x.init)
 {
      ...
 }
 ```
 
 To channel Andrei, destroy!
This pattern is called try-parse idiom or what I'll refer to as tryGet, prior works section). However unsurprisingly they also have language support for this stuff based upon nullability but this is not unified unfortunately through standard and usage: https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/pattern-matching There are two primary scenarios we need to consider for: 1. The value is required, either we have a default value, or we'll throw an exception as its an error. ```d struct Result(T) { T get(lazy T default_); } void requiredInput(Result!int value) { int unwrapped = value.get(throw new Exception("...")); } void defaultInput(Result!int value = Result!int.init) { int unwrapped = value.get(0); } ``` 2. It is optional, or we want to do something different when not present. ```d if (!result.empty) { someObj.prop = result.front; } if (!result.empty) { int unwrapped = result.front; } else { // do something } ``` --- Th tryGet pattern only partially maps to the first scenario, and the second is the exact problem motivating me to see us have a solution, a get without the check. ```d void requiredInput(Result!int value) { if (!value) throw new Exception("..."); int got = value.unwrap; } void defaultInput(Result!int value = Result!int.init) { int got; if (value) got = value.unwrap; } ``` For this version of ``requiredInput`` if you've got complex control flow, that check may not have occurred, again a get without a check is possible. For the ``defaultInput`` if you forgot the check, after all nothing requires it! You'd have been free to write: ```d void defaultInput(Result!int value = Result!int.init) { got = value.unwrap; } ``` ---- There are secondary use cases for opUnwrapIfTrue that I can see you have not considered existing, here is the lowering for a ref variable: ```d if (Result!int __temp = result, __temp.opCast!bool) { scope(exit) __temp.destroy; ref int value = __temp.opUnwrapIfTrue(); } else { __temp.destroy; } ``` The variable ``__temp`` is pinned, it cannot be modified by the user directly, to do so you must go through the variable the user declared ``value``. This controls lifetime, and allows for the declared variable to take advantage of ref variables that you added not too long ago. To get this to work you now need _another_ method, using callbacks. ```d struct Result(T) { Result ifTrue(void delegate(ref T) del) { if (this) del(this.value); return this; } Result ifFalse(void delegate()); } ``` recommended in the context of DIP1000. It is not a good solution, introducing multiple symbols with patching just to workaround the lacking of a very simple transformation is not the kind of idea that brings joy. People complain about binary bloat, let's not increase it for something so common by recommending a bad pattern. Note: this causes real problems see October's quarterly meeting where Weka was maxing out a 31bit integer for their patching of symbols. --- Your switch example shows that you have not simplified the problem down correctly. ``switch (r.get(x) ? x : x.init)`` Should be: ``switch (r.get(int.init))``
Mar 13
parent reply Walter Bright <newshound2 digitalmars.com> writes:
On 3/13/2026 6:45 PM, Richard (Rikki) Andrew Cattermole wrote:
 Your switch example shows that you have not simplified the problem down
correctly.
 
 ``switch (r.get(x) ? x : x.init)``
 
 Should be:
 
 ``switch (r.get(int.init))``
I don't know what is incorrect about it.
Mar 13
parent "Richard (Rikki) Andrew Cattermole" <richard cattermole.co.nz> writes:
On 14/03/2026 3:41 PM, Walter Bright wrote:
 On 3/13/2026 6:45 PM, Richard (Rikki) Andrew Cattermole wrote:
 Your switch example shows that you have not simplified the problem 
 down correctly.

 ``switch (r.get(x) ? x : x.init)``

 Should be:

 ``switch (r.get(int.init))``
I don't know what is incorrect about it.
It is the wrong idiom for what is being archived, it is easily used wrong. My reply was all about why this idiom is not one I can recommend.
Mar 14
prev sibling parent monkyyy <crazymonkyyy gmail.com> writes:
On Friday, 13 March 2026 at 19:14:32 UTC, Walter Bright wrote:
     int x;
     if (r.get(x)) { bar(x); }
Its clearly the goal to avoid this and is not meta programming viable; youd be expecting end users to correctly use `is`
Mar 13
prev sibling parent Jonathan M Davis <newsgroup.d jmdavisprog.com> writes:
On Thursday, February 26, 2026 6:53:41 AM Mountain Daylight Time Richard
Andrew Cattermole (Rikki) via dip.development 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
 }
 ```
Well, ultimately, it's up to Walter and Atila to decide on this, but personally, I don't like the fact that it means that you have to look up the type of result to know what's going on here. Even worse, if you have something like if(auto value = foo()) {...} then what the code does is dependent on what foo returns, which is even less obvious, since there won't be a variable declaration within the function to at least know what the type being used is. You'd have to either be familiar with foo already or look it up - though with how often auto is used in D code, even with result, you'd probably have to look up the function that generated it to know what it's type is, since it was probably declared with something like auto result = foo(); So, in order to know the behavior of the if statement conditional, you're generally going to go look up definitions in other code. This is a case where the operator overload completely changes the semantics of what's going on, whereas generally speaking, operator overloads are there to add abilities to user-defined types which are in line with what those same operators do for built-in types. So, IMHO, the proposed an operator would reduce code clarity, and I think that if we add a feature like this that it should have explicit syntax which is distinct from a simple variable declaration so that it's clear that it's not the case that the variable is being checked for truthiness as would normally be the case. I confess I also don't see much utility in this, since I see no problem whatosever with code such as if(result.hasValue) { auto value = result.get; } And if anything, I'd probably just use result.get everywhere within the branch without bothering to declare a variable to copy it into. And if I were going to have syntatic sugar, I'd want the type to use pointer syntax so that it would be something like if(result) { auto value = *result; } which you can do right now (and I'd also probably just use *result everywhere in that case rather than copying it). The main reason behind the proposal seems to be that separating the hasValue and get calls runs the risk of get being called when hasValue is false, which is then a source of bugs, but personally, I don't recall the last time that I ever encountered a bug like that. It's not something that I would have considered error-prone. So, for myself at least, it seems like it's trying to solve a problem that isn't really a problem, though I can certainly believe that other folks code in a way that hits this issue more frequently (just like I really don't understand why so many folks get bent out of shape about dereferencing null pointers, since in my experience, that's a very rare problem and easily caught and fixed). So, it seems like a non-issue to me, but to be fair, there are probably a number of mistakes that I routinely make that plenty of other folks would think are non-issues. Either way, even if the separation of hasValue and get is a big enough problem to merit a language feature, I would like it to be something with clear syntax, not something that's invisible and requires knowing what type result is in code such as if(auto value = result) {} or what type foo returns in code such as if(auto value = foo()) {} in order to have any idea whether value is being checked for truthiness or whether the object that value is being taken from is being checked for truthiness. IMHO, the ambiguity is a problem, and the feature should have unambiguous syntax which does not look like existing code. And as others have pointed out, it's particularly bad when you consider cases such as if(int value = foo()) {} since right now, you can assume that value is non-zero within the if statement, whereas with the proposed operator, that would not necessarily be the case - which would be fine if the syntax were distinct, but it's not. Another issue would be that it would mean that generic code can no longer rely on if(auto value = foo()) {} checking whether cast(bool)value is true or not. I'm not sure that such checks are particularly common in generic code, since plenty of types cannot be cast to bool, but any generic code which works with types which can be cast to bool would then do something different with any types which implement the proposed operator. So, instead of implementing an overloaded operator making it so that a type can work with more code, it makes it so that code potentilaly has to check for this operator to get the correct behavior, or it will have to avoid declaring variables in if conditions in order to not trigger it. None of that would strictly speaking break existing code, since the operator would have to be added to an existing type to change its behavior, but it would mean that such types would potentially not work correctly with existing generic code, forcing such code to add additional template constraints and/or static if branches to work correctly with such types. And in the future if a struct which currently overloaded the cast operator to work with bool then had this new operator added to it, that would almost certainly break existing code, because any if conditions where the type would be used would suddenly change behavior (and might or might not result in compilation errors when the resulting variable was used). So, personally, I'm against the DIP. I think that unambiguous syntax is needed - syntax where when you look at it, you know that a wrapper type is being checked for truthiness and not the variable being declared. Also, Paul Backus has argued in discord that a generalized pattern matching solution would be better on the grounds that it should be able to solve this problem while providing more general value rather than just solving a fairly narrow problem, but I'm not familiar enough with what that would look like to really argue that one way or the other. So, I think that it's probably a good idea to consider that, but that's obviously a _much_ more involved discussion. - Jonathan M Davis
Mar 14