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 }