1750654189
2025-06-23 01:47:00
ポリステート:構成可能な有限状態マシン
プロジェクトルートで次のコマンドを実行することにより、依存関係としてポリステートをダウンロードして追加します。
zig fetch --save git+https://github.com/sdzx-1/polystate.git
次に、PolyStateを依存関係として追加し、build.zigにモジュールとアーティファクトをインポートします。
const polystate = b.dependency("polystate", .{
.target = target,
.optimize = optimize,
});
通常、モジュールにモジュールを追加します。
exe_mod.addImport("polystate", typed_fsm.module("root"));
このドキュメントを書くことは、私が予想していたよりも困難でした。私はしばらくこのライブラリを書いて使用してきましたが、他の人にはっきりと説明するのは簡単ではありません。このドキュメントを読んだ後にご質問がある場合は、お気軽にお問い合わせください。混乱を解消してうれしいです!
- タイプレベルで状態マシンのステータスを記録します。
- タイプ構成を通じて、構成可能な状態マシンを実現します。
有限状態マシン(FSM)は、強力なプログラミングパターンです。複合性とタイプの安全性と組み合わせると、さらに理想的なプログラミングパラダイムになります。
polystate ライブラリは、この目的のために正確に設計されています。これを達成するには、いくつかの簡単なプログラミング規則に従う必要があります。これらの慣習は非常に簡単であり、彼らがもたらす利益は完全に価値があります。
- 構成宣言を通じてプログラムの全体的な動作を定義します。 これは、タイプレベルでプログラムの全体的な動作を指定する機能を獲得することを意味します。これにより、命令プログラム構造の正確性が大幅に向上します。このプログラミングスタイルは、タイプと構成の観点からプログラムの状態を再設計し、それによってコードの複合性を高めることも促進します。
- 単純な状態を構成することにより、複雑な状態マシンを構築します。 初めて、タイプ構成を通じてセマンティックレベルのコードの再利用を実現できます。言い換えれば、タイプレベルでセマンティックレベルのコードの再利用を表現する方法を見つけました。このアプローチは、同時に3つの効果を達成します:簡潔さ、正確性、安全性。
- 状態図を自動的に生成します。 プログラムの全体的な動作は宣言によって決定されるため、
polystate状態図を自動的に生成できます。ユーザーは、これらの図を通じてプログラムの全体的な動作を直感的に理解できます。
これはすべて、命令的なプログラミングのための大きな前進を表していると思います!
シンプルな状態マシンの具体的な例から始めましょう。コードのコメントを通じて、このライブラリのコアデザイン哲学を詳細に説明します。
const std = @import("std");
const polystate = @import("polystate");
pub fn main() !void {
var st: GST = .{};
/// Determine an initial state
const wa = Example.Wit(Example.a){};
/// Start executing the state machine with the message handler of this initial state
/// The reason for using handler_normal here is related to tail-call optimization, which I will explain in detail later.
wa.handler_normal(&st);
}
pub const GST = struct {
counter_a: i64 = 0,
counter_b: i64 = 0,
};
/// `polystate` has two core state types: FST (FSM Type) and GST (Global State Type).
/// FST must be an enum type. In this example, the FST is `Example`, which defines all the states of our state machine. In other words, it defines the set of states we will track at the type level.
/// GST is the global data. In this example, the GST is defined above with two fields, `counter_a` and `counter_b`, representing the data needed for state `a` and state `b`, respectively.
/// When we compose states, what we really want is to compose state handler functions, which implies a requirement for global data.
/// Therefore, the first programming convention is: the handler function for any state has access to the GST (i.e., global data), but users should try to use only the data corresponding to the current state.
/// For example, in the handler function for state `a`, you should try to use only the data `counter_a`.
/// This can be easily achieved through some naming conventions, and it's easy to create corresponding generic functions through metaprogramming, but the specific implementation is outside the scope of `polystate`.
const Example = enum {
/// Three concrete states are defined here
exit,
a,
b,
/// `Wit` is a core concept in `polystate`, short for Witness. The term comes from [Haskell](https://serokell.io/blog/haskell-type-level-witness), where it's called a 'type witness' or 'runtime evidence'.
/// The core concepts of a finite state machine include four parts: state, message, message handler function, and message generator function. I will detail these parts in the example below.
/// The purpose of the `Wit` function is to specify the state information contained in a message.
pub fn Wit(val: anytype) type {
return polystate.Witness(@This(), GST, null, polystate.val_to_sdzx(@This(), val));
}
/// This is the second programming convention: The FST needs a public declaration that contains the specific content of the state. By adding `ST` after the state name, we implicitly associate the state with its specific content.
/// In this example, this corresponds to the public declarations below:
/// exit ~ exitST
/// a ~ aST
/// b ~ bST
/// Here, `exitST` describes the four parts for the `exit` state: state, message, message handler function, and message generator function.
/// Since the `exit` state has no messages, it also has no message generator function.
/// This is the third programming convention: The implementation of a state's specific content must contain a function: `pub fn handler(*GST) void` or `pub fn conthandler(*GST) ContR`.
/// They represent the message handler function. The former means the state machine has full control of the control flow. The latter means a continuation function is returned, leaving the external caller to invoke the continuation function and take control of the flow.
pub const exitST = union(enum) {
pub fn handler(ist: *GST) void {
std.debug.print("exitn", .{});
std.debug.print("st: {any}n", .{ist.*});
}
};
pub const aST = a_st;
pub const bST = b_st;
};
/// This describes the four parts for state `a`: state, message, message handler function, and message generator function.
/// 1. State
/// The state here is `a`.
pub const a_st = union(enum) {
/// 2. Message
/// A tagged union is used here to describe the messages, and `Wit` is used to describe the state we are about to transition to.
AddOneThenToB: Example.Wit(Example.b),
Exit: Example.Wit(Example.exit),
/// 3. Message Handler Function
/// Handles all messages generated by `genMsg`.
pub fn handler(ist: *GST) void {
switch (genMsg(ist)) {
.AddOneThenToB => |wit| {
ist.counter_a += 1;
/// This is the fourth programming convention: At the end of the message handling block, you must call `wit.handler(ist)` or similar code.
/// This indicates that the message handler function of the new state will be executed. The new state is controlled by the `Wit` function of the message.
wit.handler(ist);
},
.Exit => |wit| wit.handler(ist),
}
}
/// 4. Message Generator Function
/// If the value of `counter_a` is greater than 3, return `.Exit`.
/// Otherwise, return `.AddOneThenToB`.
/// The messages generated and handled here are defined in part 2 above.
fn genMsg(ist: *GST) @This() {
if (ist.counter_a > 3) return .Exit;
return .AddOneThenToB;
}
};
pub const b_st = union(enum) {
AddOneThenToA: Example.Wit(Example.a),
pub fn handler(ist: *GST) void {
switch (genMsg()) {
.AddOneThenToA => |wit| {
ist.counter_b += 1;
wit.handler(ist);
},
}
}
fn genMsg() @This() {
return .AddOneThenToA;
}
};
上記は、単純な状態マシンを構築する方法を示す簡単な例です polystate。この例は実証されていません polystate最も強力な機能: 複合性。
新しい状態を追加して、上記の例を変更させてください。 yes_or_no、構成可能性を実証する。上記と同じコードの一部を省略します。この例の完全なコードを見つけることができます ここ。
const std = @import("std");
const polystate = @import("polystate");
pub fn main() !void {
...
}
pub const GST = struct {
...
buf: [10] u8 = @splat(0),
};
///Example
const Example = enum {
exit,
a,
b,
/// A new state `yes_or_no` is defined here
yes_or_no,
pub fn Wit(val: anytype) type {
...
}
pub const exitST = union(enum) {
...
};
pub const aST = a_st;
pub const bST = b_st;
/// The specific implementation of the new state is a function that depends on two state parameters: `yes` and `no`.
/// Its semantic is to provide an interactive choice for the user: if the user chooses 'yes', it transitions to the state corresponding to `yes`; if the user chooses 'no', it transitions to the state corresponding to `no`.
/// The `sdzx` function here turns a regular enum type into a new, composable type.
/// For example, I can use `polystate.sdzx(Example).C(.yes_or_no, &.{ .a, .b })` to represent the state `(yes_or_no, a, b)`.
/// I usually write this type as `yes_or_no(a, b)`, which indicates that `yes_or_no` is a special state that requires two concrete state parameters.
/// Semantically, `yes_or_no(exit, a)` means: user confirmation is required before exiting. If the user chooses 'yes', it will transition to the `exit` state; if the user chooses 'no', it will transition to the `a` state.
/// Similarly, `yes_or_no(yes_or_no(exit, a), a)` means: user confirmation is required twice before exiting. The user must choose 'yes' both times to exit.
/// This is what composability means. Please make sure you understand this.
pub fn yes_or_noST(yes: polystate.sdzx(@This()), no: polystate.sdzx(@This())) type {
return yes_or_no_st(@This(), yes, no, GST);
}
};
pub const a_st = union(enum) {
AddOneThenToB: Example.Wit(Example.b),
/// This shows how to build and use a composite message in code.
/// For a composite message, it needs to be placed in a tuple. The first state is the function, and the rest are its state parameters.
/// Here, `.{ Example.yes_or_no, Example.exit, Example.a }` represents the state: `yes_or_no(exit, a)`.
Exit: Example.Wit(.{ Example.yes_or_no, Example.exit, Example.a }),
/// Similarly, `.{ Example.yes_or_no, .{Example.yes_or_no, Example.exit, Example.a}, Example.a }` can be used to represent the state: `yes_or_no(yes_or_no(exit, a), a)`.
...
};
pub const b_st = union(enum) {
...
};
/// Specific implementation of the `yes_or_no` state.
/// First, it's a function that takes four parameters: `FST`, `GST1`, `yes`, and `no`. Note that its implementation is independent of `Example` itself.
/// This is a generic implementation, independent of any specific state machine. You can use this code in any state machine.
/// I will explain this code again from four aspects: state, message, message handler function, and message generator function.
pub fn yes_or_no_st(
FST: type,
GST1: type,
yes: polystate.sdzx(FST),
no: polystate.sdzx(FST),
) type {
/// 1. State
/// Its specific state is: `polystate.sdzx(FST).C(FST.yes_or_no, &.{ yes, no })`.
/// It requires two parameters, `yes` and `no`, and also needs to ensure that `FST` definitely has a `yes_or_no` state.
return union(enum) {
/// 2. Message
/// There are three messages here. Special attention should be paid to `Retry`, which represents the semantic of re-entering due to an input error.
Yes: Wit(yes),
No: Wit(no),
/// Note the state being constructed here; it points to itself.
Retry: Wit(polystate.sdzx(FST).C(FST.yes_or_no, &.{ yes, no })),
fn Wit(val: polystate.sdzx(FST)) type {
return polystate.Witness(FST, GST1, null, val);
}
/// 3. Message Handler Function
pub fn handler(gst: *GST1) void {
switch (genMsg(gst)) {
.Yes => |wit| wit.handler(gst),
.No => |wit| wit.handler(gst),
.Retry => |wit| wit.handler(gst),
}
}
const stdIn = std.io.getStdIn().reader();
/// 4. Message Generator Function
/// Reads a string from `stdIn`. If the string is "y", it returns the message `.Yes`. If the string is "n", it returns the message `.No`.
/// In other cases, it returns `.Retry`.
fn genMsg(gst: *GST) @This() {
std.debug.print(
\Yes Or No:
\y={}, n={}
\
,
.{ yes, no },
);
const st = stdIn.readUntilDelimiter(&gst.buf, 'n') catch |err| {
std.debug.print("Input error: {any}, retryn", .{err});
return .Retry;
};
if (std.mem.eql(u8, st, "y")) {
return .Yes;
} else if (std.mem.eql(u8, st, "n")) {
return .No;
} else {
std.debug.print("Error input: {s}n", .{st});
return .Retry;
}
}
};
}
この例は、タイプ構成を介して構成可能な状態マシンを達成する方法を明確に示しています。
ATMを想像してください。私たちが中にいるとき checkPin 状態では、ユーザーは外部ソースからピンを入力する必要があります。ピンが正しい場合、それはaを送ります Successed メッセージと、次のように指定された状態への移行 success パラメーター。間違っている場合は、aを送信します Failed メッセージと、次のように指定された状態への移行 failed パラメーター。
一般的な要件は、ユーザーが最大3回ピンを入力しようとすることです。 3つの試行すべてが失敗した場合、カードは排出され、マシンは最初の画面に戻る必要があります。
ここに「最大3回」は、簡単に変更すべきではない非常に重要なセキュリティ要件です。
状態を作成することにより、この効果を自然に実装できます。私たちはデザインします checkPin 一般的な状態として、そして州の移行宣言では、このビジネスロジックを作成して正確に説明します checkPin。
pub fn checkPinST(success: polystate.sdzx(Atm), failed: polystate.sdzx(Atm)) type {
return union(enum) {
Successed: polystate.Witness(Atm, GST, null, success),
Failed: polystate.Witness(Atm, GST, null, failed),
...
...
}
}
pub const readyST = union(enum) {
/// By nesting the declaration of `checkPin` three times, we ensure that the PIN check happens at most three times. This precisely describes the behavior we need.
/// This demonstrates how to determine the program's overall behavior through compositional declarations.
InsertCard: Wit(.{ Atm.checkPin, Atm.session, .{ Atm.checkPin, Atm.session, .{ Atm.checkPin, Atm.session, Atm.ready } } }),
Exit: Wit(.{ Atm.are_you_sure, Atm.exit, Atm.ready }),
...
}
状態図を介してその全体的な論理を直接見ることができ、 polystate これらすべてを自動的に生成できます。
使った raylib 一般的な「選択」セマンティック:マウスを介したインタラクティブ選択を実装します。
選択の特定の動作は、3つの一般的な状態で構成されています(select、 inside、 hover)およびそれらに関連するメッセージ。
これらの状態とメッセージの実装:マウスを使用した要素を選択し、マウスがその上に浮かんだときの応答方法。
pub fn selectST(
FST: type,
GST: type,
enter_fn: ?fn (polystate.sdzx(FST), *GST) void,
back: polystate.sdzx(FST),
selected: polystate.sdzx(FST),
) type {
const cst = polystate.sdzx_to_cst(FST, selected);
const SDZX = polystate.sdzx(FST);
return union(enum) {
// zig fmt: off
ToBack : polystate.Witness(FST, GST, enter_fn, back),
ToInside: polystate.Witness(FST, GST, enter_fn, SDZX.C(FST.inside, &.{ back, selected })),
// zig fmt: on
...
};
}
pub fn insideST(
FST: type,
GST: type,
enter_fn: ?fn (polystate.sdzx(FST), *GST) void,
back: polystate.sdzx(FST),
selected: polystate.sdzx(FST),
) type {
const cst = polystate.sdzx_to_cst(FST, selected);
const SDZX = polystate.sdzx(FST);
return union(enum) {
// zig fmt: off
ToBack : polystate.Witness(FST, GST, enter_fn, back),
ToOutside : polystate.Witness(FST, GST, enter_fn, SDZX.C(FST.select, &.{ back, selected })),
ToHover : polystate.Witness(FST, GST, enter_fn, SDZX.C(FST.hover, &.{ back, selected })),
ToSelected: polystate.Witness(FST, GST, enter_fn, selected),
// zig fmt: on
...
};
}
pub fn hoverST(
FST: type,
GST: type,
enter_fn: ?fn (polystate.sdzx(FST), *GST) void,
back: polystate.sdzx(FST),
selected: polystate.sdzx(FST),
) type {
const cst = polystate.sdzx_to_cst(FST, selected);
const SDZX = polystate.sdzx(FST);
return union(enum) {
// zig fmt: off
ToBack : polystate.Witness(FST, GST, enter_fn, back),
ToOutside : polystate.Witness(FST, GST, enter_fn, SDZX.C(FST.select, &.{ back, selected })),
ToInside : polystate.Witness(FST, GST, enter_fn, SDZX.C(FST.inside, &.{ back, selected })),
ToSelected: polystate.Witness(FST, GST, enter_fn, selected),
// zig fmt: on
...
};
}
で ray-game プロジェクト、「選択」セマンティックは少なくとも8回再利用され、コードが大幅に減少し、正確性が向上しました。
このプロジェクトの興味深い例は、「2段階の選択」です。最初に建物を選択してから、グリッドの場所を選択して配置する必要があります。建物の選択は、場所の選択も制約します。 
このようなセマンティクスは、次のように簡潔に表現できます。
pub const placeST = union(enum) {
ToPlay: Wit(.{ Example.select, Example.play, .{ Example.select, Example.play, Example.place } }),
...
};
このコードは、極端な簡潔さで私たちの意図を説明しています。しかし、状態図を見ると、実際の状態遷移が非常に複雑であることがわかります。
単純な宣言を通じて、複雑な「選択」セマンティクスをネスト的に再利用しました。これは大きな勝利です!
これらすべての完全なコードはここにあります、約130行のコードで。
#SDZX1ポリステートポリステート構成可能な有限状態マシン
