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 }