1 /**
2 Provides functionality for unpacking values from collections into local variables,
3 similar to C++'s `std::tie`.
4 
5 Authors: Tony J. Hudgins
6 Copyright: Copyright © 2017, Tony J. Hudgins
7 License: MIT
8 */
9 module dext.let;
10 
11 /++
12 Unpacks forward ranges, input ranges, static arrays,
13 dynamic arrays, tuples, and user-defined types (via deconstructor methods)
14 into the specified variables.
15 
16 Authors: Tony J. Hudgins
17 Copyright: Copyright © 2017, Tony J. Hudgins
18 License: MIT
19 
20 Examples:
21 ---------
22 // unpack an array
23 auto nums = [ 1, 2, 3 ];
24 
25 int a, b, c;
26 let( a, b, c ) = nums; // access the array indices and assign them to the a, b, and c variables
27 
28 // unpack a struct with a built-in deconstructor
29 struct Point
30 {
31     immutable int x;
32     immutable int y;
33 
34     void deconstruct( int* x, int* y )
35     {
36         *x = this.x;
37         *y = this.y;
38     }
39 }
40 
41 auto pt = Point( 5, 6 );
42 int x, y;
43 let( x, y ) = pt; // call Point.deconstruct() with pointers to the x and y variables
44 ---------
45 +/
46 auto let( Ts... )( ref Ts locals )
47 {
48     import core.exception : RangeError;
49     import std.meta : allSatisfy, staticMap;
50     import std.range.primitives : ElementType, isForwardRange, isInputRange;
51     import std..string : format;
52     import std.traits : CommonType, fullyQualifiedName, isArray, isCallable,
53                         isFunctionPointer, isPointer, isStaticArray, Parameters,
54                         ReturnType, Unqual;
55     import std.typecons : Tuple;
56 
57     alias toPointer( T ) = T*;
58     alias TPointers = staticMap!( toPointer, Ts );
59     alias TCommon = CommonType!Ts;
60 
61     enum hasDeconstructorMethod( T ) = is( typeof( {
62         static assert( __traits( hasMember, T, "deconstruct" ) );
63 
64         enum method = &__traits( getMember, T, "deconstruct" );
65         static assert( isCallable!method );
66         static assert( is( ReturnType!method == void ) );
67 
68         alias locals = Parameters!method;
69         static assert( locals.length == Ts.length );
70         static assert( allSatisfy!( isPointer, locals ) );
71         static assert( is( locals == TPointers ) );
72     } ) );
73 
74     static struct LetAssigner
75     {
76         private {
77             TPointers pointers;
78         }
79 
80         this( Ts... )( ref Ts locals )
81         {
82             foreach( i, ref x; locals )
83                 this.pointers[i] = &x;
84         }
85 
86         void opAssign( Tuple!Ts tup )
87         {
88             foreach( i, _; Ts )
89                 *this.pointers[i] = tup[i];
90         }
91 
92         static if( !is( TCommon == void ) )
93         {
94             void opAssign( TCommon[] arr )
95             {
96                 if( arr.length < Ts.length )
97                     throw new RangeError(
98                         "Array has too few items to unpack (expecting at least %u)".format( Ts.length )
99                     );
100 
101                 foreach( i, t; Ts )
102                     *this.pointers[i] = cast(t)arr[i];
103             }
104         }
105 
106         void opAssign( R )( R r )
107             if( !isArray!R && isInputRange!R &&
108                 !isForwardRange!R && !is( TCommon == void ) &&
109                 is( ElementType!R : TCommon )
110             )
111         {
112             this.rangeImpl( r );
113         }
114 
115         void opAssign( R )( R r )
116             if( !isArray!R && isForwardRange!R &&
117                 !is( TCommon == void ) && is( ElementType!R : TCommon )
118             )
119         {
120             this.rangeImpl( r.save() );
121         }
122 
123         private void rangeImpl( R )( R r )
124         {
125             foreach( i, t; Ts )
126             {
127                 if( r.empty )
128                     throw new RangeError(
129                         "Range has too few items to unpack (expecting at least %u)".format( Ts.length )
130                     );
131 
132                 *this.pointers[i] = cast(t)r.front;
133                 r.popFront();
134             }
135         }
136 
137         void opAssign( T )( T value ) if( hasDeconstructorMethod!T )
138         {
139             value.deconstruct( this.pointers );
140         }
141     }
142 
143     return LetAssigner( locals );
144 }
145 
146 @system unittest
147 {
148     import std.range : iota;
149 
150     struct Point
151     {
152         int x, y;
153 
154         void deconstruct( int* x, int* y )
155         {
156             *x = this.x;
157             *y = this.y;
158         }
159     }
160 
161     int[] dynamic = [ 1, 2 ];
162     float[2] static_ = [ 3f, 4f ];
163     auto nums = iota( 0, 500 );
164     auto pt = Point( 10, 15 );
165 
166     // test dynamic arrays
167     int a, b;
168     let( a, b ) = dynamic;
169 
170     assert( a == dynamic[0] );
171     assert( b == dynamic[1] );
172 
173     // test static arrays
174     float c, d;
175     let( c, d ) = static_;
176 
177     assert( c == static_[0] );
178     assert( d == static_[1] );
179 
180     // test ranges
181     int i, j, k;
182     let( i, j, k ) = nums;
183 
184     assert( i == 0 );
185     assert( j == 1 );
186     assert( k == 2 );
187 
188     // test deconstructor
189     int x, y;
190     let( x, y ) = pt;
191 
192     assert( x == pt.x );
193     assert( y == pt.y );
194 }