{"id":13507239,"url":"https://github.com/sasa1977/fsm","last_synced_at":"2025-05-16T09:05:54.171Z","repository":{"id":57501568,"uuid":"12301027","full_name":"sasa1977/fsm","owner":"sasa1977","description":"Finite State Machine data structure","archived":false,"fork":false,"pushed_at":"2020-02-04T23:18:03.000Z","size":24,"stargazers_count":361,"open_issues_count":0,"forks_count":22,"subscribers_count":9,"default_branch":"master","last_synced_at":"2025-05-13T12:54:32.996Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Elixir","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/sasa1977.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}},"created_at":"2013-08-22T15:44:56.000Z","updated_at":"2025-03-22T20:03:19.000Z","dependencies_parsed_at":"2022-09-19T09:10:42.458Z","dependency_job_id":null,"html_url":"https://github.com/sasa1977/fsm","commit_stats":null,"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sasa1977%2Ffsm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sasa1977%2Ffsm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sasa1977%2Ffsm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sasa1977%2Ffsm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sasa1977","download_url":"https://codeload.github.com/sasa1977/fsm/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254501557,"owners_count":22081528,"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":[],"created_at":"2024-08-01T02:00:28.497Z","updated_at":"2025-05-16T09:05:49.164Z","avatar_url":"https://github.com/sasa1977.png","language":"Elixir","funding_links":[],"categories":["Algorithms and Data structures","Algorithms and Datastructures"],"sub_categories":[],"readme":"# Fsm\n\n**This project is not maintained anymore, and I don't advise using it. Pure functional FSMs are still my preferred approach (as opposed to gen_statem), but you don't need this library for that. Regular data structures, such as maps or structs, with pattern matching in multiclauses will serve you just fine.**\n\nFsm is pure functional finite state machine. Unlike `gen_fsm`, it doesn't run in its own process. Instead, it is a functional data structure.\n\n## Why?\n\nIn the rare cases I needed a proper fsm, I most often wanted to use it inside the already existing process, together with the already present state data. Creating another process didn't work for me because that requires additional bookkeeping such as supervising and process linking. More importantly, fsm as a process implies mutability and side effects, which is harder to deal with. In addition, `gen_fsm` introduces more complicated protocol of cross-process communication such as `send_event`, `sync_send_event`, `send_all_state_event` and `sync_send_all_state_event`.\n\nUnlike `gen_fsm`, the `Fsm` data structure has following benefits:\n\n* It is immutable and side-effect free\n* No need to create and manage separate processes\n* You can persist it, use it via ets, embed it inside `gen_server` or plain processes\n\n## Basic example\n\n```elixir\ndefmodule BasicFsm do\n  use Fsm, initial_state: :stopped\n\n  defstate stopped do         # opens the state scope\n    defevent run do           # defines event\n      next_state(:running)    # transition to next state\n    end\n  end\n\n  defstate running do\n    defevent stop do\n      next_state(:stopped)\n    end\n  end\nend\n```\n\nUsage:\n\nBe sure to include a dependency in your mix.exs:\n\n```elixir\ndeps: [{:fsm, \"~\u003e 0.3.1\"}, ...]\n```\n\n```elixir\n# basic usage\nBasicFsm.new\n|\u003e BasicFsm.run\n|\u003e BasicFsm.stop\n\n# invalid state/event combination throws exception\nBasicFsm.new\n|\u003e BasicFsm.run\n|\u003e BasicFsm.run\n\n# you can query fsm for its state:\nBasicFsm.new\n|\u003e BasicFsm.run\n|\u003e BasicFsm.state\n```\n\n## Data\nAs you probably know, basic fsm is not Turing complete, and has limited uses. Therefore, `Fsm` introduces concept of data, just like `gen_fsm`:\n\n```elixir\ndefmodule DataFsm do\n  use Fsm, initial_state: :stopped, initial_data: 0\n\n  defstate stopped do\n    defevent run(speed) do                    # events can have arguments\n      next_state(:running, speed)             # changing state and data\n    end\n  end\n\n  defstate running do\n    defevent slowdown(by), data: speed do     # you can pattern match data with dedicated option\n      next_state(:running, speed - by)\n    end\n\n    defevent stop do\n      next_state(:stopped, 0)\n    end\n  end\nend\n\nDataFsm.new\n|\u003e DataFsm.run(50)\n|\u003e DataFsm.slowdown(10)\n|\u003e DataFsm.data\n```\n\n## Global handlers\n\nNormally, undefined state/event mapping throws an exception. You can handle this by using special `_` event definition:\n\n```elixir\ndefmodule BasicFsm do\n  use Fsm, initial_state: :stopped\n\n  defstate stopped do\n    defevent run, do: next_state(:running)\n\n    # called for undefined state/event mapping when inside stopped state\n    defevent _, do:\n  end\n\n  defstate running do\n    defevent stop, do: next_state(:stopped)\n  end\n\n  # called for some_event, regardless of the state\n  defevent some_event, do:\n\n  # called for undefined state/event mapping when inside any state\n  defevent _, do:\nend\n```\n\nKeep in mind that public functions are defined only for the specified events. In the example above those are `run`, `stop`, and `some_event`. So you cannot call `BasicFsm.undefined_event`, because such event is not defined. You can explicitly define events, without adding them to state/event map:\n\n```elixir\ndefmodule MyFsm do\n  defevent my_event1        # 0 arity event\n  defevent my_event2/2      # 2 arity event\nend\n```\n\nIn global handlers, it is often useful to know about event context:\n\n```elixir\ndefevent _, state: state, data: data, event: event, args: args do\n  # now you can reference state, data, event and args\n  ...\nend\n```\n\n## Pattern matching and options\n\nPattern matching works with event arguments, and all available options:\n\n```elixir\ndefstate some_state do\n  defevent event(1), do:\n  defevent event(2), do:\n\n  defevent event(x), state: 0, do:\n  defevent event(x), state: 1, do:\nend\n```\n\nIt is allowed to define multiple global handlers:\n\n```elixir\ndefevent _, event: :event_1, do:\ndefevent _, event: :event_2, do:\ndefevent _, event: something_else, do:\n```\n\nYou can also specify guards:\n```elixir\ndefevent my_event, when: ..., do:\n```\n\n## Event results\nThe result of the event handler determines the response of the event:\n\n```elixir\ndefevent my_event do\n  ...\n  next_state(:new_state)             # data remains the same\nend\n\ndefevent my_event do\n  ...\n  next_state(:new_state, new_data)\nend\n```\n\nThe result of the event will be the new fsm instance:\n\n```elixir\nfsm2 = MyFsm.my_event(fsm)\nMyFsm.another_event(fsm2, ...)\n```\n\nYou can also return some result and the modified fsm instance:\n```elixir\nrespond(response)                         # data and state remain the same\nrespond(response, :new_state)             # data remains the same\nrespond(response, :new_state, new_data)\n```\n\nIn this case, the result of calling the event is a two elements tuple:\n```elixir\n{response, fsm2} = MyFsm.my_event(mfs)\n```\n\nIf the result of event handler is not created via `next_state` or `respond` it will be ignored, and the input fsm instance will be returned. This is useful when the event handler needs to perform some side-effect operations (file or network I/O) without changing the state or data.\n\n## Dynamic definitions\nFsm macros are runtime friendly, so you can build your fsm dynamically:\n\n```elixir\ndefmodule DynamicFsm do\n  use Fsm, initial_state: :stopped\n\n  # define states and transition\n  fsm = [\n    stopped: [run: :running],\n    running: [stop: :stopped]\n  ]\n\n  # loop through definition and dynamically call defstate/defevent\n  for {state, transitions} \u003c- fsm do\n    defstate unquote(state) do\n      for {event, target_state} \u003c- transitions do\n        defevent unquote(event) do\n          next_state(unquote(target_state))\n        end\n      end\n    end\n  end\nend\n```\n\nYou might use this to define your fsm in the separate file, and in compile time read it and build the corresponding module.\n\n## Generated functions\nNormally, `defevent` generates corresponding public interface function, which has the same name as the event. In addition, the multi-clause public `transition` function exists where all possible transitions are implemented. Interface functions simply delegate to the `transition` function, and their purpose is simply to have nicer looking interface.\n\nYou can make interface function private:\n\n```elixir\ndefeventp ...\n```\n\nThe `transition` function is always public. It can be used for dynamic fsm manipulation:\n\n```elixir\nMyFsm.transition(fsm, :my_event, [arg1, arg2])\n```\n\nNotice that with `transition`, you can also use undefined events, and they will be caught by global handlers (if such exist).\n\n## Extending the module\nInside your fsm module, you can add additional functions which manipulate the fsm. An fsm instance is represented by the private `fsm_rec` record:\n\n```elixir\ndef my_fun(fsm_rec() = fsm, ...), do:\n```\n\n## In a separate process\nFsm makes sense even when used from a separate process. Instead of relying on `gen_fsm` verbs, you can use `gen_server` simple call/cast approach. If the interface of the fsm is large, it may be tedious to create wrappers for all events. Runtime friendly [ExActor](https://github.com/sasa1977/exactor) can make your life a bit easier:\n\n```elixir\ndefmodule BasicFsmServer do\n  use ExActor\n\n  def init(_), do: initial_state(BasicFsm.new)\n\n  # dynamic wrapping of zero arity events inside casts\n  for event \u003c- [:run, :stop] do\n    defcast unquote(event), state: fsm do\n      BasicFsm.unquote(event)(fsm)\n      |\u003e new_state\n    end\n  end\n\n  # call wrapper to get the state\n  defcall state, state: fsm, do: BasicFsm.state(fsm)\nend\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsasa1977%2Ffsm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsasa1977%2Ffsm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsasa1977%2Ffsm/lists"}