|
Pattern matched state machines are how Prolog, one of Erlang's parent languages, is expected to work. This is how they implement horn clauses. I can't speak for parent poster, but, I imagine that they mean something like this (which is also almost valid prolog, and mozart/oz, and some others:) -module(pattern_match_tl_fsm).
-export([ next/1, disable/1, enable/1, enabled/1, may_pass/1, create/0 ]).
next(red) -> green;
next(green) -> yellow; % allow the lack of a match to throw for off; there is
next(yellow) -> red. % no next state for a light that's turned off
enable(off) -> red. % allow the lack of a match to throw for every color;
% only lights turned off may be turned on
disable(red) -> off;
disable(green) -> off; % allow the lack of a match to throw for off; cannot
disable(yellow) -> off. % disable if already disabled
enabled(off) -> false;
enabled(_) -> true.
may_pass(green) -> true;
may_pass(yellow) -> careful;
may_pass(red) -> false;
may_pass(off) -> careful.
create() -> off.
And then you can do stuff like TrafficLight = pattern_match_tl_fsm:create(),
TurnedOn = pattern_match_tl_fsm:enable(TrafficLight),
Step2 = pattern_match_tl_fsm:next(TurnedOn),
etc
For very simple things this is probably a great choice, and OP is correct to suggest that this is very debuggable, especially if those states are tagged tuples instead of lazy-example atoms. However, the rate at which it doesn't scale is magnificent. By contrast: import { sm } from 'jssm';
const make_light = () =>
sm`off -> red => green => yellow => red; [red yellow green] ~> off;`,
const safety = { red: false, green: true, yellow: 'careful', off: 'careful' },
enabled = light => light.state() !== 'off',
may_pass = light => safety[light.state()];
export { make_light, enabled, may_pass };
And that gives you basically the same interface for way less code.Of course, bringing in an interpreter and/or a compiler isn't zero cost, so if you only have a couple of the straight module ones, the first notation honestly can be a pretty good approach, and given Erlang's module compositional nature, it's frequent to have things that in other languages would be unrealistically simple. By example, the TCP/IP state machine is 11 states and 19 edges, and in my opinion would make sense in the first notation as its own standalone module in erlang. I believe that parent post has a solid point. The reason one tool hasn't universally won is that there isn't a best way to do this, style of thing. Trade-offs. Still, with the sm`` notation, I find that I very frequently one-liner state machines with <= 6 states, and that in my head they start to act like something adjacent to types. Like ... behavioral enumerations? Things that simple which behave in a fixed fashion are sorta-types in my head, and they become very easy to use as a structural element. I dunno. They start getting low-ish level-ish to me, especially when they're behind generators, even though in reality that isn't true at all. The library is sufficiently fast and sufficiently tested that I feel comfortable using them that way, at least. Also, when you start thinking about larger state machines, which sometimes have 50 states and 200 transitions, it is my opinion that that they grow at such different rates can quickly become important. When I say the TCP machine with 11 and 19 makes sense as a standalone, to me, that's getting not too far from the upper limit of where I'd think spelling it out manually was smart. (Still, to be fair, most FSMs are simple, rather than complex, so that's not all that harsh of a cutting criterion in the balance.) Even when hyper-dense, my opinion is that the sm`` arrow notation can easily carry 10x the structure on-screen and be comfortably readable than the typical datastructure based approach. For me, the `jssm` chain is modifying how I think about code in some subtle ways, and that makes me feel like there might be actual value there. I hope you'll give it a shot and tell me whether or not you agree. HTH |
Kind of, but notice that I mentioned receive statements. Those are important. I'll show what I mean this time, because I guess people aren't picturing what I'm describing:
https://gist.github.com/tsutsu/5543bcaec4448b1de0023fbde31c8...
(Got way too long to fit in an HN comment.)
> they start to act like something adjacent to types
Not just "adjacent"; automata are monoids!
I think I saw a language reify this concept once — where you were able to declare an abstract state-machine interface/protocol, and then the compiler would check that for implementations of that interface/protocol, each function actually has the proper precondition states (i.e. only reachable from those states' functions) and postcondition states (i.e. has live code-paths reaching exactly those functions.)
Interestingly, I think it did this with individual types for each state-transition function, rather than there being a single higher-level type for the digraph of functions.
Anyone know what language (or maybe it was a computational proof assistant) I'm referring to?