异步状态机 旨在提供一种通过状态机构建应用程序结构的方法。其目标是识别每个功能中涉及的状态和副作用,并通过 DSL 以一致且可扩展的方式对它们进行建模。
let stateMachine = StateMachine(initial: .state1) {
When(state: .state1) {
Execute(output: .output1)
} transitions: {
On(event: .event1) { Transition(to: .state2) }
On(event: .event2) { Transition(to: .state3) }
On(event: .event3(value:)) { value in Transition(to: .state4(value)) }
}
When(state: .state2(value:)) { value in
Execute.noOutput
} transitions: { value in
…
}
}
let package = Package(
name: "Example",
dependencies: [
.package(url: "https://github.com/sideeffect-io/AsyncStateMachine.git", .upToNextMajor(from: "0.0.1"))
],
targets: [
.target(name: "Example", dependencies: ["AsyncStateMachine"])
]
)
AsyncSequence
Task
内部运行,该 Task
受益于协同取消一图胜千言,这是一个驱动电梯门打开的状态机示例
open
状态,里面有 0 个人open
时,在 personsHaveEntered
事件发生时,新状态为 open
,人数为 n + x
。open
时,在 closeButtonWasPressed
事件发生时,如果人数少于 10 人(电梯容量有限),则新状态为 closing
。closing
时,执行 close
动作(门可以以不同的速度关闭)。closing
时,在 doorHasLocked
事件发生时,新状态为 closed。open
、closing
和 closed
。 这是可能的状态的有限集合。电梯的初始状态是 open
,里面有 0 个人。personsHaveEntered
、closeButtonWasPressed
和 doorHasLocked
。 这是可能的事件的有限集合。closing
且人数少于 10 人时,close
门。门的速度由里面的人数决定。 这是可能的输出的有限集合。我们做出的假设是,几乎任何功能都可以用状态机来描述。为了尽可能简化,我们使用领域特定语言。
这是使用枚举和 异步状态机 DSL 对上述状态机的翻译
enum State: DSLCompatible {
case open(persons: Int)
case closing(persons: Int)
case closed
}
enum Event: DSLCompatible {
case personsHaveEntered(persons: Int)
case closeButtonWasPressed
case doorHasLocked
}
enum Output: DSLCompatible {
case close(speed: Int)
}
let stateMachine = StateMachine(initial: State.open(persons: 0)) {
When(state: State.open(persons:)) { _ in
Execute.noOutput
} transitions: { persons in
On(event: Event.personsHaveEntered(persons:)) { newPersons in
Transition(to: State.open(persons: persons + newPersons))
}
On(event: Event.closeButtonWasPressed) {
Guard(predicate: persons < 10)
} transition: {
Transition(to: State.closing(persons: persons))
}
}
When(state: State.closing(persons:)) { persons in
Execute(output: Output.close(speed: persons > 5 ? 1 : 2))
} transitions: { _ in
On(event: Event.doorHasLocked) {
Transition(to: State.closed)
}
}
}
能够将枚举与 DSL 一起使用的唯一要求是让它们符合 DSLCompatible(这允许以声明方式使用枚举,而无需模式匹配)。
DSL 旨在描述一个形式化的状态机:没有副作用,只有纯函数!
StateMachine
声明 output 值 来描述要执行的副作用的 意图,但这些副作用的 实现 在 Runtime
中声明,在 Runtime
中可以将输出映射到副作用函数。
(除此之外,这种解耦还允许更轻松地测试您的状态机,而无需依赖副作用的实现。)
func close(speed: Int) async -> Event {
try? await Task.sleep(nanoseconds: UInt64(1_000_000_000 / speed))
return .doorHasLocked
}
let runtime = Runtime<State, Event, Output>()
.map(output: Output.close(speed:), to: close(speed:))
副作用是 async
函数,返回单个 Event
或 AsyncSequence<Event>
。每次状态机产生预期的 output
时,都会执行相应的副作用。
此外,Runtime 可以注册在任何 state
或 event
上执行的 中间件 函数
let runtime = Runtime<State, Event, Output>()
.map(output: Output.close(speed:), to: close(speed:))
.register(middleware: { (state: State) in print("State: \(state)") })
.register(middleware: { (event: Event) in print("Event: \(event)") })
然后可以实例化 AsyncStateMachine
let sequence = AsyncStateMachine(
stateMachine: stateMachine,
runtime: runtime
)
for await state in sequence { … }
await sequence.send(Event.personsHaveEntered(persons: 3))
异步状态机 100% 基于 Swift 5.5 并发模型构建。
async
函数;它们将以非阻塞方式执行。send(_:)
将 await
。这可以防止并发转换同时发生(否则可能导致状态不一致)。Tasks
上下文中执行的 async
函数。.map(output: Output.close(speed:), to: close(speed:), priority: .high)
。AsyncStateMachine
被 deinit 时,所有挂起的副作用任务都将被标记为已取消。AsyncStateMachine
受益于与 AsyncSequence
关联的所有运算符(map
、filter
、…)。(另请参阅 swift async algorithms)AsyncStateMachine
符合并发模式下的多生产者/多消费者模式。虽然输出不共享(意味着每个消费者将接收状态的连续版本),但转换保证是并发安全的。大多数时候,副作用将需要依赖项来执行其职责。但是,异步状态机 期望副作用是一个最终接受参数(来自 Output
)并返回 Event
或 AsyncSequence<Event>
的函数。它们的签名中没有依赖项的位置。
有几种方法可以克服这个问题
class ElevatorUseCase {
let engine: Engine
init(engine: Engine) { self.engine = engine }
func close(speed: Int) async -> Event {
try? await Task.sleep(nanoseconds: UInt64(self.engine.delay / speed))
return .doorHasLocked
}
}
let useCase = ElevatorUseCase(engine: FastEngine())
let runtime = Runtime<State, Event, Output>()
.map(output: Output.close(speed:), to: useCase.close(speed:))
func makeClose(engine: Engine) -> (Int) async -> Event {
return { (speed: Int) in
try? await Task.sleep(nanoseconds: UInt64(engine.delay / speed))
return .doorHasLocked
}
}
let close = makeClose(engine: FastEngine())
let runtime = Runtime<State, Event, Output>()
.map(output: Output.close(speed:), to: close)
inject
函数(从冗余角度来看是首选方式)func close(speed: Int, engine: Engine) async -> Event {
try? await Task.sleep(nanoseconds: UInt64(engine.delay / speed))
return .doorHasLocked
}
let closeSideEffect = inject(dep: Engine(), in: close(speed:engine:))
let runtime = Runtime<State, Event, Output>()
.map(output: Output.close(speed:), to: closeSideEffect)
状态机定义不依赖于任何依赖项,因此可以在不使用模拟的情况下进行测试。异步状态机 提供了一个单元测试助手,使其更加容易
XCTStateMachine(stateMachine)
.assertNoOutput(when: .open(persons: 0))
.assert(
when: .open(persons: 0),
on: .personsHaveEntered(persons: 1),
transitionTo: .open(persons: 1)
)
.assert(
when: .open(persons: 5),
on: .closeButtonWasPressed,
transitionTo: .closing(persons: 5)
)
.assertNoTransition(when: .open(persons: 15), on: .closeButtonWasPressed)
.assert(when: .closing(persons: 1), execute: .close(speed: 2))
.assert(
when: .closing(persons: 1),
on: .doorHasLocked,
transitionTo: .closed
)
.assertNoOutput(when: .closed)
无论您使用哪种 UI 框架,渲染用户界面都是关于解释状态。您可以将 AsyncStateMachine
用作可靠的状态工厂。ViewStateMachine
是 AsyncStateMachine
周围的一个方便的包装器,可以简化从 UI 角度对状态的消费。
一个简单而朴素的 SwiftUI 用法可能是
struct ContentView: View {
@ObservedObject viewStateMachine: ViewStateMachine<State, Event, Output>
var body: some View {
VStack {
Text(self.viewStateMachine.state.description)
Button {
self.viewStateMachine.send(Event.personsHaveEntered(persons: 1))
} label: {
Text("New person")
}
Button {
self.viewStateMachine.send(Event.closeButtonWasPressed)
} label: {
Text("Close the door")
}
}.task {
await self.viewStateMachine.start()
}
}
}
…
let viewStateMachine = ViewStateMachine(asyncStateMachine: myAsyncStateMachine)
ContentView(viewStateMachine: viewStateMachine)
对于 UIKit,一个简单而朴素的方法是
let task: Task<Void, Never>!
let viewStateMachine: ViewStateMachine<State, Event, Output>!
let cancellable = AnyCancellable()
override func viewDidLoad() {
super.viewDidLoad()
self.task = Task { [weak self] in
await self?.viewStateMachine.start()
}
self.cancellable = self.viewStateMachine.$state.sink { [weak self] state in
self?.render(state: state)
}
}
func render(state: State) {
…
}
func deinit() {
self.task.cancel()
}
send()
函数await viewStateMachine.send(
.closeButtonWasPressed,
resumeWhen: .closed
)`
close(speed:)
副作用执行在状态机产生任何新状态时被取消。也可以在特定状态下取消。Runtime.map(
output: Output.close(speed:),
to: close(speed:),
strategy: .cancelWhenAnyState
)
When(states: OneOf {
State.closing(persons:),
State.closed
}) { _ in
Execute.noOutput
} transitions: {
On(event: Event.closeButtonWasPressed) { _ in
Transition(to: State.opening)
}
}`
self.viewStateMachine.binding(send: .closeButtonWasPressed)
允许在当前状态的属性上创建 SwiftUI 绑定,并在绑定更改时发送事件。
self.viewStateMachine.binding(
keypath: \.persons,
send: .closeButtonWasPressed
)
允许在当前状态的属性上创建 SwiftUI 绑定,并在绑定更改时发送事件,并使用指定的 dueTime 进行去抖动。
self.viewStateMachine
.binding(send: .closeButtonWasPressed)
.debounce(for: .seconds(1))
State.closed
时,这将在另一个状态机中发送事件 OtherEvent.refresh
。let channel = Channel<OtherEvent>()
let runtime = Runtime<State, Event, Output>()
...
.connectAsSender(to: channel, when: State.closed, send: OtherEvent.refresh)
let otherRuntime = Runtime<OtherState, OtherEvent, OtherOutput>()
...
.connectAsReceiver(to: channel)