{"id":13995131,"url":"https://github.com/Actomaton/Actomaton","last_synced_at":"2025-07-22T21:32:09.646Z","repository":{"id":37241067,"uuid":"403304255","full_name":"Actomaton/Actomaton","owner":"Actomaton","description":"🎭 Swift async/await \u0026 Actor-powered effectful state-management framework.","archived":false,"fork":false,"pushed_at":"2024-11-09T16:15:39.000Z","size":2087,"stargazers_count":246,"open_issues_count":3,"forks_count":11,"subscribers_count":3,"default_branch":"main","last_synced_at":"2024-11-09T16:17:40.358Z","etag":null,"topics":["actor","async-await","elm-architecture","swift"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Actomaton.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-09-05T12:35:44.000Z","updated_at":"2024-11-06T12:50:08.000Z","dependencies_parsed_at":"2024-07-28T11:45:19.455Z","dependency_job_id":null,"html_url":"https://github.com/Actomaton/Actomaton","commit_stats":null,"previous_names":[],"tags_count":16,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Actomaton%2FActomaton","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Actomaton%2FActomaton/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Actomaton%2FActomaton/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Actomaton%2FActomaton/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Actomaton","download_url":"https://codeload.github.com/Actomaton/Actomaton/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":227177786,"owners_count":17743167,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["actor","async-await","elm-architecture","swift"],"created_at":"2024-08-09T14:03:15.872Z","updated_at":"2024-11-29T17:31:05.673Z","avatar_url":"https://github.com/Actomaton.png","language":"Swift","funding_links":[],"categories":["Swift"],"sub_categories":[],"readme":"# 🎭 Actomaton\n\n[![Swift 6.0](https://img.shields.io/badge/swift-6.0-orange.svg?style=flat)](https://swift.org/download/)\n![](https://github.com/Actomaton/Actomaton/actions/workflows/main.yml/badge.svg)\n\n🧑‍🎤 Actor + 🤖 Automaton = 🎭 Actomaton\n\n**Actomaton** is Swift `async`/`await` \u0026 `Actor`-powered effectful state-management framework\ninspired by [Elm](http://elm-lang.org/) and [swift-composable-architecture](https://github.com/pointfreeco/swift-composable-architecture).\n\nThis repository consists of 3 frameworks:\n\n1. `Actomaton`: Actor-based effect-handling state-machine at its core. Linux ready.\n    - [Documentation](https://actomaton.github.io/Actomaton/documentation/actomaton/)\n2. `ActomatonUI`: SwiftUI \u0026 UIKit \u0026 Combine support\n    - [Documentation (Currently in Japanese only)](https://actomaton.github.io/Actomaton/documentation/actomatonui/)\n3. `ActomatonDebugging`: Helper module to print `Action` and `State` (with diffing) per `Reducer` call.\n    - [Documentation](https://actomaton.github.io/Actomaton/documentation/actomatondebugging/)\n\nThese frameworks depend on [swift-case-paths](https://github.com/pointfreeco/swift-case-paths) as Functional Prism library, which is a powerful tool to construct an App-level Mega-Reducer from each screen's Reducers.\n\nThis framework is a successor of the following projects:\n\n- [Harvest](https://github.com/inamiy/Harvest) (using Combine with SwiftUI support)\n- [ReactiveAutomaton](https://github.com/inamiy/ReactiveAutomaton) (using [ReactiveSwift](https://github.com/ReactiveCocoa/ReactiveSwift))\n- [RxAutomaton](https://github.com/inamiy/RxAutomaton) (using [RxSwift](https://github.com/ReactiveX/RxSwift))\n\n## Installation\n\nIn `Package.swift`:\n\n```swift\nlet package = Package(\n    ...\n    dependencies: [\n        .package(url: \"https://github.com/Actomaton/Actomaton\", .branch(\"main\"))\n    ]\n)\n```\n\nNote: Specifying by \"git version tag\" is not currently supported due to usage of unsafe flags.\nSee also: [#56](https://github.com/Actomaton/Actomaton/issues/56)\n\n## Demo App\n\n- [Actomaton-Gallery](https://github.com/Actomaton/Actomaton-Gallery)\n\n## 1. Actomaton (Core)\n\n### Example 1-1. Simple Counter\n\n```swift\nstruct State: Sendable {\n    var count: Int = 0\n}\n\nenum Action: Sendable {\n    case increment\n    case decrement\n}\n\ntypealias Environment = Void\n\nlet reducer: Reducer\u003cAction, State, Environment\u003e\nreducer = Reducer { action, state, environment in\n    switch action {\n    case .increment:\n        state.count += 1\n        return Effect.empty\n    case .decrement:\n        state.count -= 1\n        return Effect.empty\n    }\n}\n\nlet actomaton = Actomaton\u003cAction, State\u003e(\n    state: State(),\n    reducer: reducer\n)\n\n@main\nenum Main {\n    static func main() async {\n        assertEqual(await actomaton.state.count, 0)\n\n        await actomaton.send(.increment)\n        assertEqual(await actomaton.state.count, 1)\n\n        await actomaton.send(.increment)\n        assertEqual(await actomaton.state.count, 2)\n\n        await actomaton.send(.decrement)\n        assertEqual(await actomaton.state.count, 1)\n\n        await actomaton.send(.decrement)\n        assertEqual(await actomaton.state.count, 0)\n    }\n}\n```\n\nIf you want to do some logging (side-effect), add `Effect` in `Reducer` as follows:\n\n```swift\nreducer = Reducer { action, state, environment in\n    switch action {\n    case .increment:\n        state.count += 1\n        return Effect.fireAndForget {\n            print(\"increment\")\n        }\n    case .decrement:\n        state.count -= 1\n        return Effect.fireAndForget {\n            print(\"decrement and sleep...\")\n            try await Task.sleep(...) // NOTE: We can use `await`!\n            print(\"I'm awake!\")\n        }\n    }\n}\n```\n\nNOTE: There are 5 ways of creating `Effect` in Actomaton:\n\n1. No side-effects, but next action only\n    - `Effect.nextAction`\n2. Single `async` without next action\n    - `Effect.fireAndForget(id:run:)`\n3. Single `async` with next action\n    - `Effect.init(id:run:)`\n4. Multiple `async`s (i.e. `AsyncSequence`) with next actions\n    - `Effect.init(id:sequence:)`\n5. Manual cancellation\n    - `Effect.cancel(id:)` / `.cancel(ids:)`\n\n### Example 1-2. Login-Logout (and ForceLogout)\n\n![login-diagram](https://user-images.githubusercontent.com/138476/132146518-686deb5f-ff01-489a-abf2-e2ef2a2adb03.png)\n\n```swift\nenum State: Sendable {\n    case loggedOut, loggingIn, loggedIn, loggingOut\n}\n\nenum Action: Sendable {\n    case login, loginOK, logout, logoutOK\n    case forceLogout\n}\n\n// NOTE:\n// Use same `EffectID` so that if previous effect is still running,\n// next effect with same `EffectID` will automatically cancel the previous one.\n//\n// Note that `EffectID` is also useful for manual cancellation via `Effect.cancel`.\nstruct LoginFlowEffectID: EffectIDProtocol {}\n\nstruct Environment: Sendable {\n    let loginEffect: (userId: String) -\u003e Effect\u003cAction\u003e\n    let logoutEffect: Effect\u003cAction\u003e\n}\n\nlet environment = Environment(\n    loginEffect: { userId in\n        Effect(id: LoginFlowEffectID()) {\n            let loginRequest = ...\n            let data = try? await URLSession.shared.data(for: loginRequest)\n            if Task.isCancelled { return nil }\n            ...\n            return Action.loginOK // next action\n        }\n    },\n    logoutEffect: {\n        Effect(id: LoginFlowEffectID()) {\n            let logoutRequest = ...\n            let data = try? await URLSession.shared.data(for: logoutRequest)\n            if Task.isCancelled { return nil }\n            ...\n            return Action.logoutOK // next action\n        }\n    }\n)\n\nlet reducer = Reducer { action, state, environment in\n    switch (action, state) {\n    case (.login, .loggedOut):\n        state = .loggingIn\n        return environment.login(state.userId)\n\n    case (.loginOK, .loggingIn):\n        state = .loggedIn\n        return .empty\n\n    case (.logout, .loggedIn),\n        (.forceLogout, .loggingIn),\n        (.forceLogout, .loggedIn):\n        state = .loggingOut\n        return environment.logout()\n\n    case (.logoutOK, .loggingOut):\n        state = .loggedOut\n        return .empty\n\n    default:\n        return Effect.fireAndForget {\n            print(\"State transition failed...\")\n        }\n    }\n}\n\nlet actomaton = Actomaton\u003cAction, State\u003e(\n    state: .loggedOut,\n    reducer: reducer,\n    environment: environment\n)\n\n@main\nenum Main {\n    static func test_login_logout() async {\n        var t: Task\u003c(), Error\u003e?\n\n        assertEqual(await actomaton.state, .loggedOut)\n\n        t = await actomaton.send(.login)\n        assertEqual(await actomaton.state, .loggingIn)\n\n        await t?.value // wait for previous effect\n        assertEqual(await actomaton.state, .loggedIn)\n\n        t = await actomaton.send(.logout)\n        assertEqual(await actomaton.state, .loggingOut)\n\n        await t?.value // wait for previous effect\n        assertEqual(await actomaton.state, .loggedOut)\n\n        XCTAssertFalse(isLoginCancelled)\n    }\n\n    static func test_login_forceLogout() async throws {\n        var t: Task\u003c(), Error\u003e?\n\n        assertEqual(await actomaton.state, .loggedOut)\n\n        await actomaton.send(.login)\n        assertEqual(await actomaton.state, .loggingIn)\n\n        // Wait for a while and interrupt by `forceLogout`.\n        // Login's effect will be automatically cancelled because of same `EffectID.\n        try await Task.sleep(/* 1 ms */)\n        t = await actomaton.send(.forceLogout)\n\n        assertEqual(await actomaton.state, .loggingOut)\n\n        await t?.value // wait for previous effect\n        assertEqual(await actomaton.state, .loggedOut)\n\n    }\n}\n```\n\nHere we see the notions of `EffectID`, `Environment`, and `let task: Task\u003c(), Error\u003e = actomaton.send(...)`\n\n- `EffectID` is for both manual \u0026 automatic cancellation of previous running effects. In this example, `forceLogout` will cancel `login`'s networking effect.\n- `Environment` is useful for injecting effects to be called inside `Reducer` so that they become replaceable. **`Environment` is known as Dependency Injection** (using Reader monad).\n- (Optional) `Task\u003c(), Error\u003e` returned from `actomaton.send(action)` is another fancy way of dealing with \"all the effects triggered by `action`\". We can call `await task.value` to wait for all of them to be completed, or `task.cancel()` to cancel all. Note that `Actomaton` already manages such `task`s for us internally, so we normally don't need to handle them by ourselves (use this as a last resort!).\n\n### Example 1-3. Timer (using `AsyncSequence`) and `EffectID` cancellation\n\n```swift\ntypealias State = Int\n\nenum Action: Sendable {\n    case start, tick, stop\n}\n\nstruct TimerID: EffectIDProtocol {}\n\nstruct Environment {\n    let timerEffect: Effect\u003cAction\u003e\n}\n\nlet environment = Environment(\n    timerEffect: { userId in\n        Effect(id: TimerID(), sequence: {\n            AsyncStream\u003c()\u003e { continuation in\n                let task = Task {\n                    while true {\n                        try await Task.sleep(/* 1 sec */)\n                        continuation.yield(())\n                    }\n                }\n\n                continuation.onTermination = { @Sendable _ in\n                    task.cancel()\n                }\n            }\n        })\n    }\n)\n\nlet reducer = Reducer { action, state, environment in\n    switch action {\n    case .start:\n        return environment.timerEffect\n    case .tick:\n        state += 1\n        return .empty\n    case .stop:\n        return Effect.cancel(id: TimerID())\n    }\n}\n\nlet actomaton = Actomaton\u003cAction, State\u003e(\n    state: 0,\n    reducer: reducer,\n    environment: environment\n)\n\n@main\nenum Main {\n    static func test_timer() async {\n        assertEqual(await actomaton.state, 0)\n\n        await actomaton.send(.start)\n\n        assertEqual(await actomaton.state, 0)\n\n        try await Task.sleep(/* 1 sec */)\n        assertEqual(await actomaton.state, 1)\n\n        try await Task.sleep(/* 1 sec */)\n        assertEqual(await actomaton.state, 2)\n\n        try await Task.sleep(/* 1 sec */)\n        assertEqual(await actomaton.state, 3)\n\n        await actomaton.send(.stop)\n\n        try await Task.sleep(/* long enough */)\n        assertEqual(await actomaton.state, 3,\n                    \"Should not increment because timer is stopped.\")\n    }\n}\n```\n\nIn this example, `Effect(id:sequence:)` is used for timer effect, which yields `Action.tick` multiple times.\n\n### Example 1-4. `EffectQueue`\n\n```swift\nenum Action: Sendable {\n    case fetch(id: String)\n    case _didFetch(Data)\n}\n\nstruct State: Sendable {} // no state\n\nstruct Environment: Sendable {\n    let fetch: @Sendable (_ id: String) async throws -\u003e Data\n}\n\nstruct DelayedEffectQueue: EffectQueueProtocol {\n    // First 3 effects will run concurrently, and other sent effects will be suspended.\n    var effectQueuePolicy: EffectQueuePolicy {\n        .runOldest(maxCount: 3, .suspendNew)\n    }\n\n    // Adds delay between effect start. (This is useful for throttling / deboucing)\n    var effectQueueDelay: EffectQueueDelay {\n        .random(0.1 ... 0.3)\n    }\n}\n\nlet reducer = Reducer\u003cAction, State, Environment\u003e { action, state, environment in\n    switch action {\n    case let .fetch(id):\n        return Effect(queue: DelayedEffectQueue()) {\n            let data = try await environment.fetch(id)\n            return ._didFetch(data)\n        }\n    case let ._didFetch(data):\n        // Do something with `data`.\n        return .empty\n    }\n}\n\nlet actomaton = Actomaton\u003cAction, State\u003e(\n    state: State(),\n    reducer: reducer,\n        environment: Environment(fetch: { /* ... */ })\n)\n\nawait actomaton.send(.fetch(id: \"item1\"))\nawait actomaton.send(.fetch(id: \"item2\")) // min delay of 0.1\nawait actomaton.send(.fetch(id: \"item3\")) // min delay of 0.1 (after item2 actually starts)\nawait actomaton.send(.fetch(id: \"item4\")) // starts when item1 or 2 or 3 finishes\n```\n\nAbove code uses a custom `DelayedEffectQueue` that conforms to `EffectQueueProtocol` with suspendable `EffectQueuePolicy` and delays between each effect by `EffectQueueDelay`.\n\nSee [EffectQueuePolicy](https://github.com/Actomaton/Actomaton/blob/main/Sources/Actomaton/EffectQueuePolicy.swift) for how each policy takes different queueing strategy for effects.\n\n```swift\n/// `EffectQueueProtocol`'s buffering policy.\npublic enum EffectQueuePolicy: Hashable, Sendable\n{\n    /// Runs `maxCount` newest effects, cancelling old running effects.\n    case runNewest(maxCount: Int)\n\n    /// Runs `maxCount` old effects with either suspending or discarding new effects.\n    case runOldest(maxCount: Int, OverflowPolicy)\n\n    public enum OverflowPolicy: Sendable\n    {\n        /// Suspends new effects when `.runOldest` `maxCount` of old effects is reached until one of them is completed.\n        case suspendNew\n\n        /// Discards new effects when `.runOldest` `maxCount` of old effects is reached until one of them is completed.\n        case discardNew\n    }\n}\n```\n\nFor convenient `EffectQueueProtocol` protocol conformance, there are built-in sub-protocols:\n\n```swift\n/// A helper protocol where `effectQueuePolicy` is set to `.runNewest(maxCount: 1)`.\npublic protocol Newest1EffectQueueProtocol: EffectQueueProtocol {}\n\n/// A helper protocol where `effectQueuePolicy` is set to `.runOldest(maxCount: 1, .discardNew)`.\npublic protocol Oldest1DiscardNewEffectQueueProtocol: EffectQueueProtocol {}\n\n/// A helper protocol where `effectQueuePolicy` is set to `.runOldest(maxCount: 1, .suspendNew)`.\npublic protocol Oldest1SuspendNewEffectQueueProtocol: EffectQueueProtocol {}\n```\n\nso that we can write in one-liner: `struct MyEffectQueue: Newest1EffectQueueProtocol {}`\n\n### Example 1-5. Reducer composition\n\n[Actomaton-Gallery](https://github.com/Actomaton/Actomaton-Gallery) provides a good example of how `Reducer`s can be combined together into one big Reducer using `Reducer.combine`.\n\nIn this example, [swift-case-paths](https://github.com/pointfreeco/swift-case-paths) is used as a counterpart of `WritableKeyPath`, so if we use both, we can easily construct Mega-Reducer without a hassle.\n\n(NOTE: `CasePath` is useful when dealing with enums, e.g. `enum Action` and `enum Current` in this example)\n\n```swift\nenum Root {} // just a namespace\n\nextension Root {\n    enum Action: Sendable {\n        case changeCurrent(State.Current?)\n\n        case counter(Counter.Action)\n        case stopwatch(Stopwatch.Action)\n        case stateDiagram(StateDiagram.Action)\n        case todo(Todo.Action)\n        case github(GitHub.Action)\n    }\n\n    struct State: Equatable, Sendable {\n        var current: Current?\n\n        // Current screen (NOTE: enum, so only 1 screen will appear)\n        enum Current: Equatable {\n            case counter(Counter.State)\n            case stopwatch(Stopwatch.State)\n            case stateDiagram(StateDiagram.State)\n            case todo(Todo.State)\n            case github(GitHub.State)\n        }\n    }\n\n    // NOTE: `contramap` is also called `pullback` in swift-composable-architecture.\n    static var reducer: Reducer\u003cAction, State, Environment\u003e {\n        Reducer.combine(\n            Counter.reducer\n                .contramap(action: /Action.counter)\n                .contramap(state: /State.Current.counter)\n                .contramap(state: \\State.current)\n                .contramap(environment: { _ in () }),\n\n            Todo.reducer\n                .contramap(action: /Action.todo)\n                .contramap(state: /State.Current.todo)\n                .contramap(state: \\State.current)\n                .contramap(environment: { _ in () }),\n\n            StateDiagram.reducer\n                .contramap(action: /Action.stateDiagram)\n                .contramap(state: /State.Current.stateDiagram)\n                .contramap(state: \\State.current)\n                .contramap(environment: { _ in () }),\n\n            Stopwatch.reducer\n                .contramap(action: /Action.stopwatch)\n                .contramap(state: /State.Current.stopwatch)\n                .contramap(state: \\State.current)\n                .contramap(environment: { $0.stopwatch }),\n\n            GitHub.reducer\n                .contramap(action: /Action.github)\n                .contramap(state: /State.Current.github)\n                .contramap(state: \\State.current)\n                .contramap(environment: { $0.github })\n        )\n    }\n}\n```\n\nTo learn more about `CasePath`, visit the official site and tutorials:\n\n- [swift-case-paths](https://github.com/pointfreeco/swift-case-paths)\n- [Episode #87: The Case for Case Paths: Introduction](https://www.pointfree.co/episodes/ep87-the-case-for-case-paths-introduction)\n\n## 2. ActomatonUI (SwiftUI \u0026 UIKit)\n\n`Store` (from `ActomatonUI.framework`) provides a thin wrapper of `Actomaton` to work seamlessly in SwiftUI and UIKit world.\n\nTo find out more, check the following resources:\n\n- [Actomaton-Gallery](https://github.com/Actomaton/Actomaton-Gallery) (example apps)\n- [ActomatonUI | Documentation](https://actomaton.github.io/Actomaton/documentation/actomatonui/)\n    - [RouteStore チュートリアル | Documentation](https://actomaton.github.io/Actomaton/documentation/actomatonui/oop-tutorial) (in Japanese)\n\n## References\n\n- [Functional iOS Architecture for SwiftUI - Speaker Deck](https://speakerdeck.com/inamiy/functional-ios-architecture-for-swiftui)\n- [Functional iOS Architecture for SwiftUI (English)](https://zenn.dev/inamiy/books/3dd014a50f321040a047)\n- [Swift アクターモデルと Elm Architecture の融合](https://speakerdeck.com/inamiy/iosdc-japan-2022) (Japanese)\n\n## License\n\n[MIT](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FActomaton%2FActomaton","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FActomaton%2FActomaton","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FActomaton%2FActomaton/lists"}