1 module dls.util.nullable;
2 
3 import std.format : singleSpec, FormatSpec, formatValue;
4 import std.meta : AliasSeq, allSatisfy;
5 import std.range.primitives : isOutputRange;
6 import std.traits;
7 
8 struct Nullable(T)
9 {
10     private union DontCallDestructorT
11     {
12         T payload;
13     }
14 
15     private DontCallDestructorT _value = DontCallDestructorT.init;
16 
17     private bool _isNull = true;
18 
19     /**
20      * Constructor initializing `this` with `value`.
21      *
22      * Params:
23      *     value = The value to initialize this `Nullable` with.
24      */
25     this(inout T value) inout
26     {
27         _value.payload = value;
28         _isNull = false;
29     }
30 
31     static if (is(T == struct) && hasElaborateDestructor!T)
32     {
33         ~this()
34         {
35             if (!_isNull)
36             {
37                 destroy(_value.payload);
38             }
39         }
40     }
41 
42     /**
43      * If they are both null, then they are equal. If one is null and the other
44      * is not, then they are not equal. If they are both non-null, then they are
45      * equal if their values are equal.
46      */
47     bool opEquals()(auto ref const(typeof(this)) rhs) const
48     {
49         if (_isNull)
50             return rhs._isNull;
51         if (rhs._isNull)
52             return false;
53         return _value.payload == rhs._value.payload;
54     }
55 
56     /// Ditto
57     bool opEquals(U)(auto ref const(U) rhs) const
58     if (!is(U : typeof(this)) && is(typeof(this.get == rhs)))
59     {
60         return _isNull ? false : rhs == _value.payload;
61     }
62 
63     ///
64     @safe unittest
65     {
66         Nullable!int empty;
67         Nullable!int a = 42;
68         Nullable!int b = 42;
69         Nullable!int c = 27;
70 
71         assert(empty == empty);
72         assert(empty == Nullable!int.init);
73         assert(empty != a);
74         assert(empty != b);
75         assert(empty != c);
76 
77         assert(a == b);
78         assert(a != c);
79 
80         assert(empty != 42);
81         assert(a == 42);
82         assert(c != 42);
83     }
84 
85     @safe unittest
86     {
87         // Test constness
88         immutable Nullable!int a = 42;
89         Nullable!int b = 42;
90         immutable Nullable!int c = 29;
91         Nullable!int d = 29;
92         immutable e = 42;
93         int f = 29;
94         assert(a == a);
95         assert(a == b);
96         assert(a != c);
97         assert(a != d);
98         assert(a == e);
99         assert(a != f);
100 
101         // Test rvalue
102         assert(a == const Nullable!int(42));
103         assert(a != Nullable!int(29));
104     }
105 
106     // https://issues.dlang.org/show_bug.cgi?id=17482
107     @system unittest
108     {
109         import std.variant : Variant;
110         Nullable!Variant a = Variant(12);
111         assert(a == 12);
112         Nullable!Variant e;
113         assert(e != 12);
114     }
115 
116     size_t toHash() const @safe nothrow
117     {
118         static if (__traits(compiles, .hashOf(_value.payload)))
119             return _isNull ? 0 : .hashOf(_value.payload);
120         else
121             // Workaround for when .hashOf is not both @safe and nothrow.
122             return _isNull ? 0 : typeid(T).getHash(&_value.payload);
123     }
124 
125     /**
126      * Gives the string `"Nullable.null"` if `isNull` is `true`. Otherwise, the
127      * result is equivalent to calling $(REF formattedWrite, std,format) on the
128      * underlying value.
129      *
130      * Params:
131      *     writer = A `char` accepting
132      *     $(REF_ALTTEXT output range, isOutputRange, std, range, primitives)
133      *     fmt = A $(REF FormatSpec, std,format) which is used to represent
134      *     the value if this Nullable is not null
135      * Returns:
136      *     A `string` if `writer` and `fmt` are not set; `void` otherwise.
137      */
138     string toString()
139     {
140         import std.array : appender;
141         auto app = appender!string();
142         auto spec = singleSpec("%s");
143         toString(app, spec);
144         return app.data;
145     }
146 
147     /// ditto
148     string toString() const
149     {
150         import std.array : appender;
151         auto app = appender!string();
152         auto spec = singleSpec("%s");
153         toString(app, spec);
154         return app.data;
155     }
156 
157     /// ditto
158     void toString(W)(ref W writer, scope const ref FormatSpec!char fmt)
159     if (isOutputRange!(W, char))
160     {
161         import std.range.primitives : put;
162         if (isNull)
163             put(writer, "Nullable.null");
164         else
165             formatValue(writer, _value.payload, fmt);
166     }
167 
168     /// ditto
169     void toString(W)(ref W writer, scope const ref FormatSpec!char fmt) const
170     if (isOutputRange!(W, char))
171     {
172         import std.range.primitives : put;
173         if (isNull)
174             put(writer, "Nullable.null");
175         else
176             formatValue(writer, _value.payload, fmt);
177     }
178 
179     /**
180      * Check if `this` is in the null state.
181      *
182      * Returns:
183      *     true $(B iff) `this` is in the null state, otherwise false.
184      */
185     @property bool isNull() const @safe pure nothrow
186     {
187         return _isNull;
188     }
189 
190     ///
191     @safe unittest
192     {
193         Nullable!int ni;
194         assert(ni.isNull);
195 
196         ni = 0;
197         assert(!ni.isNull);
198     }
199 
200     // https://issues.dlang.org/show_bug.cgi?id=14940
201     @safe unittest
202     {
203         import std.array : appender;
204         import std.format : formattedWrite;
205 
206         auto app = appender!string();
207         Nullable!int a = 1;
208         formattedWrite(app, "%s", a);
209         assert(app.data == "1");
210     }
211 
212     // https://issues.dlang.org/show_bug.cgi?id=19799
213     @safe unittest
214     {
215         import std.format : format;
216 
217         const Nullable!string a = const(Nullable!string)();
218 
219         format!"%s"(a);
220     }
221 
222     /**
223      * Forces `this` to the null state.
224      */
225     void nullify()()
226     {
227         static if (is(T == class) || is(T == interface))
228             _value.payload = null;
229         else
230             .destroy(_value.payload);
231         _isNull = true;
232     }
233 
234     ///
235     @safe unittest
236     {
237         Nullable!int ni = 0;
238         assert(!ni.isNull);
239 
240         ni.nullify();
241         assert(ni.isNull);
242     }
243 
244     /**
245      * Assigns `value` to the internally-held state. If the assignment
246      * succeeds, `this` becomes non-null.
247      *
248      * Params:
249      *     value = A value of type `T` to assign to this `Nullable`.
250      */
251     void opAssign()(T value)
252     {
253         import std.algorithm.mutation : moveEmplace, move;
254 
255         // the lifetime of the value in copy shall be managed by
256         // this Nullable, so we must avoid calling its destructor.
257         auto copy = DontCallDestructorT(value);
258 
259         if (_isNull)
260         {
261             // trusted since payload is known to be T.init here.
262             () @trusted { moveEmplace(copy.payload, _value.payload); }();
263         }
264         else
265         {
266             move(copy.payload, _value.payload);
267         }
268         _isNull = false;
269     }
270 
271     /**
272      * If value is null, sets this to null, otherwise assigns
273      * `value.get` to the internally-held state. If the assignment
274      * succeeds, `this` becomes non-null.
275      *
276      * Params:
277      *     value = A value of type `Nullable!T` to assign to this `Nullable`.
278      */
279     void opAssign()(Nullable!T value)
280     {
281         if (value._isNull)
282             nullify();
283         else
284             opAssign(value.get());
285     }
286 
287     /**
288      * If this `Nullable` wraps a type that already has a null value
289      * (such as a pointer), then assigning the null value to this
290      * `Nullable` is no different than assigning any other value of
291      * type `T`, and the resulting code will look very strange. It
292      * is strongly recommended that this be avoided by instead using
293      * the version of `Nullable` that takes an additional `nullValue`
294      * template argument.
295      */
296     @safe unittest
297     {
298         //Passes
299         Nullable!(int*) npi;
300         assert(npi.isNull);
301 
302         //Passes?!
303         npi = null;
304         assert(!npi.isNull);
305     }
306 
307     /**
308      * Gets the value if not null. If `this` is in the null state, and the optional
309      * parameter `fallback` was provided, it will be returned. Without `fallback`,
310      * calling `get` with a null state is invalid.
311      *
312      * When the fallback type is different from the Nullable type, `get(T)` returns
313      * the common type.
314      *
315      * Params:
316      *     fallback = the value to return in case the `Nullable` is null.
317      *
318      * Returns:
319      *     The value held internally by this `Nullable`.
320      */
321     @property ref inout(T) get() inout @safe pure nothrow
322     {
323         enum message = "Called `get' on null Nullable!" ~ T.stringof ~ ".";
324         assert(!isNull, message);
325         return _value.payload;
326     }
327 
328     /// ditto
329     @property inout(T) get()(inout(T) fallback) inout @safe pure nothrow
330     {
331         return isNull ? fallback : _value.payload;
332     }
333 
334     /// ditto
335     @property auto get(U)(inout(U) fallback) inout @safe pure nothrow
336     {
337         return isNull ? fallback : _value.payload;
338     }
339 
340     //@@@DEPRECATED_2.096@@@
341     deprecated(
342         "Implicit conversion with `alias Nullable.get this` will be removed after 2.096. Please use `.get` explicitly.")
343     @system unittest
344     {
345         import core.exception : AssertError;
346         import std.exception : assertThrown, assertNotThrown;
347 
348         Nullable!int ni;
349         int i = 42;
350         //`get` is implicitly called. Will throw
351         //an AssertError in non-release mode
352         assertThrown!AssertError(i = ni);
353         assert(i == 42);
354 
355         ni = 5;
356         assertNotThrown!AssertError(i = ni);
357         assert(i == 5);
358     }
359 
360     //@@@DEPRECATED_2.096@@@
361     deprecated(
362         "Implicit conversion with `alias Nullable.get this` will be removed after 2.096. Please use `.get` explicitly.")
363     @property ref inout(T) get_() inout @safe pure nothrow
364     {
365         return get;
366     }
367 
368     ///
369     @safe pure nothrow unittest
370     {
371         int i = 42;
372         Nullable!int ni;
373         int x = ni.get(i);
374         assert(x == i);
375 
376         ni = 7;
377         x = ni.get(i);
378         assert(x == 7);
379     }
380 
381     /**
382      * Implicitly converts to `T`.
383      * `this` must not be in the null state.
384      * This feature is deprecated and will be removed after 2.096.
385      */
386     alias get_ this;
387 }
388 
389 /// ditto
390 auto nullable(T)(T t)
391 {
392     return Nullable!T(t);
393 }