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