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 }