1 /**
2     Create a function that encapsulates if/else-if/else logic
3 */
4 module ddash.functional.cond;
5 
6 ///
7 unittest {
8     alias lessThanZero = (a) => a < 0;
9     alias greaterOrEqualThan10 = (a) => a >= 10;
10     alias identity = (a) => a;
11     alias negate = (a) => -a;
12 
13     alias abs = cond!(
14         a => a == 1, 42,
15         2, a => a * 2,
16         3, 999,
17         lessThanZero, negate,
18         greaterOrEqualThan10, identity,
19         a => a * 100,
20     );
21 
22     assert(abs(1) == 42);
23     assert(abs(2) == 4);
24     assert(abs(3) == 999);
25     assert(abs(4) == 400);
26     assert(abs(5) == 500);
27     assert(abs(-3) == 3);
28     assert(abs(11) == 11);
29 
30     alias str = cond!(
31         a => a < 1, "1",
32         a => a < 5, "5",
33         a => a < 10, "10",
34         "moar"
35     );
36 
37     assert(str(0) == "1");
38     assert(str(2) == "5");
39     assert(str(7) == "10");
40     assert(str(11) == "moar");
41 }
42 
43 import ddash.common;
44 
45 /**
46     Takes pairs of predicates and transforms and uses the first transform that a predicate
47     return true for.
48 
49     For example in the following call:
50 
51     ---
52     cond!(
53         pred1, trsnaform1,
54         pred2, trsnaform2,
55         .
56         .
57         predN, trsnaformN,
58         default
59     )(value);
60     ---
61 
62     `predN` can either be a unary function or an expression. `transformN` can be a unary,
63     or binary function, or an expression. The unary function cannot be a string since
64     narrow strings will be treated as values. `default` is treated as a transform.
65 
66     All transforms must return a common type, or void. If there's a common return then a
67     default transform must be provided. If all the transforms return void then a default
68     is not needed.
69 
70     Params:
71         statements = pairs of predicates and transforms followed by a default transform
72         value = the value to evaluate
73 
74     Returns:
75         Whatever is returned by the result that wins
76 
77     Benchmarks:
78 
79     A sample `cond` if/else chain was used with a mixture of expressions and unary functions and
80     iterated over. A couple of hand written if/else chains were compared. The first used lambdas
81     to evaluate their conditions, the second just used inline code.
82 
83     ---
84     Benchmarking cond against hand written if/elses
85       cond:           2 hnsecs
86       hand written 1: 0 hnsecs
87       hand written 2: 0 hnsecs
88     ---
89 */
90 template cond(statements...) {
91     import std.traits: isExpressions, isCallable, arity, isNarrowString;
92     import std.functional: unaryFun;
93     import bolts.traits: isUnaryOver;
94     static template resolve(alias f) {
95         auto resolve(V...)(V values) {
96             static if (isUnaryOver!(f, V) && !isNarrowString!(typeof(f))){
97                 return f(values);
98             } else static if (isCallable!f && arity!f == 0) {
99                 return f();
100             } else static if (isExpressions!f) {
101                 return f;
102             } else {
103                 static assert(
104                     0,
105                     "Could not resolve " ~ f.stringof ~ " with values " ~ V.stringof
106                 );
107             }
108         }
109     }
110     auto cond(T)(T value) {
111         immutable cases = statements.length / 2;
112         static foreach(I; 0 .. cases) {{
113             static if (isExpressions!(statements[I * 2])) {
114                 immutable c = statements[I * 2] == value;
115             } else {
116                 immutable c = statements[I * 2](value);
117             }
118             if (c) {
119                 return resolve!(statements[I * 2 + 1])(value);
120             }
121         }}
122         // Return default case only if one was provided
123         static if (cases * 2 < statements.length) {
124             return resolve!(statements[$ - 1])(value);
125         } else {
126             static if (statements.length > 0) {
127                 static assert(
128                     is(typeof(return) == void),
129                     "no default for " ~ typeof(return).stringof
130                         ~ " return. if any transform returns non void a default transform must be provided"
131                     );
132             }
133             return;
134         }
135     }
136 }
137 
138 unittest {
139     static assert(
140         is(
141             typeof(
142                 cond!(
143                     1, () {}
144                 )(3)
145             )
146             == void
147         )
148     );
149 
150     // If transforms return, default must be provided
151     // TODO: Re-evaluate this. It'll be a compiler error if one o the matches
152     // does not cond anyway so maybe this can be relaxed so that if a user knows
153     // they are matching all conditions then it's ok.
154     static assert(
155         !__traits(
156             compiles,
157             {
158                 cond!(
159                     1, 2
160                 )(3);
161             }
162         )
163     );
164 }
165 
166 unittest {
167     static auto g() { return 10; }
168     assert(cond!(1, g, 4)(1) == 10);
169 }