1 module drecord; 2 3 import std.meta; 4 import std.traits; 5 import std.conv : to; 6 7 /++ Add a field and getter to the record 8 + Params: 9 + type: The type the field will be 10 + name: Name of the field 11 + args: An optional default initialisation lambda (no arguments, returns value) ++/ 12 template get(alias type, string name, args...) 13 { 14 static assert(args.length <= 1, "There may only be 0 or 1 default initialisers"); 15 16 private alias type_ = type; 17 private alias name_ = name; 18 private alias args_ = args; 19 } 20 21 private mixin template getImpl(alias type, string name, args...) 22 { 23 static if(args.length == 0) 24 mixin("protected " ~ type.stringof ~ " " ~ name ~ "_;"); 25 else 26 mixin("protected " ~ type.stringof ~ " " ~ name ~ "_ = AliasSeq!(args)[0]();"); 27 mixin("public @property auto " ~ name ~ "() { return " ~ name ~ "_; }"); 28 } 29 30 /++ Add a field and appropriate getter and setter to the record 31 + Params: 32 + type: The type the field will be 33 + name: Name of the field 34 + args: An optional default initialisation lambda (no arguments, returns value) ++/ 35 template get_set(alias type, string name, args...) 36 { 37 static assert(args.length <= 1, "There may only be 0 or 1 default initialisers"); 38 39 private alias type_ = type; 40 private alias name_ = name; 41 private alias args_ = args; 42 } 43 44 private mixin template get_setImpl(alias type, string name, args...) 45 { 46 static if(args.length == 0) 47 mixin("protected " ~ type.stringof ~ " " ~ name ~ "_;"); 48 else 49 mixin("protected " ~ type.stringof ~ " " ~ name ~ "_ = AliasSeq!(args)[0]();"); 50 mixin("public @property auto " ~ name ~ "() { return " ~ name ~ "_; }"); 51 mixin("public @property void " ~ name ~ "(" ~ type.stringof ~ " nval__) { " ~ name ~ "_ = nval__; }"); 52 } 53 54 /++ Add a field whose value is computed when the record is created. 55 + It is run after `get`, and `get_set` fields have been set. 56 + Params: 57 + type: Type of the field 58 + name: Name of the field 59 + construct: A lambda value that sets this field 60 + Notice: the construct lambda must accept the record as its only parameter 61 + and return the field value. 62 ++/ 63 template get_compute(alias type, string name, alias construct) 64 { 65 private alias type_ = type; 66 private alias name_ = name; 67 private alias construct_ = construct; 68 } 69 70 private mixin template get_computeImpl(alias type, string name, alias construct) 71 { 72 private static string generateImpl() 73 { 74 string header = "protected " ~ type.stringof ~ " " ~ name ~ "_;" ~ 75 "protected @property auto " ~ name ~ "_construct() { return construct(this); }" ~ 76 "public @property auto " ~ name ~ "() { return " ~ name ~ "_; }"; 77 return header; 78 } 79 mixin(generateImpl); 80 } 81 82 /++ Add a property to the record 83 + Params: 84 + name: Name of the property 85 + accessor: A lambda of the property body 86 + args: Types of arguments the property needs ++/ 87 template property(string name, alias accessor, args...) 88 { 89 private alias name_ = name; 90 private alias accessor_ = accessor; 91 private alias args_ = AliasSeq!args; 92 } 93 94 private mixin template propertyImpl(string name, alias accessor, args...) 95 { 96 private alias seq = AliasSeq!args; 97 private static string generateImpl() 98 { 99 string header = "public @property auto " ~ name ~ "("; 100 string body_ = "{ return accessor(this, "; 101 static foreach(i, item; seq) 102 { 103 header ~= item.stringof ~ " arg" ~ to!string(i) ~ "__"; 104 body_ ~= "arg" ~ to!string(i) ~ "__"; 105 static if(i < seq.length - 1) 106 { 107 header ~= ", "; 108 body_ ~= ", "; 109 } 110 } 111 header ~= ") "; 112 body_ ~= "); }"; 113 return header ~ body_; 114 } 115 mixin(generateImpl); 116 } 117 118 template record(args...) 119 { 120 private enum isGet(alias T) = __traits(isSame, TemplateOf!T, get); 121 private enum isGetSet(alias T) = __traits(isSame, TemplateOf!T, get_set); 122 private enum isProperty(alias T) = __traits(isSame, TemplateOf!T, property); 123 private enum isGetCompute(alias T) = __traits(isSame, TemplateOf!T, get_compute); 124 125 private enum isCtorParam(alias T) = isGet!T || isGetSet!T; 126 private enum numCtorParam = Filter!(isCtorParam, AliasSeq!args).length; 127 private enum isField(alias T) = isGet!T || isGetSet!T || isGetCompute!T; 128 private enum numFields = Filter!(isField, AliasSeq!args).length; 129 130 /// Generate a constructor that takes inputs for every field 131 private static string genCtor() 132 { 133 string header = "public this("; 134 string body_ = "{\n"; 135 136 static foreach(i, item; Filter!(isCtorParam, AliasSeq!args)) 137 { 138 header ~= item.type_.stringof ~ " arg" ~ to!string(i) ~ "__"; 139 body_ ~= "\t" ~ item.name_ ~ "_ = arg" ~ to!string(i) ~ "__;\n"; 140 141 static if(i < numCtorParam - 1) 142 header ~= ", "; 143 } 144 header ~= ")\n"; 145 body_ ~= "constructs; }"; 146 return header ~ body_; 147 } 148 149 /++ Test for equality. Reference types are checked to ensure 150 + their references are the same (point to same thing), value types 151 + are checked to ensure their values are identical. 152 ++/ 153 private static string genEquals() 154 { 155 string res = "import std.math : isClose;\nbool result = true;\n"; 156 static foreach(i, item; Filter!(isField, AliasSeq!args)) 157 { 158 static if(is(item.type_ == class) || 159 is(item.type_ == interface) || 160 isPointer!(item.type_)) 161 { 162 res ~= "if(" ~ item.name_ ~ "_ !is otherRec." ~ item.name_ ~ "_) { result = false; }\n"; 163 } 164 else static if(isFloatingPoint!(item.type_)) 165 { 166 res ~= "if(!isClose(" ~ item.name_ ~ "_, otherRec." ~ item.name_ ~ "_)) { result = false; }\n"; 167 } 168 else 169 { 170 res ~= "if(" ~ item.name_ ~ "_ != otherRec." ~ item.name_ ~ "_) { result = false; }\n"; 171 } 172 } 173 res ~= "return result;"; 174 return res; 175 } 176 177 final class record 178 { 179 static foreach(item; AliasSeq!args) 180 { 181 static if(isGet!item) 182 mixin getImpl!(item.type_, item.name_, item.args_); 183 else static if(isGetSet!item) 184 mixin get_setImpl!(item.type_, item.name_, item.args_); 185 else static if(isProperty!item) 186 mixin propertyImpl!(item.name_, item.accessor_, item.args_); 187 else static if(isGetCompute!item) 188 mixin get_computeImpl!(item.type_, item.name_, item.construct_); 189 else static assert(false, "Unsupported type. Please ensure types for record are either get!T, get_set!T, property!T"); 190 } 191 192 /// Default initialise all fields 193 this(bool runConstructs = true) 194 { 195 if(runConstructs) 196 { 197 constructs; 198 } 199 } 200 201 private void constructs() 202 { 203 static foreach(item; Filter!(isGetCompute, AliasSeq!args)) 204 { 205 mixin("this." ~ item.name_ ~ "_ = " ~ item.name_ ~ "_construct();"); 206 } 207 } 208 209 /// Explicitly set all fields 210 mixin(genCtor); 211 212 /// Explicitly set certain fields, default initialise the rest 213 static record create(TNames...)(...) 214 { 215 auto r = new record(false); 216 import core.vararg; 217 static foreach(item; AliasSeq!TNames) 218 { 219 static foreach(b; AliasSeq!args) 220 static if(isGetCompute!b) 221 static assert(b.name_ != item, "Cannot set a get_compute property '" ~ item ~ "'"); 222 223 mixin("r." ~ item ~ "_ = va_arg!(typeof(" ~ item ~ "_))(_argptr);"); 224 } 225 r.constructs; 226 return r; 227 } 228 229 /++ Test for equality. Reference types are checked to ensure 230 + their references are the same (point to same thing), value types 231 + are checked to ensure their values are identical. 232 ++/ 233 override bool opEquals(Object other) 234 { 235 if(record otherRec = cast(record)other) 236 { 237 mixin(genEquals); 238 } 239 else return false; 240 } 241 242 /// Generate a human-readable string. Fields are sampled. 243 override string toString() 244 { 245 string result = "{"; 246 247 static foreach(i, item; Filter!(isField, AliasSeq!args)) 248 { 249 result ~= item.name_ ~ " = " ~ to!string(mixin(item.name_ ~ "_")); 250 static if(i < numFields - 1) 251 result ~= ", "; 252 } 253 result ~= "}"; 254 255 return result; 256 } 257 258 /// Compute the hash of this record. Algorithm is `result = 31 * previousResult + currentField.toHash` 259 override nothrow @trusted size_t toHash() 260 { 261 size_t result = 0; 262 263 static foreach(i, item; Filter!(isField, AliasSeq!args)) 264 { 265 static if(is(item.type_ == class) || is(item.type_ == interface)) 266 result = result * 31 + cast(size_t)mixin(item.name_ ~ "_.toHash()"); 267 else static if(is(item.type_ == struct)) 268 { 269 static if(__traits(compiles, () { item.type_().toHash; }())) 270 result = result * 31 + cast(size_t)mixin(item.name_ ~ "_"); 271 } 272 else 273 result = result * 31 + cast(size_t)mixin(item.name_ ~ "_"); 274 } 275 276 return result; 277 } 278 /++ Create a copy of this record and permit modification of read-only fields. ++/ 279 record duplicate(TNames...)(...) 280 { 281 record r = new record; 282 static foreach(item; Filter!(isField, AliasSeq!args)) 283 mixin("r." ~ item.name_ ~ "_ = this." ~ item.name_ ~ "_;"); 284 import core.vararg; 285 static foreach(item; AliasSeq!TNames) 286 { 287 static foreach(b; AliasSeq!args) 288 static if(isGetCompute!b) 289 static assert(b.name_ != item, "Cannot set a get_compute property '" ~ item ~ "'"); 290 291 mixin("r." ~ item ~ "_ = va_arg!(typeof(" ~ item ~ "_))(_argptr);"); 292 } 293 r.constructs; 294 return r; 295 } 296 } 297 } 298 299 unittest 300 { 301 alias MyRecord = record!( 302 get!(int, "x"), /// x is an int, can only be set during construction 303 get_set!(float, "y"), /// y is a float, can be get or set whenever 304 property!("getDoubleOfX", (r) => r.x * 2), /// a property that returns the double of x 305 property!("getMultipleOfX", (r, m) => r.x * m, int), /// that takes an argument and multiples x by that value 306 property!("resetY", (r) => r.y = 0) /// resets y to 0f 307 ); 308 309 auto r = new MyRecord(12, 4.5f); /// sets x, y 310 311 assert(r.toString == `{x = 12, y = 4.5}`,); 312 assert(r.toHash == 376); 313 314 assert(r.x == 12); 315 assert(r.getDoubleOfX == 24); 316 assert(r.getMultipleOfX(4) == 48); 317 assert(r.y == 4.5f); 318 r.resetY; 319 assert(r.y == 0); 320 r.y = 13f; 321 assert(r.y == 13f); 322 323 /// Duplicate r, and set x to 17 (we can only do this in ctor, or during duplication) 324 /// This is equivalent to C#'s "with" syntax for records [0] 325 r.y = 0; 326 auto q = r.duplicate!("x")(17); 327 assert(q.toString == `{x = 17, y = 0}`); 328 assert(q != r); 329 assert(q !is r); 330 331 auto b = r.duplicate; // duplicate, don't change any fields 332 assert(b == r); 333 assert(b !is r); 334 } 335 336 unittest 337 { 338 alias DefaultRecord = record!( 339 // The third parameter is a lambda which provides default initialisation 340 get!(int, "x", () => 4), // x is set to 4 by default 341 get_set!(Object, "o", () => new Object) // o is set to a new Object by default 342 ); 343 344 auto r = new DefaultRecord; // run the default initialisers 345 assert(r.toString == `{x = 4, o = object.Object}`); 346 347 auto q = DefaultRecord.create!"x"(9); // run default initialisers, then set x to 9 348 assert(q.toString == `{x = 9, o = object.Object}`); 349 } 350 351 version (unittest) 352 { 353 alias TC3Record = record!( 354 get!(int, "x", () => 20), 355 // get_compute lets you compute a field after the rest have been initialised 356 get_compute!(float, "y", (rec) => rec.x * 2f) 357 ); 358 } 359 unittest 360 { 361 auto r = new TC3Record; 362 assert(r.toString == `{x = 20, y = 40}`); 363 r = new TC3Record(10); 364 assert(r.toString == `{x = 10, y = 20}`); 365 r = TC3Record.create!"x"(5); 366 assert(r.toString == `{x = 5, y = 10}`); 367 auto q = r.duplicate!"x"(2); 368 assert(q.toString == `{x = 2, y = 4}`); 369 }