1 /**
2 Copyright: Copyright (c) 2020, Joakim Brännström. All rights reserved.
3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0)
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 This is inspired from [NamedTypes C++](https://github.com/joboccara/NamedType).
7 
8 A strong type is a type used in place of another type to carry specific meaning
9 through its name. It is a variant of `TypeDef` in phobos
10 
11 ## Basic usage
12 
13 The central piece is the templated class NamedType, which can be used to declare
14 a strong type with a typedef-like syntax:
15 
16 ```d
17 alias Width = NamedType!(double, Tag!"width");
18 alias Height = NamedType!(double, Tag!"height");
19 ```
20 
21 which can be used to make interfaces more expressive and more robust. Note how
22 the below constructor shows in which order it expects its parameters:
23 
24 ```d
25 class Rectangle {
26     private double width_;
27     private double height_;
28 
29     this(Width width, Height height) {
30         this.width_ = width.get;
31         this.height_ = height.get;
32     }
33 
34     double width() const { return width_; }
35     double height() const { return height_; }
36 }
37 ```
38 
39 **Strong types are about better expressing your intentions, both to the
40 compiler and to other human developers.**
41 
42 ## Strong typing over generic types
43 
44 This implementation of strong types can be used to add strong typing over
45 generic or unknown types such as lambdas:
46 
47 ```d
48 static bool performAction(T)(T x, T y) if(hasTraits!(T, Comparable)) {
49     return x > y;
50 }
51 ```
52 
53 ## Inheriting the underlying type functionalities
54 
55 You can declare which functionalities should be inherited from the underlying
56 type. So far, only basic operators are taken into account. For instance, to
57 inherit from `+` and `toString`, you can declare the strong type:
58 
59 ```d
60 alias Meter = NamedType!(double, Tag!"meter", Addable, Printable);
61 ```
62 
63 There is one special trait, `ImplicitConvertable`, that lets the strong type be
64 converted in the underlying type. This has the effect of removing the need to
65 call .get() to get the underlying value.
66 
67 ## Named arguments
68 
69 By their nature strong types can play the role of named parameters:
70 
71 ```d
72 alias FirstName = NamedType!(string, Tag!"firstName");
73 alias LastName = NamedType!(string, Tag!"lastName");
74 
75 void displayName(FirstName theFirstName, LastName theLastName);
76 
77 // Call site
78 displayName(FirstName("John"), LastName("Doe"));
79 ```
80 
81 But the nested type `argument` allows to emulate a named argument syntax:
82 
83 ```d
84 alias FirstName = NamedType!(string, Tag!"firstName");
85 alias LastName = NamedType!(string, Tag!"lastName");
86 
87 void displayName(FirstName theFirstName, LastName theLastName);
88 
89 // Call site
90 displayName(FirstName.argument = "John", LastName.argument = "Doe");
91 ```
92 */
93 module my.named_type;
94 
95 struct Tag(alias T) {
96     static string toString() {
97         static if (is(typeof(T) : string))
98             return T;
99         else
100             return T.stringof;
101     }
102 }
103 
104 alias NamedTypeT(T, Traits...) = NamedType!(T, Tag!(T.stringof), T.init, Traits);
105 
106 enum hasTraits(T, Traits...) = is(T.Traits == Traits);
107 
108 struct NamedType(T, TagT = Tag!(T.stringof), T init = T.init, TraitsT...)
109         if (is(TagT : Tag!U, alias U)) {
110     import std.meta : staticMap, AliasSeq;
111     import std.range : isOutputRange;
112 
113     /// The underlying type.
114     alias Type = T;
115     alias Traits = TraitsT;
116     alias Tag = TagT;
117     alias ThisT = typeof(this);
118 
119     private T value = init;
120 
121     // https://issues.dlang.org/show_bug.cgi?id=18415
122     // prevent default construction if original type does too.
123     static if ((is(T == struct) || is(T == union)) && !is(typeof({ T t; }))) {
124         @disable this();
125     }
126 
127     this(T v) {
128         this.value = v;
129     }
130 
131     this(NamedType!(T, Tag, init, Traits) v) {
132         this.value = v.value;
133     }
134 
135     T opCast(T2 : T)() inout {
136         return value;
137     }
138 
139     /// The underlying value.
140     ref inout(T) get() inout {
141         return value;
142     }
143 
144     /// Useful for e.g. getopt integration
145     scope T* getPtr() {
146         return &value;
147     }
148 
149     static typeof(this) make(T v) {
150         return typeof(this)(v);
151     }
152 
153     private static struct EmulateNamedArgument {
154         ThisT value;
155         alias value this;
156     }
157 
158     static EmulateNamedArgument argument(T value) {
159         return EmulateNamedArgument(ThisT(value));
160     }
161 
162     template ReplaceTrait(T) {
163         static if (is(T == Arithmetic)) {
164             alias ReplaceTrait = AliasSeq!(Incrementable, Decrementable, Addable,
165                     Subtractable, Modulable, Divisable, Multiplicable, Comparable);
166         } else {
167             alias ReplaceTrait = T;
168         }
169     }
170 
171     static foreach (Tr; staticMap!(ReplaceTrait, Traits)) {
172         //pragma(msg, Tr);
173         static if (is(Tr == Comparable)) {
174             bool opEquals(const typeof(this) rhs) inout {
175                 return value == rhs.value;
176             }
177 
178             bool opEquals(ref const typeof(this) rhs) inout {
179                 return value == rhs.value;
180             }
181 
182             int opCmp(ref const typeof(this) rhs) const {
183                 // return -1 if "this" is less than rhs, 1 if bigger and zero
184                 // equal.
185                 if (value < rhs.value)
186                     return -1;
187                 if (value > rhs.value)
188                     return 1;
189                 return 0;
190             }
191         } else static if (is(Tr == Hashable)) {
192             static if (__traits(hasMember, T, "toHash")) {
193                 size_t toHash(T2 = typeof(this))() {
194                     return value.toHash;
195                 }
196             } else {
197                 size_t toHash() @safe nothrow const scope {
198                     return typeid(value).getHash(&value);
199                 }
200             }
201         } else static if (is(Tr == Incrementable)) {
202             auto opUnary(string op)() if (op == "++") {
203                 return typeof(this)(++value);
204             }
205         } else static if (is(Tr == Decrementable)) {
206             auto opUnary(string op)() if (op == "--") {
207                 return typeof(this)(--value);
208             }
209         } else static if (is(Tr == Addable)) {
210             auto opBinary(string op)(typeof(this) rhs) if (op == "+") {
211                 return typeof(this)(value + rhs.value);
212             }
213         } else static if (is(Tr == Subtractable)) {
214             auto opBinary(string op)(typeof(this) rhs) if (op == "-") {
215                 return typeof(this)(value - rhs.value);
216             }
217         } else static if (is(Tr == Modulable)) {
218             auto opBinary(string op)(typeof(this) rhs) if (op == "%") {
219                 return typeof(this)(value % rhs.value);
220             }
221         } else static if (is(Tr == Divisable)) {
222             auto opBinary(string op)(typeof(this) rhs) if (op == "/") {
223                 return typeof(this)(value / rhs.value);
224             }
225         } else static if (is(Tr == Multiplicable)) {
226             auto opBinary(string op)(typeof(this) rhs) if (op == "*") {
227                 return typeof(this)(value * rhs.value);
228             }
229         } else static if (is(Tr == ImplicitConvertable)) {
230             alias get this;
231         } else static if (is(Tr == Lengthable)) {
232             static if (is(T : U[], U)) {
233             } else static if (!__traits(hasMember, T, "length")) {
234                 static assert(0, ".length() is not implemented for " ~ T.stringof);
235             }
236 
237             size_t length() inout {
238                 return value.length;
239             }
240 
241             bool empty() inout {
242                 return value.length == 0;
243             }
244         } else static if (is(Tr == TagStringable)) {
245             import std.format : singleSpec, FormatSpec, formatValue;
246 
247             string toString(T2 = typeof(this))() inout {
248                 import std.array : appender;
249 
250                 auto buf = appender!string;
251                 auto spec = singleSpec("%s");
252                 toString(buf, spec);
253                 return buf.data;
254             }
255 
256             void toString(Writer, T2 = typeof(this))(ref Writer w, scope const ref FormatSpec!char fmt) inout
257                     if (isOutputRange!(Writer, char)) {
258                 import std.range : put;
259 
260                 put(w, Tag.toString);
261                 put(w, "(");
262                 formatValue(w, value, fmt);
263                 put(w, ")");
264             }
265         } else static if (is(Tr == ForwardStringable)) {
266             import std.format : singleSpec, FormatSpec, formatValue;
267 
268             string toString(T2 = typeof(this))() inout {
269                 static if (__traits(hasMember, T, "toString"))
270                     return value.toString();
271                 else
272                     return value;
273             }
274 
275             void toString(Writer, T2 = typeof(this))(ref Writer w, scope const ref FormatSpec!char fmt) inout
276                     if (isOutputRange!(Writer, char)) {
277                 import std.range : put;
278 
279                 static if (__traits(hasMember, T, "toString"))
280                     return value.toString(w, fmt);
281                 else
282                     return put(w, value);
283             }
284         } else static if (is(Tr == ConvertStringable)) {
285             string toString(T2 = typeof(this))() inout {
286                 import std.conv : to;
287 
288                 return value.to!string();
289             }
290         } else {
291             static assert(0, "Unknown trait " ~ Tr.stringof);
292         }
293     }
294 }
295 
296 struct Comparable {
297 }
298 
299 struct Incrementable {
300 }
301 
302 struct Decrementable {
303 }
304 
305 struct Addable {
306 }
307 
308 struct Subtractable {
309 }
310 
311 struct Modulable {
312 }
313 
314 struct Divisable {
315 }
316 
317 struct Multiplicable {
318 }
319 
320 // Format the type to a string by using the Tag template parameter.
321 //
322 // `writeln(NamedType!(int, Tag!"MyInt", 0, TagToString)(42)`
323 // is printed as:
324 // `MyInt(42)`
325 struct TagStringable {
326 }
327 
328 // for backward compatibility
329 alias Printable = TagStringable;
330 
331 /// Forward to the underlying type the toString.
332 struct ForwardStringable {
333 }
334 
335 /// Uses `std.conv.to!string` to convert the underlying type to a string.
336 struct ConvertStringable {
337 }
338 
339 struct ImplicitConvertable {
340 }
341 
342 struct Hashable {
343 }
344 
345 struct Arithmetic {
346 }
347 
348 struct Lengthable {
349 }
350 
351 @("shall be possible to use a NamedType to express the intent of parameters")
352 unittest {
353     alias Width = NamedType!(double, Tag!"width");
354     alias Height = NamedType!(double, Tag!"height");
355 
356     static void calcSquare(Width w, Height h) {
357         assert(w.get * h.get > w.get);
358     }
359 
360     calcSquare(Width(42), Height(84));
361 }
362 
363 @("shall be possible to cast to the underlying type")
364 unittest {
365     auto x = NamedType!(int, Tag!"x")(42);
366     assert(cast(int) x == 42);
367 }
368 
369 @("shall implement the compare operators")
370 unittest {
371     alias A = NamedType!(int, Tag!"x", 0, Comparable);
372     auto x = A(42);
373     auto y = A(84);
374     assert(x == x);
375     assert(x != y);
376     assert(x < y);
377     assert(y > x);
378 }
379 
380 @("shall implement the arithmetic operators")
381 unittest {
382     alias A = NamedType!(int, Tag!"x", 0, Arithmetic);
383     auto x = A(10);
384     auto y = A(20);
385     assert(++A(x)++ == A(11));
386     assert(--A(x)-- == A(9));
387 
388     assert(x + x == A(20));
389     assert(x + y == A(30));
390     assert(x - y == A(-10));
391     assert(y / x == A(2));
392     assert(y * x == A(200));
393     assert(x % y == A(10));
394 }
395 
396 @("shall implement a function that take a type that implement the Comparable trait")
397 unittest {
398     static bool perform(T)(T x, T y) if (hasTraits!(T, Comparable)) {
399         return x > y;
400     }
401 
402     alias A = NamedTypeT!(int, Comparable);
403     assert(perform(A(20), A(10)));
404 }
405 
406 @("shall add implicit convertable")
407 unittest {
408     alias A = NamedTypeT!(int, ImplicitConvertable);
409     auto x = A(10);
410     assert(x == 10);
411 }
412 
413 @("shall emulate named arguments syntax")
414 unittest {
415     alias A = NamedTypeT!(int);
416     static bool fun(A x) {
417         return true;
418     }
419 
420     assert(fun(A.argument = 10));
421 }
422 
423 @("shall be possible to use in an AA")
424 unittest {
425     alias A = NamedType!(long, Tag!"A", 0, Comparable, Hashable);
426     A[A] x;
427     x[A(10)] = A(20);
428     assert(x[A(10)] == A(20));
429 }
430 
431 @("shall be possible to cast to the underlying type")
432 unittest {
433     alias A = NamedTypeT!(int);
434     auto a = A(10);
435     const b = A(20);
436     assert(10 == cast(int) a);
437     assert(20 == cast(int) b);
438 }
439 
440 @("shall be possible to call get of a const instance")
441 unittest {
442     alias A = NamedTypeT!(int);
443     const b = A(20);
444     assert(20 == b.get);
445 }
446 
447 @("shall expose .length when implementing Lengable")
448 @safe unittest {
449     alias A = NamedTypeT!(int[], Lengthable);
450     const b = A([20, 30]);
451     assert(b.length == 2);
452 }
453 
454 @("shall only use the Tag when stringifying")
455 unittest {
456     import std.format : format;
457     import std.conv : to;
458 
459     alias A = NamedTypeT!(int, TagStringable);
460     const a = A(10);
461     {
462         auto s = format!"value is %s"(a);
463         assert(s == "value is int(10)");
464     }
465 
466     {
467         auto s = a.to!string;
468         assert(s == "int(10)");
469     }
470 }
471 
472 @("shall forward toString to the underlying type")
473 unittest {
474     import std.format : format;
475     import std.conv : to;
476 
477     alias A = NamedTypeT!(string, ForwardStringable);
478     const a = A("apa");
479     {
480         auto s = format!"value is %s"(a);
481         assert(s == "value is apa");
482     }
483 
484     {
485         auto s = a.to!string;
486         assert(s == "apa");
487     }
488 }
489 
490 @("shall convert the value to a string using std.conv.to")
491 unittest {
492     import std.format : format;
493     import std.conv : to;
494 
495     alias A = NamedTypeT!(int, ConvertStringable);
496     const a = A(10);
497     {
498         auto s = format!"value is %s"(a);
499         assert(s == "value is 10");
500     }
501 
502     {
503         auto s = a.to!string;
504         assert(s == "10");
505     }
506 }