1 /** 2 Provides functionality for easily creating immutable record types. 3 4 Authors: Tony J. Hudgins 5 Copyright: Copyright © 2017-2019, Tony J. Hudgins 6 License: MIT 7 */ 8 module dext.record; 9 10 import dext.typecons : Params; 11 12 /++ 13 A set of configuration values for changing the behaviour of the Record mixin. 14 15 Authors: Tony J. Hudgins 16 Copyright: Copyright © 2017-2019, Tony J. Hudgins 17 License: MIT 18 +/ 19 enum RecordConfig : string 20 { 21 /// Suppress automatic constructor generation. 22 suppressCtor = "Suppress automatic constructor generation.", 23 24 /// Automatically generate a [deconstruct] method for use with the [let] module. 25 enableLet = "Automatically generate a deconstruct method for use with the 'let' module.", 26 27 /// Automatically generate [with<FieldName>] methods to create copies with new values. 28 enableMutation = "Automatically generate 'with<fieldName>' methods.", 29 30 /// Automatically generate setters for all fields, making the records mutable. 31 enableSetters = "Automatically generate setters for all fields, making the record mutable.", 32 } 33 34 alias RecordParams = Params!RecordConfig; 35 36 /++ 37 A mixin template for turning a struct into an immutable value type 38 that automatically implements equality (==, !=), hashcode computation (toHash), 39 stringification (toString), and mutation methods that create new copies with new values. 40 41 The purpose of this struct is act similarly to record types in functional 42 programming languages like OCaml and Haskell. 43 44 Accepts an optional, boolean template parameter. When true, the mixin will generate 45 a deconstruction method for use with <a href="/dext/dext/let">let</a>. Default is false. 46 47 All fields on the struct must start with an underscore and be non-public. Both are enforced with static asserts. 48 49 Authors: Tony J. Hudgins 50 Copyright: Copyright © 2017-2019, Tony J. Hudgins 51 License: MIT 52 53 Examples: 54 --------- 55 // define a point int 2D space 56 struct Point 57 { 58 mixin Record!true; 59 private int _x, _y; 60 } 61 62 auto a = Point( 3, 7 ); 63 auto b = a.withX( 10 ); // creates a new copy of 'a' with a new 'x' value of 10 and the same 'y' value such that b == Point( 10, 7 ) 64 65 // Euclidean distance 66 auto distance( in Point a, in Point b ) 67 { 68 import std.math : sqrt; 69 70 return sqrt( ( a.x - b.x ) ^^ 2f + ( a.y - b.y ) ^^ 2f ); 71 } 72 73 auto dist = distance( a, b ); 74 --------- 75 +/ 76 mixin template Record( RecordParams params = RecordParams.init ) 77 { 78 import std.traits : FieldTypeTuple, FieldNameTuple, staticMap; 79 import std.format : format; 80 81 static assert( 82 is( typeof( this ) ) && ( is( typeof( this ) == class ) || is( typeof( this ) == struct ) ), 83 "Record mixin template may only be used from within a struct or class" 84 ); 85 86 invariant 87 { 88 static foreach( name; FieldNameTuple!( typeof( this ) ) ) 89 { 90 static assert( 91 name[0] == '_', 92 "field '%s' must start with an underscore".format( name ) 93 ); 94 95 static assert( 96 name.length >= 2, 97 "field '%s' must have at least one other character after the underscore".format( name ) 98 ); 99 100 static assert( 101 __traits( getProtection, __traits( getMember, this, name ) ) != "public", 102 "field '%s' cannot be public".format( name ) 103 ); 104 } 105 } 106 107 static if( !params.suppressCtor ) 108 this( FieldTypeTuple!( typeof( this ) ) args ) @trusted 109 { 110 static foreach( i, name; FieldNameTuple!( typeof( this ) ) ) 111 __traits( getMember, this, name ) = args[i]; 112 } 113 114 // generate getters and mutation methods 115 static foreach( name; FieldNameTuple!( typeof( this ) ) ) 116 { 117 // read-only getter method 118 mixin( "auto %s() const pure nothrow @property { return this.%s; }".format( name[1 .. $], name ) ); 119 120 static if( params.enableSetters ) 121 mixin( 122 "void %01$s( typeof( this.%02$s ) x ) nothrow @property { this.%02$s = x; }" 123 .format( name[1 .. $], name ) 124 ); 125 126 static if( params.enableMutation ) 127 // mutation method 128 mixin( { 129 import std.array : appender, join; 130 import std.uni : toUpper; 131 132 enum trimmed = name[1 .. $]; 133 enum upperName = 134 name.length == 1 ? 135 trimmed.toUpper() : 136 "%s%s".format( trimmed[0].toUpper(), trimmed[1 .. $] ); 137 138 auto code = appender!string; 139 code.put( 140 "typeof( this ) with%01$s( typeof( this.%02$s ) new%01$s ) @trusted {" 141 .format( upperName, name ) 142 ); 143 144 string[] args; 145 foreach( other; FieldNameTuple!( typeof( this ) ) ) 146 args ~= name == other ? "new%s".format( upperName ) : "this.%s".format( other ); 147 148 static if( is( typeof( this ) == class ) ) 149 code.put( "return new typeof( this )( %s ); }".format( args.join( ", " ) ) ); 150 else 151 code.put( "return typeof( this )( %s ); }".format( args.join( ", " ) ) ); 152 153 return code.data; 154 }() ); 155 } 156 157 static if( params.enableLet ) 158 { 159 import dext.traits : asPointer; 160 void deconstruct( staticMap!( asPointer, FieldTypeTuple!( typeof( this ) ) ) ptrs ) nothrow @trusted 161 { 162 static foreach( i, name; FieldNameTuple!( typeof( this ) ) ) 163 *ptrs[i] = __traits( getMember, this, name ); 164 } 165 } 166 167 bool opEquals()( auto ref const typeof( this ) other ) const nothrow @trusted 168 { 169 auto eq = true; 170 171 static foreach( name; FieldNameTuple!( typeof( this ) ) ) 172 eq = eq && ( __traits( getMember, this, name ) == __traits( getMember, other, name ) ); 173 174 return eq; 175 } 176 177 static if( is( typeof( this ) == class ) ) 178 override string toString() const { return this.toStringImpl(); } 179 else 180 string toString() const { return this.toStringImpl(); } 181 182 static if( is( typeof( this ) == class ) ) 183 override size_t toHash() const nothrow @trusted { return this.toHashImpl(); } 184 else 185 size_t toHash() const nothrow @trusted { return this.toHashImpl(); } 186 187 private string toStringImpl() const 188 { 189 import std.traits : Unqual, isSomeString, isSomeChar; 190 import std.array : appender, join, replace; 191 import std.conv : to; 192 193 auto str = appender!string; 194 str.put( Unqual!( typeof( this ) ).stringof ); 195 str.put( "(" ); 196 197 string[] values; 198 199 foreach( name; FieldNameTuple!( typeof( this ) ) ) 200 { 201 alias T = typeof( __traits( getMember, this, name ) ); 202 const value = __traits( getMember, this, name ); 203 204 static if( isSomeString!T ) 205 values ~= `"%s"`.format( value.replace( "\"", "\\\"" ) ); 206 else static if( isSomeChar!T ) 207 values ~= value == '\'' || value == '\\' ? "'\\%s'".format( value ) : "'%s'".format( value ); 208 else 209 values ~= "%s".format( value ); 210 } 211 212 str.put( values.join( ", " ) ); 213 str.put( ")" ); 214 215 return str.data; 216 } 217 218 private size_t toHashImpl() const nothrow @trusted 219 { 220 import std.traits : fullyQualifiedName; 221 222 size_t hash = 486_187_739; 223 224 foreach( name; FieldNameTuple!( typeof( this ) ) ) 225 { 226 const typeName = fullyQualifiedName!( typeof( this ) ); 227 const value = __traits( getMember, this, name ); 228 229 // create a local variable so we can take the address 230 const nameTemp = name; 231 232 // hash the names to try and avoid collisions with identical structs. 233 const typeHash = typeid( string ).getHash( &typeName ); 234 const nameHash = typeid( string ).getHash( &nameTemp ); 235 const valueHash = typeid( typeof( value ) ).getHash( &value ); 236 237 hash = ( hash * 15_485_863 ) ^ typeHash ^ nameHash ^ valueHash; 238 } 239 240 return hash; 241 } 242 } 243 244 @system unittest 245 { 246 import std.typecons : Tuple, tuple; 247 import dext.let : let; 248 249 struct Mutable 250 { 251 mixin Record!( RecordParams.ofEnableSetters ); 252 private int _x; 253 } 254 255 auto mut = Mutable( 5 ); 256 assert( mut.x == 5 ); 257 mut.x = 10; 258 assert( mut.x == 10 ); 259 260 final class RecordClass 261 { 262 mixin Record; 263 private int _x; 264 } 265 266 auto klass = new RecordClass( 5 ); 267 assert( klass.x == 5 ); 268 269 struct Point 270 { 271 mixin Record!( RecordParams.ofEnableLet ); 272 private int _x, _y; 273 } 274 275 struct Size 276 { 277 mixin Record; 278 private int _width, _height; 279 } 280 281 struct Rectangle 282 { 283 mixin Record; 284 private Point _location; 285 private Size _size; 286 } 287 288 struct Person 289 { 290 mixin Record!( RecordParams.ofEnableLet ); 291 292 private { 293 string _firstName; 294 string[] _middleNames; 295 string _lastName; 296 ubyte _age; 297 } 298 } 299 300 // to ensure non-primitive types defined in other modules/packages work properly 301 struct External 302 { 303 mixin Record; 304 private Tuple!( int, int ) _tup; 305 } 306 307 auto ext = External( tuple( 50, 100 ) ); 308 309 int e1, e2; 310 let( e1, e2 ) = ext.tup; 311 312 assert( e1 == 50 ); 313 assert( e2 == 100 ); 314 315 // test to ensure arrays work 316 auto richardPryor = Person( 317 "Richard", 318 [ "Franklin", "Lennox", "Thomas" ], 319 "Pryor", 320 65 321 ); 322 323 string[] middleNames; 324 string _, n1, n2, n3; 325 ubyte __; 326 327 assert( richardPryor.middleNames == [ "Franklin", "Lennox", "Thomas" ] ); 328 329 let( _, middleNames, _, __ ) = richardPryor; 330 let( n1, n2, n3 ) = middleNames; 331 332 assert( n1 == "Franklin" ); 333 assert( n2 == "Lennox" ); 334 assert( n3 == "Thomas" ); 335 336 auto a = Point( 1, 2 ); 337 auto b = Point( 3, 4 ); 338 339 int x, y; 340 let( x, y ) = a; 341 342 assert( x == 1 ); 343 assert( y == 2 ); 344 345 assert( a != b && b != a ); 346 assert( a.toHash() != b.toHash() ); 347 348 auto c = Size( 50, 100 ); 349 auto d = Rectangle( a, c ); 350 351 assert( d.location == a ); 352 assert( d.size == c ); 353 }