Build and test Lint Codecov

异步状态机

异步状态机 旨在提供一种通过状态机构建应用程序结构的方法。其目标是识别每个功能中涉及的状态和副作用,并通过 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"])
  ]
)

关键点

一个简单的例子

一图胜千言,这是一个驱动电梯门打开的状态机示例


如何解读?

是什么定义了这个状态机?

我们做出的假设是,几乎任何功能都可以用状态机来描述。为了尽可能简化,我们使用领域特定语言。

状态机 DSL

这是使用枚举和 异步状态机 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 函数,返回单个 EventAsyncSequence<Event>。每次状态机产生预期的 output 时,都会执行相应的副作用。

此外,Runtime 可以注册在任何 stateevent 上执行的 中间件 函数

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))

Swift 并发是核心

异步状态机 100% 基于 Swift 5.5 并发模型构建。

转换

副作用

异步序列

如何注入依赖项?

大多数时候,副作用将需要依赖项来执行其职责。但是,异步状态机 期望副作用是一个最终接受参数(来自 Output)并返回 EventAsyncSequence<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)
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)

将异步状态机与 SwiftUI 和 UIKit 一起使用

无论您使用哪种 UI 框架,渲染用户界面都是关于解释状态。您可以将 AsyncStateMachine 用作可靠的状态工厂。ViewStateMachineAsyncStateMachine 周围的一个方便的包装器,可以简化从 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)
        }
}` 
SwiftUI 绑定允许在当前状态上创建 SwiftUI 绑定,并在绑定更改时发送事件。
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)