1 /** 2 Provides functionality for easily creating immutable record types. 3 4 Authors: Tony J. Hudgins 5 Copyright: Copyright © 2017, Tony J. Hudgins 6 License: MIT 7 */ 8 module dext.record; 9 10 private { 11 import std.traits : isSomeString; 12 13 template isIdentifier( T... ) if( T.length == 1 ) 14 { 15 static if( is( T[0] ) ) 16 enum isIdentifier = false; 17 else 18 enum isIdentifier = stringIsIdentifier!( T[0] ); 19 } 20 21 template stringIsIdentifier( alias S ) if( isSomeString!( typeof( S ) ) ) 22 { 23 import std.algorithm.searching : all; 24 import std.uni : isAlpha, isAlphaNum; 25 26 static if( S.length == 0 ) 27 enum stringIsIdentifier = false; 28 static if( S.length == 1 ) 29 enum stringIsIdentifier = S[0] == '_' || S[0].isAlpha; 30 else 31 enum stringIsIdentifier = ( S[0] == '_' || S[0].isAlpha ) 32 && S[1 .. $].all!( c => c == '_' || c.isAlphaNum ); 33 } 34 35 template areTypeNamePairs( T... ) if( T.length % 2 == 0 ) 36 { 37 static if( T.length == 2 ) 38 enum areTypeNamePairs = is( T[0] ) && 39 isIdentifier!( T[1] ); 40 else 41 enum areTypeNamePairs = is( T[0] ) && 42 isIdentifier!( T[1] ) && 43 areTypeNamePairs!( T[2 .. $] ); 44 } 45 } 46 47 /++ 48 Immutable value type that automatically implements equality (==, !=), 49 hashcode computation (toHash), and stringification (toString). 50 The purpose of this struct is act similarly to record types in functional 51 programming languages like OCaml and Haskell. 52 53 Authors: Tony J. Hudgins 54 Copyright: Copyright © 2017, Tony J. Hudgins 55 License: MIT 56 57 Examples: 58 --------- 59 // define a point int 2D space 60 alias Point = Record!( 61 int, "x", 62 int, "y" 63 ); 64 65 auto a = Point( 3, 7 ); 66 auto b = Point( 9, 6 ); 67 68 // Euclidean distance 69 auto distance( in Point a, in Point b ) 70 { 71 import std.math : sqrt; 72 73 return sqrt( ( a.x - b.x ) ^^ 2f + ( a.y - b.y ) ^^ 2f ); 74 } 75 76 auto dist = distance( a, b ); // 6.08276 77 --------- 78 +/ 79 struct Record( T... ) if( T.length % 2 == 0 && areTypeNamePairs!T ) 80 { 81 import std.meta : Filter, staticMap; 82 import std.traits : fullyQualifiedName; 83 84 private { 85 alias toPointer( T ) = T*; 86 template isType( T... ) if( T.length == 1 ) 87 { 88 enum isType = is( T[0] ); 89 } 90 91 alias Self = typeof( this ); 92 alias Types = Filter!( isType, T ); 93 94 static immutable _fieldNames = [ Filter!( isIdentifier, T ) ]; 95 } 96 97 // private backing fields and getter-only properties 98 mixin( (){ 99 import std.array : appender; 100 import std.range : zip; 101 102 static immutable typeNames = [ staticMap!( fullyQualifiedName, Types ) ]; 103 auto code = appender!string; 104 105 foreach( pair; typeNames.zip( _fieldNames ) ) 106 { 107 // Private backing field 108 code.put( "private " ); 109 code.put( pair[0] ); // type name 110 code.put( " _" ); // field names are prefixed with an underscore 111 code.put( pair[1] ); // field name; 112 code.put( ";" ); 113 114 // Public getter-only property 115 //code.put( pair[0] ); // type name 116 code.put( "auto " ); 117 code.put( pair[1] ); // field name 118 code.put( "() const @property" ); 119 code.put( "{ return this._" ); 120 code.put( pair[1] ); // field name 121 code.put( "; }" ); 122 } 123 124 return code.data; 125 }() ); 126 127 /++ 128 Accepts parameters matching the types of the fields declared in the template arguments 129 and automatically assigns values to the backing fields. 130 131 Authors: Tony J. Hudgins 132 Copyright: Copyright © 2017, Tony J. Hudgins 133 License: MIT 134 +/ 135 this( Types values ) 136 { 137 import std..string : format; 138 foreach( i, _; Types ) 139 mixin( "this._%s = values[%u];".format( _fieldNames[i], i ) ); 140 } 141 142 /** 143 Deconstruction support for the <a href="/dext/dext/let">let module</a>. 144 145 Authors: Tony J. Hudgins 146 Copyright: Copyright © 2017, Tony J. Hudgins 147 License: MIT 148 */ 149 void deconstruct( staticMap!( toPointer, Types ) ptrs ) const 150 { 151 import std.traits : isArray; 152 153 foreach( i, T; Types ) 154 { 155 static if( isArray!T ) 156 *(ptrs[i]) = (*this.pointerTo!( _fieldNames[i] ) ).dup; 157 else 158 *(ptrs[i]) = *this.pointerTo!( _fieldNames[i] ); 159 } 160 } 161 162 /** 163 Implements equality comparison with other records of the same type. 164 Two records are only considered equal if all fields are equal. 165 166 Authors: Tony J. Hudgins 167 Copyright: Copyright © 2017, Tony J. Hudgins 168 License: MIT 169 */ 170 bool opEquals()( auto ref const Self other ) const nothrow @trusted 171 { 172 auto eq = true; 173 foreach( i, _; Types ) 174 { 175 auto thisPtr = this.pointerTo!( _fieldNames[i] ); 176 auto otherPtr = other.pointerTo!( _fieldNames[i] ); 177 178 eq = eq && *thisPtr == *otherPtr; 179 } 180 181 return eq; 182 } 183 184 /** 185 Computes the hashcode of this record based on the hashes of the fields, 186 as well as the field names to avoid collisions with other records with 187 the same number and type of fields. 188 189 Authors: Tony J. Hudgins 190 Copyright: Copyright © 2017, Tony J. Hudgins 191 License: MIT 192 */ 193 size_t toHash() const nothrow @trusted 194 { 195 size_t hash = 486_187_739; 196 197 foreach( i, T; Types ) 198 { 199 auto ptr = this.pointerTo!( _fieldNames[i] ); 200 auto nameHash = 201 typeid( typeof( _fieldNames[i] ) ) 202 .getHash( cast(const(void)*)&_fieldNames[i] ); 203 204 auto fieldHash = typeid( T ).getHash( cast(const(void)*)ptr ); 205 206 hash = ( hash * 15_485_863 ) ^ nameHash ^ fieldHash; 207 } 208 209 return hash; 210 } 211 212 /** 213 Stringifies and formats all fields. 214 215 Authors: Tony J. Hudgins 216 Copyright: Copyright © 2017, Tony J. Hudgins 217 License: MIT 218 */ 219 string toString() const 220 { 221 import std.array : appender; 222 import std.conv : to; 223 224 auto str = appender!string; 225 str.put( "{ " ); 226 227 enum len = Types.length; 228 foreach( i, _; Types ) 229 { 230 auto ptr = this.pointerTo!( _fieldNames[i] ); 231 str.put( _fieldNames[i] ); 232 str.put( " = " ); 233 str.put( (*ptr).to!string ); 234 235 if( i < len - 1 ) 236 str.put( ", " ); 237 } 238 239 str.put( " }" ); 240 return str.data; 241 } 242 243 private auto pointerTo( string name )() const nothrow @trusted 244 { 245 mixin( "return &this._" ~ name ~ ";" ); 246 } 247 } 248 249 @system unittest 250 { 251 import dext.let : let; 252 253 alias Point = Record!( 254 int, "x", 255 int, "y" 256 ); 257 258 alias Size = Record!( 259 int, "width", 260 int, "height" 261 ); 262 263 alias Rectangle = Record!( 264 Point, "location", 265 Size, "size" 266 ); 267 268 alias Person = Record!( 269 string, "firstName", 270 string[], "middleNames", 271 string, "lastName", 272 ubyte, "age" 273 ); 274 275 // test to ensure arrays work 276 auto richardPryor = Person( 277 "Richard", 278 [ "Franklin", "Lennox", "Thomas" ], 279 "Pryor", 280 65 281 ); 282 283 string[] middleNames; 284 string _, n1, n2, n3; 285 ubyte __; 286 287 assert( richardPryor.middleNames == [ "Franklin", "Lennox", "Thomas" ] ); 288 289 let( _, middleNames, _, __ ) = richardPryor; 290 let( n1, n2, n3 ) = middleNames; 291 292 assert( n1 == "Franklin" ); 293 assert( n2 == "Lennox" ); 294 assert( n3 == "Thomas" ); 295 296 auto a = Point( 1, 2 ); 297 auto b = Point( 3, 4 ); 298 299 int x, y; 300 let( x, y ) = a; 301 302 assert( x == 1 ); 303 assert( y == 2 ); 304 305 assert( a != b && b != a ); 306 assert( a.toHash() != b.toHash() ); 307 308 auto c = Size( 50, 100 ); 309 auto d = Rectangle( a, c ); 310 311 assert( d.location == a ); 312 assert( d.size == c ); 313 }