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 }