Swift FSM

codecov Testspace tests

适用于 macOS、iOS、tvOS 和 watchOS 的 Swift 友好的有限状态机语法

Swift FSM 的灵感来自 Uncle Bob 的 SMC 语法,它是一个用于指定和操作有限状态机 (FSM) 的 Swift DSL。

本指南假定您熟悉 FSM,特别是上面链接的 SMC 语法。 Swift FSM 大量使用 @resultBuilder 块、运算符重载callAsFunction()尾随闭包,所有这些都相互结合使用 - 熟悉这些概念很有帮助。

要求

Swift FSM 是一个适用于所有 Apple 平台的 Swift Package,可通过 Swift Package Manager 获得,并且需要 Swift 6 或更高版本。 它仅限于 macOS 15、iOS 18、tvOS 18 和 watchOS 11 或更高版本。

建议使用 Swift 6 语言模式 - 它应该可以与仍使用 Swift 5 语言模式的项目一起使用,但是可能会出现警告,并且某些环境中可能会出现编译错误(请参阅Swift 并发Swift 6 语言模式)。

它有一个依赖项 - Apple 的 Algorithms

基本语法

我们将使用地铁旋转栅门系统来镜像 SMC 的示例。 此旋转栅门具有两种状态:LockedUnlocked,以及两个事件:CoinPass

逻辑如下(来自 Uncle Bob,重点强调)

SMC

Initial: Locked
FSM: Turnstile
{
  Locked    {
    Coin    Unlocked    unlock
    Pass    Locked      alarm
  }
  Unlocked  {
    Coin    Unlocked    thankyou
    Pass    Locked      lock
  }
}

Swift FSM

let turnstile = FSM<State, Event>(initialState: .locked)
try turnstile.buildTable {
    define(.locked) {
        when(.coin) | then(.unlocked) | unlock
        when(.pass) | then(.locked)   | alarm
    }

    define(.unlocked) {
        when(.coin) | then(.unlocked) | thankyou
        when(.pass) | then(.locked)   | lock
    }
}

Swift FSM(带有其他上下文代码)

import SwiftFSM

class MyClass: SyntaxBuilder {
    enum State { case locked, unlocked }
    enum Event { case coin, pass }

    let turnstile = FSM<State, Event>(initialState: .locked)

    func myMethod() async throws {
        try turnstile.buildTable {
            define(.locked) {
                when(.coin) | then(.unlocked) | unlock
                when(.pass) | then(.locked)   | alarm
            }

            define(.unlocked) {
                when(.coin) | then(.unlocked) | thankyou
                when(.pass) | then(.locked)   | lock
            }
        }

        await turnstile.handleEvent(.coin)
    }
}
class MyClass: SyntaxBuilder {

SyntaxBuilder 协议提供指定转换表所需的 definewhenthen 方法。 它有两个关联类型 StateEvent,它们必须是 Hashable & Sendable

let turnstile = FSM<State, Event>(initialState: .locked)

FSM 是泛型类型,适用于 StateEvent。 在这里,我们使用 enum 将 FSM 的初始状态指定为 .locked

try turnstile.buildTable {

turnstile.buildTable 是一个抛出错误的函数 - 尽管类型系统会阻止各种不合逻辑的语句,但仍然存在一些语义问题只能在运行时检测到。

define(.locked) {

define 语句大致对应于 FSM 的自然语言描述中的“Given”关键字。 但是,预计每个状态只会编写一个 define

define 接受两个参数 - 一个 State 和一个 @resultBuilder 块。

when(.coin) | then(.unlocked) | unlock

|(管道)运算符将 whenthen 和 actions 绑定到离散的转换中。 它将左侧的输出馈送到右侧的输入,就像您在终端中预期的那样。

由于我们位于 define 块内,因此我们将 .locked 状态作为给定状态。 我们现在逐行列出我们的转换。 when 我们收到 .coin 事件时,我们将 then 转换到 .unlocked 状态并调用函数 unlock

由于 unlock 是对函数的引用,因此也可以声明如下

when(.coin) | then(.unlocked) | { unlock() //; otherFunction(); etc. }

两种类型的函数作为 Swift FSM 动作有效

@isolated(any) () async -> Void
@isolated(any) (Event) async -> Void

如果希望将关联值与事件 enum 一起传递到回调函数,则接受 Event 的 Actions 很有用(有关如何实现此操作的更多详细信息,请参阅使用事件传递值,有关组合不同类型操作列表的方法,请参阅操作数组)。

await turnstile.handleEvent(.coin)

由于 handleEvent 可能会调用 async 动作,因此 handleEvent 本身也必须是 async

FSM 将为其当前状态找到适当的转换,调用关联的函数,并转换到关联的下一个状态。 在这种情况下,我们调用 unlock 函数并转换到 unlocked 状态。 如果未找到任何转换,则不会发生任何事情,并且如果为调试编译,则会在控制台中打印警告消息。

操作数组

如果传递一个操作数组,您可能希望使用 Swift FSM 提供的便利的 & 运算符重载,以便能够混合和匹配不同的操作签名

when(.coin) | then(.unlocked) | first & secondAsync & thirdWithEvent ...

这等效于(尽管在技术上并不完全相同)更详细但同样有效的

when(.coin) | then(.unlocked) | { event in await first(); await secondAsync(); thirdWithEvent(event) ... }

可选参数

现在让我们添加一个报警状态,必须由维修人员重置

SMC

Initial: Locked
FSM: Turnstile
{
  Locked    {
    Coin    Unlocked    unlock
    Pass    Alarming    alarmOn
    Reset   -           {alarmOff lock}
  }
  Unlocked  {
    Reset   Locked      {alarmOff lock}
    Coin    Unlocked    thankyou
    Pass    Locked      lock
  }
  Alarming {
    Coin    -          -
    Pass    -          -  
    Reset   Locked     {alarmOff lock}
  }
}

Swift FSM

try turnstile.buildTable {
    define(.locked) {
        when(.coin)  | then(.unlocked) | unlock
        when(.pass)  | then(.alarming) | alarmOn
        when(.reset) | then()          | alarmOff & lock
    }

    define(.unlocked) {
        when(.reset) | then(.locked)   | alarmOff & lock
        when(.coin)  | then(.unlocked) | thankyou
        when(.pass)  | then(.locked)   | lock
    }

    define(.alarming) {
        when(.coin)  | then()
        when(.pass)  | then()
        when(.reset) | then(.locked)   | alarmOff & lock
    }
}

没有参数的 then() 表示“没有状态更改” - FSM 保持在其当前状态。 操作管道也是可选的 - 如果转换不执行任何操作,则可以省略它。

超级状态

请注意 Reset 转换的重复。 在所有三种状态下,Reset 事件都执行相同的操作。 它转换到 Locked 状态,并调用 lock 和 alarmOff 操作。 可以通过使用超级状态来消除这种重复,如下所示

SMC

Initial: Locked
FSM: Turnstile
{
  // This is an abstract super state.
  (Resetable)  {
    Reset       Locked       {alarmOff lock}
  }
  Locked : Resetable    { 
    Coin    Unlocked    unlock
    Pass    Alarming    alarmOn
  }
  Unlocked : Resetable {
    Coin    Unlocked    thankyou
    Pass    Locked      lock
  }
  Alarming : Resetable { // inherits all it's transitions from Resetable.
  }
}

Swift FSM

try turnstile.buildTable {
    let resetable = SuperState {
        when(.reset) | then(.locked)  | alarmOff & lock
    }

    define(.locked, adopts: resetable) {
        when(.coin) | then(.unlocked) | unlock
        when(.pass) | then(.alarming) | alarmOn
    }

    define(.unlocked, adopts: resetable) {
        when(.coin) | then(.unlocked) | thankyou
        when(.pass) | then(.locked)   | lock
    }

    define(.alarming, adopts: resetable)
}

SuperState 采用与 define 相同的 @resultBuilder,但没有起始状态。 起始状态取自传递给它的 define 语句。 然后 define 将添加在每个 SuperState 实例中声明的转换,然后再添加在 define 中声明的其他转换。

如果将 SuperState 实例传递给 define,则 @resultBuilder 参数是可选的。

SuperState 实例可以采用其他 SuperState 实例,并将它们与 define 组合在一起

let s1 = SuperState { when(.coin) | then(.unlocked) | unlock  }
let s2 = SuperState { when(.pass) | then(.alarming) | alarmOn }

let s3 = SuperState(adopts: s1, s2)

// s3 is equivalent to:

let s4 = SuperState {
    when(.coin) | then(.unlocked) | unlock
    when(.pass) | then(.alarming) | alarmOn
}

覆盖超级状态

SuperState 中声明的转换不能被其采用者覆盖。 因此,假定以下代码存在错误并抛出

let s1 = SuperState { when(.coin) | then(.unlocked) | unlock  }

let s2 = SuperState(adopts: s1) { 
    when(.coin) | then(.locked) | beGrumpy // 💥 error: clashing transitions
}

define(.locked, adopts: s1) {
    when(.coin) | then(.locked) | beGrumpy // 💥 error: clashing transitions
}

要覆盖 SuperState 转换,您必须使用 overriding { }

let s1 = SuperState { when(.coin) | then(.unlocked) | unlock  }

let s2 = SuperState(adopts: s1) {
    overriding { 
        when(.coin) | then(.locked) | beGrumpy // ✅ overrides inherited transition
    }
}

define(.locked, adopts: s1) {
    overriding { 
        when(.coin) | then(.locked) | beGrumpy // ✅ overrides inherited transition
    }
}

由于允许多重继承,因此覆盖会替换所有匹配的转换

let s1 = SuperState { when(.coin) | then(.unlocked) | doSomething     }
let s2 = SuperState { when(.coin) | then(.unlocked) | doSomethingElse }

define(.locked, adopts: s1, s2) {
    overriding { 
        when(.coin) | then(.locked) | doYetAnotherThing // ✅ overrides both inherited transitions
    }
}

如果在没有要覆盖的内容的情况下使用 overriding,则 FSM 将抛出错误

define(.locked) {
    overriding { 
        when(.coin) | then(.locked) | beGrumpy // 💥 error: nothing to override
    }
}

在父级而不是子级中编写 overriding 将抛出错误

let s1 = SuperState {
    overriding { 
        when(.coin) | then(.locked) | beGrumpy
    }
}

let s2 = SuperState(adopts: s1) { when(.coin) | then(.unlocked) | unlock }

// 💥 error: overrides are out of order

尝试在同一个 SuperState { }define { } 中覆盖会抛出错误

define(.locked) {
    when(.coin) | then(.locked) | doSomething
    overriding { 
        when(.coin) | then(.locked) | doSomethingElse
    }
}

// 💥 error: duplicate transitions

在此作用域中,单词 override 没有意义,因此会被错误处理程序忽略。 剩下的就是重复的转换,从而导致错误。

覆盖链

覆盖遵循通常的继承规则。 在覆盖链中,最后一个转换优先

let s1 = SuperState { when(.coin) | then(.unlocked) | a1  }
let s2 = SuperState(adopts: s1) { overriding { when(.coin) | then(.unlocked) | a2 } }
let s3 = SuperState(adopts: s2) { overriding { when(.coin) | then(.unlocked) | a3 } }
let s4 = SuperState(adopts: s3) { overriding { when(.coin) | then(.unlocked) | a4 } }

define(.locked, adopts: s4) {
    overriding { when(.coin) | then(.unlocked) | a5 } // ✅ overrides all others
}

turnstile.handleEvent(.coin) // 'a5' is called

进入和退出动作

在前面的示例中,每次进入 Alarming 状态时都打开警报,并在每次退出 Alarming 状态时关闭警报的事实都隐藏在几个不同转换的逻辑中。 我们可以使用进入动作和退出动作来明确这一点。

SMC

Initial: Locked
FSM: Turnstile
{
  (Resetable) {
    Reset       Locked       -
  }
  Locked : Resetable <lock     {
    Coin    Unlocked    -
    Pass    Alarming    -
  }
  Unlocked : Resetable <unlock  {
    Coin    Unlocked    thankyou
    Pass    Locked      -
  }
  Alarming : Resetable <alarmOn >alarmOff   -    -    -
}

Swift FSM

try turnstile.buildTable {
    let resetable = SuperState {
        when(.reset) | then(.locked)
    }

    define(.locked, adopts: resetable, onEntry: lock*) {
        when(.coin) | then(.unlocked)
        when(.pass) | then(.alarming)
    }

    define(.unlocked, adopts: resetable, onEntry: unlock*) {
        when(.coin) | then(.unlocked) | thankyou
        when(.pass) | then(.locked)
    }

    define(.alarming, adopts: resetable, onEntry: alarmOn*, onExit: alarmOff*)
}

onEntryonExit 指定在进入或离开定义的状态时要执行的操作数组。 由于 Swift 的函数匹配算法对采用多个闭包参数的函数存在限制,因此这些需要数组语法,而不是更方便的 varargs。

由于该数组是异构的(它可以包括两种操作类型中的任何一种),因此提供了一个特殊的后缀运算符 *,用于将其中一个转换为 AnyAction 数组。

_ = unlock* // preferred syntax, same as...
_ = Array(unlock) // same as...
_ = [AnyAction(unlock)]

_ = unlock & thankyou // preferred syntax, same as...
_ = AnyAction(unlock) & thankyou // same as...
_ = AnyAction(unlock) & AnyAction(thankyou) // same as...
_ = [AnyAction(unlock), AnyAction(thankyou)]

SuperState 实例也接受进入和退出动作

let resetable = SuperState(onEntry: lock*) {
    when(.reset) | then(.locked)
}

define(.locked, adopts: resetable) {
    when(.coin) | then(.unlocked)
    when(.pass) | then(.alarming)
}

// equivalent to:

define(.locked, onEntry: lock*) {
    when(.reset) | then(.locked)
    when(.coin)  | then(.unlocked)
    when(.pass)  | then(.alarming)
}

SuperState 实例也会从其超状态继承进入和退出动作

let s1 = SuperState(onEntry: unlock*)  { when(.coin) | then(.unlocked) }
let s2 = SuperState(onEntry: alarmOn*) { when(.pass) | then(.alarming) }

let s3 = SuperState(adopts: s1, s2)

// s3 is equivalent to:

let s4 = SuperState(onEntry: [unlock, alarmOn]) { 
    when(.coin) | then(.unlocked)
    when(.pass) | then(.alarming)
}

配置进入和退出动作行为

在 SMC 中,即使状态没有更改,也会始终调用进入和退出动作。 因此,在所有进入 Unlocked 状态的转换中,始终会调用 unlock 进入动作。

Swift FSM 的默认行为是 仅在存在状态更改时 调用进入和退出动作。 在上面的示例中,这意味着在 .unlocked 状态下,在 .coin 事件之后, 调用 unlock

如果将 .executeAlways 传递给 FSM.init,Swift FSM 将与 SMC 匹配。 默认值为 .executeOnChangeOnly,并且不是必需的。

FSM<State, Event>(initialState: .locked, actionsPolicy: .executeAlways)

语法顺序

所有语句必须以 define { when | then | actions } 的形式进行。 有关此规则的例外情况,请参阅扩展语法

语法糖

为了方便起见,when 语句接受 vararg Event 实例。

define(.locked) {
    when(.coin, or: .pass, ...) | then(.unlocked) | unlock
}

// equivalent to:

define(.locked) {
    when(.coin) | then(.unlocked) | unlock
    when(.pass) | then(.unlocked) | unlock
    ...
}

在事件中传递值

Actions 可以接收导致调用它们的事件。 SwiftFSM 需要一个特殊的结构 FSMValue<T> 和协议 EventWithValues,它们一起工作以使您能够执行此操作。

enum Event: EventWithValues {
    case .coin(FSMValue<Int>), ...

    var coinValue: Int? {
        guard case .coin(let amount) = event else { return nil }
        return amount.wrappedValue
    }
}

func main() throws {
    try turnstile.buildTable(initialState: .locked) {
        define(.locked) {
            when(.coin(.any)) | then(.verifyingPayment) | verifyPayment
            // here we use .any to match any value
        }
    }

    try turnstile.handleEvent(.coin(50))
    // here we pass a specific value that will be matched by .any
}

func verifyPayment(_ event: Event) {
    // here we receive the actual value passed to handleEvent: .coin(50)
    if let amount = event.coinValue {
        if amount >= requiredAmount {
            letThemThrough()
        } else {
            insufficientPayment(shortfall: requiredAmount - amount)
        }
    }
}

when(.coin(.any)) 以多态方式工作,匹配 .coin(someValue) 内的任何值,并将 someValue 传递给 verifyPayment 函数。

如果没有 EventWithValuesFSMValue<T> 的组合,则必须按如下方式编写表

try turnstile.buildTable(initialState: .locked) {
    define(.locked) {
        when(.coin(1)) | then(.verifyingPayment) | verifyPayment
        when(.coin(2)) | then(.verifyingPayment) | verifyPayment
        when(.coin(3)) | then(.verifyingPayment) | verifyPayment
        when(.coin(4)) | then(.verifyingPayment) | verifyPayment
        ... // and so on for all relevant values
    }
}

通过使用 EventWithValues.any,当收到 .coin 事件时,无论包装的值如何,都会激活到 .verifyingPayment 的转换。 然后,该包装的值将传递到可以检查它的 verifyPayment 函数。 FSMValue 提供了一个方便的 var wrappedValue: T?,它返回一个可选值(如果在 .any 实例上调用它或如果 T 是可选的并且为 nil,则可能为 nil)。

文字表达式实现

FSMValue 遵循 ExpressibleByIntegerLiteralExpressibleByFloatLiteralExpressibleByArrayLiteralExpressibleByDictionaryLiteralExpressionByNilLiteralExpressionByStringLiteral 协议,并在相关情况下转发到包装的类型。 它还转发对 EquatableComparableAdditiveArithmetic 的一致性(如果适用),以及 RandomAccessCollection 及其父协议(对于数组)和字典的下标访问。 它转发 CustomStringConvertible,这也涵盖了 ExpressibleByStringInterpolation 的大多数用法。

一些例子

let s: FSMValue<String> = "1" // equivalent to .some("1")
let i: FSMValue<Int> = 1 // equivalent to .some(1)
let ai: FSMValue<[Int]> = [1] // equivalent to .some([1])

_ = s + "1" // "11"
_ = i + 1 // 2
_ = ai[0] // 1
_ = ai[0] == i // true
_ = ai[0] > i // false
_ = "\(i)\(s)" // "11"

警告: 如果包装类型上可以使用转发操作,请注意,如果您尝试访问 .any 实例上的值,这将导致崩溃(很像强制解包一个 nil 可选值 - 在这种意义上,.any 是一个空值)。 因此,.any 应该只出现在 define 语句中 - 在任何情况下,将带有 FSMValue.any 的事件传递给 handleEvent 都是没有用处或意义的。

在继续之前,您应该始终解包 FSMValue<T> 实例 - 事实上,所有返回值的便利方法都返回 T 的实例,而不是 FSMValue<T> 的实例。

@resultBuilder 实现的限制

SwiftFSM 中的 @resultBuilder 代码块不支持控制流逻辑。 尽管可以启用此类逻辑,但会产生误导。

define(.locked) {
    if something { // ⛔️ does not compile
        when(.pass) | then(.unlocked) | unlock
    } else {
        when(.pass) | then(.alarming) | alarmOn
    }
    ...
}

如果 if/else 代码块在转换时由 FSM 评估,这将是有用的。 然而,我们正在做的是编译我们的状态转换表(SMC 代表状态机编译器)。 以这种方式使用 ifelse 类似于使用 #if#else - 只会编译一个转换。

有关在运行时而不是编译时评估条件语句的替代系统,请参阅 扩展语法

Swift 并发

Swift FSM 不对其客户端的并发处理提出要求。 FSM 类上的公共方法以多态方式隔离到调用者的 Actor(如果存在),或者根本没有 Actor。 这是通过在所有公共方法签名中包含参数 isolation: isolated (any Actor)? = #isolation 来实现的。

Swift FSM 在任何并发或非并发环境中透明地工作。 然而,从技术上来说,可以(尽管不切实际)从不同的 actor 调用 FSM 类的每个公共方法,因为 actor 多态目前在单个函数级别而不是在类级别工作。

FSM 有一个可选的运行时并发检查器,如果您尝试从冲突的并发环境中调用其方法,则该检查器会使 precondition 检查失败。 可以通过将 enforceConcurrency: true 传递给 FSM.init 来启用此检查。 此检查仅在为调试构建时运行。

class MyClass {
    let fsm: FSM<Int, Int>
    init(fsm: FSM<Int, Int>) {
        self.fsm = fsm
    }
    
    func one() async { await fsm.handleEvent(1) }
    
    @MainActor
    func two() async { await fsm.handleEvent(1) }
}

let fsm = FSM<Int, Int>(initialState: 1, enforceConcurrency: true)
let c = MyClass(fsm: fsm)

try fsm.buildTable {
    define(1) { when(2) | then() }
}             
// ✅ First call sets the actor for future calls
await c.one() 
// ✅ Same 'NonIsolated' as first call
await c.two() 
// 💥 Concurrency violation: handleEvent called by MainActor (expected NonIsolated)

在 Main Actor 上工作

虽然 FSM 如果从 main actor 调用其方法,则会在 main actor 上运行,但在 Swift 提供一种跨整个类统一多态 actor 行为的方法之前,Swift FSM 还提供了一个方便的包装器 FSM<State, Event>.OnMainActor,并用 @MainActor 注释,允许编译器强制隔离,而无需使用可选的运行时检查器。

但在大多数情况下,在 main actor 上下文中,FSMFSM.OnMainActor 的行为不会有任何区别 - OnMainActor 只是防止编译时出现不太可能发生的边缘情况。

系统的一些细微之处(在 Swift 6 语言模式下编译时)

@MainActor
class MyMainActorClass {
    func myMethod() {
        // ✅ Called with Main Actor isolation
        let fsm = FSM<Int, Int>(initialState: 1)
        
        // ✅ Called with Main Actor isolation
        let mainActorFSM = FSM<Int, Int>.OnMainActor(initialState: 1)
    }
    
    func myAsyncMethod() async {
        // ✅ Called with Main Actor isolation
        let fsm = FSM<Int, Int>(initialState: 1)
        
        // ✅ Called with Main Actor isolation
        let mainActorFSM = FSM<Int, Int>.OnMainActor(initialState: 1)
    }
}

class MyNonIsolatedClass {
    func myMethod() {
        // ✅ Called without isolation
        let fsm = FSM<Int, Int>(initialState: 1)
        
        // ⛔️ Call to main actor-isolated initializer 'init(type:initialState:actionsPolicy:)' in a synchronous nonisolated context
        let mainActorFSM = FSM<Int, Int>.OnMainActor(initialState: 1)
    }
    
    func myAsyncMethod() async {
        // ✅ Called without isolation
        let fsm = FSM<Int, Int>(initialState: 1)
        
        // ✅ Called with Main Actor isolation
        let mainActorFSM = await FSM<Int, Int>.OnMainActor(initialState: 1)
    }
    
    @MainActor
    func myMainActorMethod() {
        // ✅ Called with Main Actor isolation
        let fsm = FSM<Int, Int>(initialState: 1)
        
        // ✅ Called with Main Actor isolation
        let mainActorFSM = FSM<Int, Int>.OnMainActor(initialState: 1)
    }
}

actor MyCustomActor {
    func myMethod() {
        // ✅ Called with MyCustomActor isolation
        let fsm = FSM<Int, Int>(initialState: 1)
        
        // ⛔️ Call to main actor-isolated initializer 'init(type:initialState:actionsPolicy:)' in a synchronous nonisolated context
        let mainActorFSM = FSM<Int, Int>.OnMainActor(initialState: 1)
    }
    
    func myAsyncMethod() async {
        // ✅ Called with MyCustomActor isolation
        let fsm = FSM<Int, Int>(initialState: 1)
        
        // ✅ Called with Main Actor isolation
        let mainActorFSM = await FSM.OnMainActor<Int, Int>(initialState: 1)
    }
    
    @MainActor
    func myMainActorMethod() {
        // ✅ Called with Main Actor isolation
        let fsm = FSM<Int, Int>(initialState: 1)
        
        // ✅ Called with Main Actor isolation
        let mainActorFSM = FSM<Int, Int>.OnMainActor(initialState: 1)
    }
}

运行时错误

大多数 Swift FSM 函数调用和初始化器采用额外的“magic”参数 file: String = #fileline: Int = #line。 一些还采用 isolation: isolated (any Actor)? = #isolation

由于这些无法隐藏,请注意,不太可能有任何理由使用替代值覆盖这些默认参数。

空代码块

所有代码块必须至少包含一个语句

try turnstile.buildTable { } //💥 error: empty table
try turnstile.buildTable {
    define(.locked) { } // 💥 error: empty block
}

重复的转换

如果转换共享相同的起始状态、事件和下一个状态,则它们是重复的。

try turnstile.buildTable {
    define(.locked) {
        when(.coin) | then(.unlocked) | unlock
        when(.coin) | then(.unlocked) | lock
    }
}

// 💥 error: duplicate transitions

逻辑冲突

当转换共享相同的起始状态和事件,但它们的下一个状态不同时,它们会发生冲突。

try turnstile.buildTable {
    define(.locked) {
        when(.coin) | then(.unlocked) | unlock
        when(.coin) | then(.locked)   | lock
    }
}

// 💥 error: logical clash

虽然这两个转换是不同的,但它们不能共存 - .coin 事件必须导致 .unlocked 状态或 .locked 状态。 它不能同时导致两者。

FSMValue - .any 的不正确使用

因为 .any 匹配所有情况,所以以下代码会抛出异常

try turnstile.buildTable(initialState: .locked) {
    define(.locked) {
        when(.coin(.any)) | then(.verifyingPayment) | verifyPayment
        when(.coin(50)    | then(.unlocked)         | pass
    }
} 

//💥 error: logical clash

.any 情况已经包括所有情况,从而产生歧义。 可以编写以下代码

try turnstile.buildTable(initialState: .locked) {
    define(.locked) {
        when(.coin(20) | then(.verifyingPayment) | verifyPayment
        when(.coin(50) | then(.unlocked)         | pass
    }
} 

// ✅ transitions are logically distinct

重复的 buildTable 调用

turnstile.buildTable { } 的其他调用将抛出 TableAlreadyBuiltError

性能

每次调用 handleEvent() 的性能为 O(1)。 然而,它仍然具有相当于嵌套 switch case 语句的 2-3 倍的运行开销。 Swift FSM 以性能换取便利性,不适合资源受限的环境。

扩展语法

虽然 Swift FSM 匹配了 SMC 的大多数语法,但它也引入了一些自己的新可能性。

例子

让我们想象一下对我们的旋转栅门规则的扩展:在某些时候,我们想要通过在仍然 .locked 的情况下检测到 .pass 时进入警报状态来强制执行“每个人都付费”规则。 在其他时候,也许在高峰时段,我们希望更加宽松。

我们可以在系统的其他地方实现一天中的时间检查,可能像这样

try turnstile.buildTable {
    ...
    define(.locked) {
        when(.pass) | then(.alarming) | handleAlarm
    }
    ...
}

// elsewhere in the system...

enum Enforcement: Predicate { case weak, strong }

let enforcement = Enforcement.weak

func handleAlarm() {
    switch enforcement {
    case .weak: smile()
    case .strong: defconOne()
    }
}

但我们现在在转换表中声明了我们状态转换逻辑的一些方面,而在其他地方声明了其他方面。 并且我们仍然转换到 .alarming 状态,而不管 Enforcement 策略如何。 如果不同的策略需要完全不同的转换怎么办?

我们可以引入额外的事件来区分新策略

try turnstile.buildTable {
    ...
    define(.locked) {
        when(.passWithEnforcement)    | then(.alarming) | defconOne
        when(.passWithoutEnforcement) | then(.locked)   | smile
    }
    ...
}

现在我们可以调用不同的函数并转换到不同的状态,具体取决于强制执行策略,同时将我们的逻辑保留在转换表中。

最初响应 .pass 事件的每个转换现在都需要编写两次,一次用于此事件的两个新版本中的每一个,即使它们都是相同的。 状态转换表将变得难以管理,并且充满重复。

Swift FSM 解决方案

import SwiftFSM

class MyClass: ExpandedSyntaxBuilder {
    enum State { case locked, unlocked }
    enum Event { case coin, pass }
    enum Enforcement: Predicate { case weak, strong }

    let fsm = FSM<State, Event>(initialState: .locked)

    func myMethod() throws {
        try turnstile.buildTable {
            ...
            define(.locked) {
                matching(Enforcement.weak)   | when(.pass) | then(.locked)   | smile
                matching(Enforcement.strong) | when(.pass) | then(.alarming) | defconOne
                
                when(.coin) | then(.unlocked)
            }
            ...
       }
        
       turnstile.handleEvent(.pass, predicates: Enforcement.weak)
    }
}

我们引入了函数 matching 和两个协议 ExpandedSyntaxBuilderPredicate

define(.locked) {
    matching(Enforcement.weak)   | when(.pass) | then(.locked)   | smile
    matching(Enforcement.strong) | when(.pass) | then(.alarming) | defconOne
                
    when(.coin) | then(.unlocked) | unlock
}

假设我们处于 .locked 状态

只有那些依赖于 Enforcement 策略的语句才知道已添加它 - 预先存在的语句继续按原样工作。

ExpandedSyntaxBuilder 和 Predicate

ExpandedSyntaxBuilder 使用相同的要求实现 SyntaxBuilderPredicate 要求一致性类型为 Hashable, SendableCaseIterable。 可以使用任何类型,但在实践中,CaseIterable 要求可能会将 Predicate 限制为没有关联类型的 Enums

隐式匹配语句

when(.coin) | then(.unlocked)

指定 Predicate 时,它是从转换的上下文中推断出来的。 推断的范围在 turnstile.buildTable { } 的大括号之间。 这就是为什么此函数只能调用一次的原因之一。

在我们的示例中,类型 Enforcement 出现在表中的其他位置的 matching 语句中,Swift FSM 将推断出缺少的 matching 语句

when(.coin) | then(.unlocked)

// is inferred to mean:

matching(Enforcement.weak)   | when(.coin) | then(.unlocked)
matching(Enforcement.strong) | when(.coin) | then(.unlocked)

因此,默认情况下,转换与 Predicate 无关,除非另有说明,否则匹配任何 Predicatematching 是一个可选修饰符,它约束转换到一个或多个特定的 Predicate 大小写。

多个 Predicate

可以使用的 Predicate 类型数量没有限制(有关实际限制,请参阅 Predicate 性能)。

enum Enforcement: Predicate { case weak, strong }
enum Reward: Predicate { case positive, negative }

try turnstile.buildTable {
    ...
    define(.locked) {
        matching(Enforcement.weak)   | when(.pass) | then(.locked)   | lock
        matching(Enforcement.strong) | when(.pass) | then(.alarming) | alarmOn
                
        when(.coin) | then(.unlocked) | unlock
    }

    define(.unlocked) {
        matching(Reward.positive) | when(.coin) | then(.unlocked) | thankyou
        matching(Reward.negative) | when(.coin) | then(.unlocked) | idiot

        when(.pass) | then(.locked) | lock
    }
    ...
}

await turnstile.handleEvent(.pass, predicates: Enforcement.weak, Reward.positive)

相同的推理规则仍然适用

when(.coin) | then(.unlocked) | unlock

// types Enforcement and Reward appear elsewhere in context
// when(.coin) | then(.unlocked) is now equivalent to:

matching(Enforcement.weak,   and: Reward.positive) | when(.coin) | then(.unlocked) | unlock
matching(Enforcement.strong, and: Reward.positive) | when(.coin) | then(.unlocked) | unlock
matching(Enforcement.weak,   and: Reward.negative) | when(.coin) | then(.unlocked) | unlock
matching(Enforcement.strong, and: Reward.negative) | when(.coin) | then(.unlocked) | unlock

假设当前状态为 .locked,则调用 handleEvent 的结果将是保持在 .locked 状态并调用 lock 函数。

复合匹配语句

可以通过填充 and: Predicate...or: Predicate... 参数,在单个 matching 语句中组合多个谓词。

enum A: Predicate { case x, y, z }
enum B: Predicate { case x, y, z }
enum C: Predicate { case x, y, z }

matching(A.x, or: A.y)... // if A.x OR A.y
matching(A.x, or: A.y, A.z)... // if A.x OR A.y OR A.z

matching(A.x, and: B.x)... // if A.x AND B.x
matching(A.x, and: B.x, C.x)... // if A.x AND B.x AND C.x

matching(A.x, or: A.y, A.z, and: B.x, C.x)... // if (A.x OR A.y OR A.z) AND B.x AND C.x

matching(A.x, or: B.x)...  // ⛔️ does not compile: OR types must be the same
matching(A.x, and: A.y)... // 💥 error: cannot match A.x AND A.y simultaneously

turnstile.handleEvent(.coin, predicates: A.x, B.x, C.x)

matching(and:) 意味着我们期望两个谓词同时存在,而 mathing(or:) 意味着我们期望存在任何一个且仅一个。

Swift FSM 期望传递给 handleEvent 的表中存在的每种 Predicate 类型只有一个实例,如示例中所示,其中 turnstile.handleEvent(.coin, predicates: A.x, B.x, C.x) 包含类型 ABC 的单个实例。 因此,永远不应发生 A.x AND A.y - 只能存在一个。 因此,传递给 matching(and:) 的谓词必须都属于不同的类型。

matching(or:) 为单个 Predicate 类型指定了多个可能性。 因此,由 or 连接的谓词必须都属于同一类型,并且尝试将不同的 Predicate 类型传递给 matching(or:) 将无法编译(有关此限制的更多信息,请参阅 隐式冲突)。

隐式冲突

Between-Predicates 冲突

define(.locked) {
    matching(Enforcement.weak) | when(.coin) | then(.unlocked)
    matching(Reward.negative)  | when(.coin) | then(.locked)
}

// 💥 error: implicit clash

这两个转换似乎不同,但是

define(.locked) {
    matching(Enforcement.weak) | when(.coin) ...
// inferred as:
    matching(Enforcement.weak, and: Reward.positive) | when(.coin) ...
    matching(Enforcement.weak, and: Reward.negative) | when(.coin) ... // 💥 clash 

    matching(Reward.negative) | when(.coin) ...
// inferred as:
    matching(Enforcement.weak,   and: Reward.negative) | when(.coin) ... // 💥 clash
    matching(Enforcement.strong, and: Reward.negative) | when(.coin) ...

我们可以通过消除至少一个语句的歧义来打破僵局

define(.locked) {
    matching(Enforcement.weak, and: Reward.positive) | when(.coin) | then(.unlocked)
    matching(Reward.negative)                        | when(.coin) | then(.locked)
}

// ✅ inferred as:

define(.locked) {
    matching(Enforcement.weak,   and: Reward.positive) | when(.coin) | then(.unlocked)
//  matching(Enforcement.weak,   and: Reward.negative) ... removed by disambiguation

    matching(Enforcement.weak,   and: Reward.negative) | when(.coin) | then(.locked)
    matching(Enforcement.strong, and: Reward.negative) | when(.coin) | then(.locked)
}

在某些情况下,Swift FSM 可以消除歧义而无需打破僵局

define(.locked) {
    matching(Enforcement.weak, and: Reward.positive) | when(.coin) | then(.unlocked)
    matching(Enforcement.weak)                       | when(.coin) | then(.locked)
}

// ✅ inferred as:

define(.locked) {
    matching(Enforcement.weak, and: Reward.positive) | when(.coin) | then(.unlocked)
    matching(Enforcement.weak, and: Reward.negative) | when(.coin) | then(.locked)
}

Swift FSM 优先处理指定最大数量谓词的语句 - 在这种情况下,第一个语句 matching(Enforcement.weak, and: Reward.positive) 指定了两个谓词,胜过第二个语句的单个谓词 matching(Enforcement.weak)

从本质上讲,Reward.positive 已经被更明确的转换“声明”,只留下剩余的 Reward.negative 用于不太明确的转换。

Within-Predicates 冲突

不允许通过“OR”连接不同的类型

define(.locked) {
    matching(Enforcement.weak, or: Reward.negative) | when(.coin) | then(.unlocked)
}

// ⛔️ does not compile, because it implies:

define(.locked) {
    matching(Enforcement.weak) | when(.coin) | then(.unlocked)
    matching(Reward.negative)  | when(.coin) | then(.unlocked)
}

// 💥 error: implicit clash

使用上下文代码块进行重复数据删除

matching(Enforcement.weak)   | when(.pass) /* duplication */ | then(.locked)
matching(Enforcement.strong) | when(.pass) /* duplication */ | then(.alarming)

when(.pass) 是重复的。 我们可以使用上下文块来分解它

when(.pass) {
    matching(Enforcement.weak)   | then(.locked)
    matching(Enforcement.strong) | then(.alarming)
}

完整的例子现在是

try turnstile.buildTable {
    define(.locked) {
        when(.pass) {
            matching(Enforcement.weak)   | then(.locked)
            matching(Enforcement.strong) | then(.alarming)
        }
                
        when(.coin) | then(.unlocked)
    }
}

thenmatching 也支持上下文代码块

try turnstile.buildTable {
    define(.locked) {
        then(.unlocked) {
            when(.pass) {
                matching(Enforcement.weak)   | doSomething
                matching(Enforcement.strong) | doSomethingElse
            }
        }
    }

// or identically:

    define(.locked) {
        when(.pass) {
            then(.unlocked) {
                matching(Enforcement.weak)   | doSomething
                matching(Enforcement.strong) | doSomethingElse
            }
        }
    }
}
try turnstile.buildTable {
    define(.locked) {
        matching(Enforcement.weak) {
            when(.coin) | then(.unlocked) | somethingWeak
            when(.pass) | then(.alarming) | somethingElseWeak
        }

        matching(Enforcement.strong) {
            when(.coin) | then(.unlocked) | somethingStrong
            when(.pass) | then(.alarming) | somethingElseStrong
        }
    }
}

actions 也可用于上下文代码块

try turnstile.buildTable {
    define(.locked) {
        actions(someCommonFunction) {
            when(.coin) | then(.unlocked)
            when(.pass) | then(.alarming)
        }
    }
}

链式代码块

matching(predicate) { 
    // everything in scope matches 'predicate'
}

when(event) { 
    // everything in scope responds to 'event'
}

then(state) { 
    // everything in scope transitions to 'state'
}

actions(functionCalls) { 
    // everything in scope calls 'functionCalls'
}

上下文代码块分为两组 - 可以进行逻辑链接(或 AND 运算)的代码块,以及不能进行逻辑链接的代码块。

离散代码块 - whenthen

一个状态转换响应单个事件并转换到单个状态。因此,多个 when { }then { } 语句不能进行 AND 运算。

define(.locked) {
    when(.coin) {
        when(.pass) { } // ⛔️ does not compile
        when(.pass) | ... // ⛔️ does not compile

        matching(.something) | when(.pass) | ... // ⛔️ does not compile

        matching(.something) { 
            when(.pass) { } // ⛔️ does not compile
            when(.pass) | ... // ⛔️ does not compile
        }
    }

    then(.unlocked) {
        then(.locked) { } // ⛔️ does not compile
        then(.locked) | ... // ⛔️ does not compile

        matching(.something) | then(.locked) | ... // ⛔️ does not compile

        matching(.something) { 
            then(.locked) { } // ⛔️ does not compile
            then(.locked) | ... // ⛔️ does not compile
        }
    }      
}

存在 when { }then 的特定组合,这种组合无法编译,因为在响应单个事件(在本例中为 .coin)时,除非为每个事件提供不同的 Predicate,否则不可能转换到多个状态。

define(.locked) {
    when(.coin) {
        then(.unlocked) | action // ⛔️ does not compile
        then(.locked)   | action // ⛔️ does not compile
    }
}

define(.locked) {
    when(.coin) {
        matching(Enforcement.weak)   | then(.unlocked) | action // ✅
        matching(Enforcement.strong) | then(.locked)   | otherAction // ✅
    }
}

可链接代码块 - matchingactions

这些代码块可以按如下方式构建成链

define(.locked) {
    matching(Enforcement.weak) {
        matching(Reward.positive) { } // ✅ matches Enforcement.weak AND Reward.positive
        matching(Reward.positive) | ... // ✅ matches Enforcement.weak AND Reward.positive
    }

    actions(doSomething) {
        actions(doSomethingElse) { } // ✅ calls doSomething and doSomethingElse
        ... | doSomethingElse // ✅ calls doSomething and doSomethingElse
    }      
}

嵌套的 actions 代码块会累加动作并执行所有动作。

嵌套的 matching 语句通过 AND 运算组合在一起,这可能会无意中造成冲突。

define(.locked) {
    matching(A.x) {
        matching(A.y) {
            // 💥 error: cannot match A.x AND A.y simultaneously 
        }
    }
}

matching(or:) 语句也使用 AND 运算组合

define(.locked) {
    matching(A.x, or: A.y) {
        matching(A.z) {
            // 💥 error: cannot match A.x AND A.z simultaneously
            // 💥 error: cannot match A.y AND A.z simultaneously  
        }
    }
}

有效的嵌套 matching(or:) 语句按如下方式组合

define(.locked) {
    matching(A.x, or: A.y) {
        matching(B.x, or: B.y) {
            // ✅ logically matches (A.x OR A.y) AND (B.x OR B.y)

            // internally translates to:

            // 1. matching(A.x, and: B.x)
            // 2. matching(A.x, and: B.y)
            // 3. matching(A.y, and: B.x)
            // 4. matching(A.y, and: B.y)
        }
    }
}

混合代码块和管道

管道可以而且必须在代码块内部使用,而代码块不能在管道之后打开。

define(.locked) {
    when(.coin) | then(.unlocked) { } // ⛔️ does not compile
    when(.coin) | then(.unlocked) | actions(doSomething) { } // ⛔️ does not compile
    matching(.something) | when(.coin) { } // ⛔️ does not compile
}

条件语句

使用 Predicate 是一种通用的解决方案,但在某些情况下,它可能比解决给定问题所需的复杂性更高(有关 matching 开销的说明,请参阅Predicate 性能)。

如果需要在运行时使特定转换成为条件性的,则 condition 语句可能就足够了。

define(.locked) {
    condition(complexDecisionTree) | when(.pass) | then(.locked) | lock 
}

complexDecisionTree() 是一个返回 Bool 的函数。如果为 true,则执行转换,否则,不执行任何操作。

condition 在语法上与 matching 可互换 - 它可以与管道和代码块语法一起使用,并且可以链接。

matchingcondition 可以自由组合

define(.locked) {
    condition({ reward == .positive }) {
        matching(Enforcement.weak)   | then(.unlocked) | action
        matching(Enforcement.strong) | then(.locked)   | otherAction
    }
}

condition 在它可以表达的逻辑方面比 matching 更有限

define(.locked) {
    when(.coin) {
        matching(Enforcement.weak)   | then(.unlocked) | action
        matching(Enforcement.strong) | then(.locked)   | otherAction
    }
} // ✅ all good here

...

define(.locked) {
    when(.coin) {
        condition { enforcement == .weak   } | then(.unlocked) | action
        condition { enforcement == .strong } | then(.locked)   | otherAction
    }
} // 💥 error: logical clash

无法区分不同的 condition 语句,因为 () -> Bool 是不透明的。剩下的就是两个 define(.locked) { when(.coin) | ... } 语句,这两个语句都转换到不同的状态 - FSM 无法决定调用哪个语句,因此会 throw

运行时错误

为了保持性能,turnstile.handleEvent(event:predicates:) 没有错误处理。因此,传入未出现在转换表中的 Predicate 实例不会导致错误。尽管如此,FSM 将无法执行任何转换,因为它不包含任何与意外 Predicate 匹配的语句。调用者有责任确保传递给 handleEvent 的谓词和转换表中使用的谓词类型和数量相同。

try turnstile.buildTable { } 执行重要的错误处理,以确保表在语法和语义上有效。

扩展语法还会抛出以下其他错误

匹配错误

有两种方法可以创建无效的 matching 语句。第一种是使用单个语句

matching(A.a, and: A.b) // 💥 error: cannot match A.a AND A.b simultaneously
matching(A.a, or: B.a, and: A.b) // 💥 error: cannot match A.a AND A.b simultaneously

matching(A.a, and: A.a) // 💥 error: duplicate predicate
matching(A.a, or: A.a)  // 💥 error: duplicate predicate

matching(A.x, or: B.x)... // ⛔️ does not compile: OR types must be the same
matching(A.x, and: A.y)... // 💥 error: cannot match A.x AND A.y simultaneously

第二种是在代码块中 AND 运算多个 matching 语句

matching(A.a, and: B.a) { // ✅
    matching(A.a) // 💥 error: duplicate predicate
    matching(A.b) // 💥 error: cannot match A.a AND A.b simultaneously
}

matching(A.a, or: A.b) { // ✅
    matching(A.a) // 💥 error: duplicate predicate
    matching(A.b) // 💥 error: duplicate predicate
}

隐式冲突错误

请参阅隐式冲突

Predicate 性能

概述:对于具有 100 个转换、3 种 Predicate 类型以及每种 Predicate 10 个用例的表,每个函数调用的操作数

.eager .lazy 调度
handleEvent 1 1-7 每个转换
buildTable 100,000 100 应用程序加载时一次

FSM

添加谓词不会影响 handleEvent() 的性能,但会降低 fsm.buildTable { } 的性能。默认情况下,“eager” FSM 通过在创建转换表时提前完成大量工作,为所有隐含的 Predicate 组合填充缺失的转换,从而保持 handleEvent() 的运行时性能 O(1)。

假设使用了任何谓词,fsm.buildTable { } 主要由表的“填充”操作主导。因为必须为每个转换计算和过滤给定谓词的所有可能用例组合,所以性能为 O(m^n*o),其中 m 是每个谓词的平均用例数,n 是 Predicate 类型的数量,o 是转换的数量。

在具有 100 个转换的表中,使用三种具有 10 个用例的 Predicate 类型将需要 100,000 次操作才能编译。在大多数实际用例中,这不太可能成为问题。

注意:较少使用关键字 matching 没有任何优势。一旦使用了单词 matching,并且将 Predicate 实例传递给 handleEvent(),无论使用多少次,整个表的性能影响都将相同。

Lazy FSM

如果您的表特别大(请参阅上面的概述),Swift FSM 提供了一种更平衡的替代方案。将 .lazy 参数传递给 FSM<State, Event>(type: .lazy) 避免了前瞻算法,从而减少了内部表的大小并加快了表编译时间。代价是在每次调用 handleEvent() 时进行多次表查找操作。

handleEvent() 的性能从 O(1) 降低到 O(n!),其中 n 是使用的 Predicate *类型* 的数量,而与用例的数量无关。相反,buildTable { } 的性能从 O(m^n*o) 提高到 O(n),其中 n 是转换的数量。

现在,在具有 100 个转换的表中,使用三种具有 10 个用例的 Predicate 类型将需要 100 次操作才能编译(从 .eager 的 100,000 次降至)。每次调用 handleEvent() 都需要执行 1 到 3! + 1 或 7 次操作(从 .eager 的 1 次升至)。因此,在这种情况下,不建议使用三种以上的 Predicate 类型,因为性能会以阶乘方式降低。

在大多数情况下,.eager 是首选解决方案,而 .lazy 则为特别大量的转换和/或 Predicate 用例保留。

如果不使用任何谓词,则两个实现都是相同的。

故障排除

尽管 Swift FSM 运行时错误包含对问题的详细描述,但对于消除编译器错误的歧义,几乎无法提供帮助。

熟悉 @resultBuilder 的工作方式,以及它倾向于生成的编译时错误的种类,将有助于理解您可能遇到的任何错误。几乎所有特定于 Swift FSM 的编译时错误都将由未识别的 @resultBuilder 参数和过度重载的 | 运算符的未识别参数产生。

为了提供帮助,这里简要列出如果您尝试构建 Swift FSM 在编译时不允许构建的内容,您可能会遇到的一些常见错误

生成器问题

在对静态方法“buildExpression”的调用中没有完全匹配

这是 @resultBuilder 代码块中的常见编译时错误。如果您将代码块提供给它不支持的参数,则会发生这种情况。记住此类代码块中的每一行实际上都是提供给静态方法的参数是很有用的。

例如

try turnstile.buildTable {
     actions(thankyou) { } 
// ⛔️ No exact matches in call to static method 'buildExpression'
}

这里,actions 代码块作为参数提供给支持 buildTable 函数的 @resultBuilder 上的隐藏静态函数 buildExpressiondefine 语句已被跳过,并且 actions 返回外部代码块不支持的类型,因此无法编译。

管道问题

无法将类型 <T1> 的值转换为预期的参数类型 <T2>

这在将不受支持的参数传递给管道重载的情况下很常见。

例如

try turnstile.buildTable {
    define(.locked) {
        then(.locked) | unlock
// ⛔️ Cannot convert value of type 'Internal.Then<TurnstileState>' to expected argument type 'Internal.MatchingWhenThen'
// ⛔️ No exact matches in call to static method 'buildExpression'
    }
}

在调用 then(.locked) 之前没有 matching 和/或 when 语句。没有 | 重载在左侧接受 then(.locked) 的输出,在右侧接受代码块 () -> (),因此无法编译。

不幸的是,该错误会输出一些无法隐藏的内部实现细节(见下文)

它还会产生一个次要错误 - 因为它无法确定 then(.locked) | unlock 的输出是什么,所以它声明没有可用于 buildExpression 的重载。修复底层的 | 错误,此错误也会消失。

引用 'SIMD' 上的运算符函数 '|' 要求 'Internal.When<TurnstileEvent>' 符合 'SIMD'

try turnstile.buildTable {
    define(.locked) {
        when(.coin) | matching(P.a) | then(.locked) | unlock
// ⛔️ Referencing operator function '|' on 'SIMD' requires that 'Internal.When<TurnstileEvent>' conform to 'SIMD’
    }
}

whenmatching 的顺序颠倒且不受支持。这与之前的错误没有什么不同,但编译器对问题的解释不同。它从一个不相关的模块中选择一个 | 重载,并声明它被误用。

编译器无法帮助识别链中哪个管道导致了问题。通常,只需删除并重写语句,而不是试图弄清楚问题是什么,会更简单。

虚假问题

try turnstile.buildTable {
    let resetable = SuperState {
        when(.reset) | then(.locked)
    }

    define(.locked, adopts: resetable, onEntry: [lock]) {
        when(.coin) | then(.unlocked)
        when(.pass) | then(.alarming)
    }

    define(.unlocked, adopts: resetable, onEntry: [unlock]) {
        when(.coin) | then(.unlocked) | thankyou
        when(.pass) | then(.locked)
    }

    define(.alarming, adopts: resetable, onEntry: [alarmOn], onExit: [🦤])
}

这是来自进入和退出操作的原始示例,在末尾插入了一个小错误。这可能会也可能不会在渡渡鸟旁边产生适当的错误

无法在作用域中找到 '🦤'

它还会生成 SuperState 声明中的多个虚假错误和修复,类似于此错误

闭包中调用方法“then”需要显式使用“self”来明确捕获语义

显式引用“self.” [修复]

显式捕获“self”以在此闭包中启用隐式“self”

忽略这些错误,如果没有显示其他错误,您可能需要寻找未识别的参数。

Swift 6 语言模式

该项目主要在于需要捕获客户端函数。通过 Swift 5 后半部分的演进引入的并发规则越来越限制可以执行此操作的方式,以防止数据竞争。

这些规则并不一致,在某些情况下,Swift 5.10 的行为比 Swift 6.0 更具限制性。因此,仅当使用 Swift 6 语言模式时,才能保证 Swift FSM 按预期工作。

使用 Swift 5 语言模式可能会有效,但不能保证。

暴露的内部组件

为了构建语法,SyntaxBuilderExpandedSyntaxBuilder 中声明的每个方法都需要返回一个中间对象,FSM 使用该对象将转换表中的每个条目链接在一起。每次调用 | 都必须输出“某些东西”,即使该东西与用户无关。尽管它们的实现被标记为 internal,并且应该无法访问或修改,但您可能会在编译错误和自动完成建议中看到对其中一些对象的引用。

代码质量

Swift FSM 是使用测试驱动开发编写的,作为一个非 UI 框架,它保持 100% 代码覆盖率的要求。覆盖率不能保证测试质量,但*缺乏*覆盖率确实保证了*缺乏*测试质量。

“100%”规则的例外情况是未执行的代码 - Swift 对抽象类的拒绝仍然需要使用 fatalError("subclasses must implement") 模式,在协议无法胜任工作或无法干净利落地完成工作的情况下使用。

尽管如此,该项目仍然尽可能地尊重标准的 Swift 实践,只要这些实践不影响可测试性或产生重复代码。如果出现影响或重复,可测试性和代码去重始终优先。随着时间的推移,目标是在出现合理机会时,将“非 Swift”解决方案重构为“更 Swift”的解决方案。

如果您确实遇到已执行但未被测试覆盖的代码,请提交问题,因为缺乏覆盖率是一个严重的错误和流程故障。