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 }