1 /**
2 Copyright: Copyright (c) 2020, Joakim Brännström. All rights reserved.
3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0)
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 */
6 module my.fsm;
7 
8 import std.format : format;
9 
10 /** A state machine derived from the types it is based on.
11  *
12  * Each state have its unique, temporary data that it works on. If a state
13  * needs persistent data then look into using the `TypeDataMap`.
14  *
15  * The state transitions are calculated by `next` and the actions are performed
16  * by `act`.
17  *
18  * See the unittests for example of how to use the implementation.
19  */
20 struct Fsm(StateTT...) {
21     static import sumtype;
22 
23     alias StateT = sumtype.SumType!StateTT;
24 
25     /// The states and state specific data.
26     StateT state;
27 
28     /// Log messages of the last state transition (next).
29     /// Only updated in debug build.
30     alias LogFn = void delegate(string msg) @safe;
31     LogFn logger;
32 
33     /// Helper function to convert the return type to `StateT`.
34     static StateT opCall(T)(auto ref T a) {
35         return StateT(a);
36     }
37 }
38 
39 /// Transition to the next state.
40 template next(handlers...) {
41     import std.traits : Parameters, ReturnType;
42 
43     template CoerceReturn(Self, alias Matcher) {
44         alias P = Parameters!Matcher;
45         static if (is(ReturnType!Matcher == Self.StateT)) {
46             alias CoerceReturn = Matcher;
47         } else {
48             Self.StateT CoerceReturn(P[0] a) {
49                 return Self.StateT(Matcher(a));
50             }
51         }
52     }
53 
54     void next(Self)(auto ref Self self) if (is(Self : Fsm!StateT, StateT...)) {
55         import std.meta : staticMap;
56         static import sumtype;
57 
58         alias CoerceReturnSelf(alias Matcher) = CoerceReturn!(Self, Matcher);
59         alias Handlers = staticMap!(CoerceReturnSelf, handlers);
60 
61         auto nextSt = sumtype.match!Handlers(self.state);
62         if (self.logger)
63             self.logger(format!"%s -> %s"(self.state, nextSt));
64 
65         self.state = nextSt;
66     }
67 }
68 
69 /// Act on the current state. Use `(ref S)` to modify the states data.
70 template act(handlers...) {
71     void act(Self)(auto ref Self self) if (is(Self : Fsm!StateT, StateT...)) {
72         static import sumtype;
73 
74         sumtype.match!handlers(self.state);
75     }
76 }
77 
78 @("shall transition the fsm from A to B|C")
79 unittest {
80     struct Global {
81         int x;
82     }
83 
84     struct A {
85     }
86 
87     struct B {
88         int x;
89     }
90 
91     struct C {
92         bool x;
93     }
94 
95     Global global;
96     Fsm!(A, B, C) fsm;
97     bool running = true;
98 
99     while (running) {
100         fsm.next!((A a) { global.x++; return fsm(B(0)); }, (B a) {
101             running = false;
102             if (a.x > 3)
103                 return fsm(C(true));
104             return fsm(a);
105         }, (C a) { running = false; return a; });
106 
107         fsm.act!((A a) {}, (ref B a) { a.x++; }, (C a) {});
108     }
109 
110     assert(global.x == 1);
111 }
112 
113 @("shall use a struct to provide the state callbacks")
114 unittest {
115     static struct A {
116     }
117 
118     static struct B {
119     }
120 
121     static struct Foo {
122         Fsm!(A, B) fsm;
123         bool running = true;
124 
125         void opCall(A a) {
126         }
127 
128         void opCall(B b) {
129             running = false;
130         }
131     }
132 
133     Foo foo;
134     while (foo.running) {
135         foo.fsm.next!((A a) => B.init, (B a) => a);
136         foo.fsm.act!foo;
137     }
138 }
139 
140 /** Hold a mapping between a Type and data.
141  *
142  * The get function is used to get the corresponding data.
143  *
144  * This is useful when e.g. combined with a state machine to retrieve the state
145  * local data if a state is represented as a type.
146  *
147  * Params:
148  *  RawDataT = type holding the data, retrieved via opIndex
149  *  Ts = the types mapping to RawDataT by their position
150  */
151 struct TypeDataMap(RawDataT, Ts...)
152         if (is(RawDataT : DataT!Args, alias DataT, Args...)) {
153     alias SrcT = Ts;
154     RawDataT data;
155 
156     this(RawDataT a) {
157         this.data = a;
158     }
159 
160     void opAssign(RawDataT a) {
161         this.data = a;
162     }
163 
164     static if (is(RawDataT : DataT!Args, alias DataT, Args...))
165         static assert(Ts.length == Args.length,
166                 "Mismatch between Tuple and TypeMap template arguments");
167 }
168 
169 auto ref get(T, TMap)(auto ref TMap tmap)
170         if (is(TMap : TypeDataMap!(W, SrcT), W, SrcT...)) {
171     template Index(size_t Idx, T, Ts...) {
172         static if (Ts.length == 0) {
173             static assert(0, "Type " ~ T.stringof ~ " not found in the TypeMap");
174         } else static if (is(T == Ts[0])) {
175             enum Index = Idx;
176         } else {
177             enum Index = Index!(Idx + 1, T, Ts[1 .. $]);
178         }
179     }
180 
181     return tmap.data[Index!(0, T, TMap.SrcT)];
182 }
183 
184 @("shall retrieve the data for the type")
185 unittest {
186     import std.typecons : Tuple;
187 
188     TypeDataMap!(Tuple!(int, bool), bool, int) a;
189     static assert(is(typeof(a.get!bool) == int), "wrong type");
190     a.data[1] = true;
191     assert(a.get!int == true);
192 }