1 /** Wrapper type for a sum-type (union) of an unexpected and expected value.
2  */
3 module nxt.expected;
4 
5 import std.traits : isInstanceOf;
6 
7 /** Wrapper type for an unexpected value of type `E`.
8  */
9 private struct Unexpected(E)
10 {
11     E value;
12     alias value this;
13 }
14 
15 /** Union (sum) type of either an expected (most probable) value of type `T` or
16  * an unexpected value of type `E` (being an instance of type `Unexpected!E`).
17  *
18  * `E` is typically an error code (for instance C/C++'s' `errno` int) or a
19  * subclass of `Exception` (which is the default).
20  *
21  * See_Also: https://www.youtube.com/watch?v=nVzgkepAg5Y
22  * See_Also: https://github.com/dlang/phobos/pull/6665
23  * See_Also: https://code.dlang.org/packages/expectations
24  * See_Also: https://doc.rust-lang.org/std/result/
25  * See_Also: https://github.com/tchaloupka/expected
26  *
27  * TODO https://dlang.org/phobos/std_typecons.html#.apply
28  *
29  * TODO I'm not convinced about the naming
30  * - `Expected`: instead call it something that tells us that it can be either expected or unexpected?
31  * - `Unexpected`: if so why shouldn't we have a similar value wrapper `Expected`?
32  *
33  * TODO we could get around the `Unexpected` wrapper logic by instead expressing
34  * construction in static constructor functions, say,:
35  * - static typeof(this) fromExpectedValue(T expectedValue)
36  * - static typeof(this) fromUnexpectedValue(E unexpectedValue)
37  *
38  * TODO swap
39  *
40  * TODO which functions should be `nothrow`?
41  *
42  * TODO later on: remove _ok when `_expectedValue` and ` _unexpectedValue` can store this state
43  * "collectively" for instance when both are pointers or classes (use trait
44  * `isAddress`)
45  *
46  * TODO ok to default `E` to `Exception`?
47  */
48 struct Expected(T, E = Exception)
49 if (!isInstanceOf!(Unexpected, T) && // an `Unexpected` cannot be `Expected` :)
50     !is(T == void)) // disallow void for now, for ref see https://forum.dlang.org/post/ncjhsxshttikzjqgiwev@forum.dlang.org
51 {
52     import core.lifetime : moveEmplace;
53     import nxt.container_traits : isAddress;
54 
55     // TODO ok for default construction to initialize
56     // - _expectedValue = T.init (zeros)
57     // - _ok = true (better to have _isError so default is zero bits here aswell?)
58 
59     /// Construct from expected value `expectedValue.`
60     this()(auto ref T expectedValue) @trusted
61     {
62         // TODO reuse opAssign?
63         moveEmplace(expectedValue, _expectedValue);
64         _ok = true;
65     }
66 
67     /// Construct from unexpected value `unexpectedValue.`
68     this(Unexpected!E unexpectedValue) @trusted
69     {
70         // TODO reuse opAssign?
71         moveEmplace(unexpectedValue, _unexpectedValue);
72         _ok = false;
73     }
74 
75     /// Assign from expected value `expectedValue.`
76     void opAssign()(auto ref T expectedValue) @trusted
77     {
78         // TODO is this ok?:
79         clear();
80         moveEmplace(expectedValue, _expectedValue);
81         _ok = true;
82     }
83 
84     /// Assign from unexpected value `unexpectedValue.`
85     void opAssign(E unexpectedValue) @trusted
86     {
87         clear();
88         moveEmplace(unexpectedValue, _unexpectedValue);
89         _ok = false;
90     }
91 
92     /// Clear (empty) contents.
93     private void clear() @trusted
94     {
95         release();
96         static if (isAddress!T)
97         {
98             _expectedValue = null;
99         }
100     }
101 
102     /// Release any memory used to store contents.
103     private void release() @trusted
104     {
105         import core.internal.traits : hasElaborateDestructor;
106         if (hasExpectedValue)
107         {
108             static if (!is(T == class))
109             {
110                 static if (hasElaborateDestructor!T)
111                 {
112                     destroy(_expectedValue);
113                 }
114             }
115             _ok = false;
116         }
117         else
118         {
119             static if (!is(E == class))
120             {
121                 static if (hasElaborateDestructor!E)
122                 {
123                     destroy(_unexpectedValue);
124                 }
125             }
126             destroy(_unexpectedValue);
127             // TODO change _ok?
128         }
129     }
130 
131     /** Is `true` iff this has a expectedValue of type `T`. */
132     bool hasExpectedValue() const
133     {
134         return _ok;
135     }
136 
137     import std.traits : CommonType;
138 
139     /** Get current value if any or call function `elseWorkFun` with compatible return value.
140      *
141      * TODO is this anywhere near what we want?
142      */
143     CommonType!(T, typeof(elseWorkFun())) valueOr(alias elseWorkFun)() const
144     if (is(CommonType!(T, typeof(elseWorkFun()))))
145     {
146         if (hasExpectedValue)
147         {
148             return expectedValue;
149         }
150         else
151         {
152             return elseWorkFun(); // TODO is this correct
153         }
154     }
155 
156     import std.functional : unaryFun;
157 
158     /** If `this` is an expected value (of type `T`) apply `fun` on it and
159      * return result, otherwise return current unexpected value (of type `E`).
160      *
161      * See_Also: https://dlang.org/phobos/std_typecons.html#.apply
162      */
163     Expected!(typeof(unaryFun!fun(T.init)), E) apply(alias fun)() @trusted
164     {
165         alias fn = unaryFun!fun;
166         if (hasExpectedValue)
167         {
168             return typeof(return)(fn(_expectedValue));
169         }
170         else
171         {
172             return typeof(return)(Unexpected!E(_unexpectedValue));
173         }
174     }
175 
176     // range interface:
177 
178     /// Check if empty.
179     @property bool empty() const
180     {
181         return !_ok;
182     }
183 
184     /// Get current value.
185     @property inout(T) front() inout @trusted
186     {
187         assert(_ok);
188         return _expectedValue;
189     }
190 
191     /// Pop (clear) current value.
192     void popFront()
193     {
194         assert(_ok);
195         clear();
196     }
197 
198 private:
199     union
200     {
201         T _expectedValue;         // TODO do we need to default-initialize this somehow?
202         Unexpected!E _unexpectedValue;
203     }
204 
205     /** Is true if `_expectedValue` is defined, otherwise `_unexpectedValue` is
206      * defined.
207      *
208      * According to @andralex its ok to be opportunistic and default to
209      * `T.init`, because of the naming `Expected`.
210      */
211     bool _ok = true;
212 }
213 
214 /// Insantiator for `Expected` from an expected value `expectedValue.`
215 auto expected(T, E)(auto ref T expectedValue)
216 {
217     return Expected!(T, E)(expectedValue);
218 }
219 
220 /// Insantiator for `Expected` from an unexpected value `unexpectedValue.`
221 auto unexpected(T, E)(auto ref E unexpectedValue)
222 {
223     return Expected!(T, E)(Unexpected!E(unexpectedValue));
224 }
225 
226 ///
227 @safe pure nothrow @nogc unittest
228 {
229     alias T = string;           // expected type
230     alias E = byte;
231 
232     alias Esi = Expected!(T, byte);
233 
234     // equality checks
235     assert(Esi("abc") == Esi("abc"));
236     assert(Esi("abcabc"[0 .. 3]) ==
237            Esi("abcabc"[3 .. 6]));
238 
239     auto x = Esi("abc");
240     assert(x.hasExpectedValue);
241     assert(!x.empty);
242     assert(x.apply!(threeUnderscores) == Esi("___"));
243 
244     x.popFront();
245     assert(!x.hasExpectedValue);
246     assert(x.empty);
247 
248     auto y = unexpected!(T, byte)(byte.init);
249     assert(!y.hasExpectedValue);
250     assert(x.empty);
251     assert(y.apply!(threeUnderscores) == Esi(Unexpected!byte(byte.init)));
252 }
253 
254 version(unittest)
255 inout(string) threeUnderscores(inout(string) x) @safe pure nothrow @nogc
256 {
257     return "___";
258 }