{"id":17862703,"url":"https://github.com/stevana/pipelined-state-machines","last_synced_at":"2025-07-22T15:34:24.523Z","repository":{"id":79986704,"uuid":"605569232","full_name":"stevana/pipelined-state-machines","owner":"stevana","description":"An experiment in declaratively programming parallel pipelines of state machines.","archived":false,"fork":false,"pushed_at":"2023-03-20T10:21:09.000Z","size":98,"stargazers_count":15,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-04-03T15:44:54.341Z","etag":null,"topics":["disruptor","pipelining","state-machines"],"latest_commit_sha":null,"homepage":"","language":"Haskell","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/stevana.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","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":"2023-02-23T12:44:19.000Z","updated_at":"2024-09-29T04:49:08.000Z","dependencies_parsed_at":null,"dependency_job_id":"2cc02fe3-a37a-4876-b59b-1958ab4946e2","html_url":"https://github.com/stevana/pipelined-state-machines","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/stevana/pipelined-state-machines","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevana%2Fpipelined-state-machines","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevana%2Fpipelined-state-machines/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevana%2Fpipelined-state-machines/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevana%2Fpipelined-state-machines/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stevana","download_url":"https://codeload.github.com/stevana/pipelined-state-machines/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevana%2Fpipelined-state-machines/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":266521106,"owners_count":23942389,"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","status":"online","status_checked_at":"2025-07-22T02:00:09.085Z","response_time":66,"last_error":null,"robots_txt_status":null,"robots_txt_updated_at":null,"robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["disruptor","pipelining","state-machines"],"created_at":"2024-10-28T08:54:44.767Z","updated_at":"2025-07-22T15:34:24.471Z","avatar_url":"https://github.com/stevana.png","language":"Haskell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# pipelined-state-machines\n\nAn experiment in declaratively programming parallel pipelines of state machines.\n\n## Motivation\n\nImagine a flat complex in Sweden. Being the socialist utopia Sweden is there's a\nshared laundry room which the people in the flat complex can book. In the\nlaundry room there's everything one needs to wash, dry and iron your clothes.\nYou don't even need to bring your own laundry detergent!\n\nLets call three people living there Ann, Bo and Cecilia, and lets assume they\nall want to use the laundry room. Depending on how the booking system is\nimplemented the total time it would take for all three people to do their\nlaundry varies.\n\nFor example if the booking system allocates a big time slot per person in which\nthat person can do the whole cycle of *W*ashing, *D*rying and *I*roning then,\nassuming each step takes one time unit, we get a situation like this:\n\n          Person\n            ^\n        Ann | W D I                             W = Washing\n         Bo |       W D I                       D = Drying\n    Cecilia |             W D I                 I = Ironing\n            +-------------------\u003e Time\n            0 1 2 3 4 5 6 7 8 9\n\nBo cannot start washing until Ann is done ironing, because Ann has booked the\nroom for the whole cycle, and so on.\n\nIf the booking system is more granular and allows booking a time slot per step\nthen we can get a situation that looks like this:\n\n          Person\n            ^\n        Ann | W D I\n         Bo |   W D I\n    Cecilia |     W D I\n            +-------------------\u003e Time\n            0 1 2 3 4 5 6 7 8 9\n\nIt should be clear that the total time is shorter in this case, because the\nmachines are utilised better (Bo can start using the washing machine right after\nAnn is done with it). Also note that if each person would start a new washing\nafter they finish ironing the first one and so on then the time savings would be\neven greater.\n\nThis optimisation is called pipelining. It's used a lot in manufacturing, for\nexample Airbus [builds](https://youtu.be/oxjT7veKi9c?t=2682) two airplanes per\nday. If you were to order a plane today you'd get it delivered in two months\ntime. How is that they deliver two per day if it takes two months to build them?\nPipelining! It's also used inside CPUs to [pipeline\ninstructions](https://en.wikipedia.org/wiki/Instruction_pipelining).\n\nThe rest of this document is an experiment in how we can construct such\npipelining in software in a declarative way.\n\n## Usage\n\nThe workers or stages in our pipeline will be state machines of the following\ntype.\n\n```haskell\ndata SM s a b where\n  Id      :: SM s a a\n  Compose :: SM s b c -\u003e SM s a b -\u003e SM s a c\n  Fst     :: SM s (a, b) a\n  Snd     :: SM s (a, b) b\n  (:\u0026\u0026\u0026)  :: SM s a c -\u003e SM s a d -\u003e SM s a (c, d)\n  (:***)  :: SM s a c -\u003e SM s b d -\u003e SM s (a, b) (c, d)\n  SlowIO  :: SM s a a -- Simulate a slow I/O computation.\n```\n\nHere's an example of a stage which takes an ordered pair as input and swaps the\nelements of the pair. Note the use of `SlowIO` to simulate that some slow I/O\ncomputation happens.\n\n```haskell\nswap :: SM () (a, b) (b, a)\nswap = Snd :*** Fst `Compose` copy `Compose` SlowIO\n  where\n    copy = Id :\u0026\u0026\u0026 Id\n```\n\nWe can `interpret` such state machines into plain functions as follows.\n\n```haskell\ninterpret :: SM s a b -\u003e (a -\u003e s -\u003e IO (s, b))\ninterpret Id            x s = return (s, x)\ninterpret (Compose g f) x s = do\n  (s', y) \u003c- interpret f x s\n  interpret g y s'\ninterpret Fst           x s = return (s, fst x)\ninterpret Snd           x s = return (s, snd x)\ninterpret (f :\u0026\u0026\u0026 g)    x s = do\n  (s', y)  \u003c- interpret f x s\n  (s'', z) \u003c- interpret g x s'\n  return (s'', (y, z))\ninterpret (f :*** g)    x s = do\n  (s', y)  \u003c- interpret f (fst x) s\n  (s'', z) \u003c- interpret g (snd x) s'\n  return (s'', (y, z))\ninterpret SlowIO x s = do\n  threadDelay 200000 -- 0.2s\n  return (s, x)\n```\n\nNext lets have a look at how we can construct pipelines of such state machines.\n\n```haskell\ndata P a b where\n  SM     :: String -\u003e SM s a b -\u003e s -\u003e P a b\n  (:\u003e\u003e\u003e) :: P a b -\u003e P b c -\u003e P a c\n```\n\nThe following is an example pipeline where there's only one stage in which we do\nour pair swapping three times.\n\n```haskell\nswapsSequential :: P (a, b) (b, a)\nswapsSequential = SM \"three swaps\" (swap `Compose` swap `Compose` swap) ()\n```\n\nThe above corresponds to our coarse grained booking system where the laundry was\nbooked for the whole cycle. Whereas the following corresponds to the more fine\ngrained approach where we get pipelining.\n\n```haskell\nswapsPipelined :: P (a, b) (b, a)\nswapsPipelined =\n  SM \"first swap\"  swap () :\u003e\u003e\u003e\n  SM \"second swap\" swap () :\u003e\u003e\u003e\n  SM \"third swap\"  swap ()\n```\n\nA pipeline can be deployed, we'll use the following type to keep track of the\nqueue associated with the pipeline as well as the name and pids of the state\nmachines involved in the pipeline.\n\n```haskell\ndata Deployment a = Deployment\n  { queue :: TQueue a\n  , pids  :: [(String, Async ())]\n  }\n\nnames :: Deployment a -\u003e String\nnames = bracket . intercalate \",\" . reverse . map fst . pids\n  where\n    bracket s = \"[\" ++ s ++ \"]\"\n```\n\nHere's the actual `deploy`ment function which takes a pipeline and gives back an\ninput-queue and a `Deployment` which holds the output-queue and the names and\npids of the state machines.\n\n```haskell\ndeploy :: P a b -\u003e IO (TQueue a, Deployment b)\ndeploy p = do\n  q \u003c- newTQueueIO\n  d \u003c- deploy' p (Deployment q [])\n  return (q, d)\n\ndeploy' :: P a b -\u003e Deployment a -\u003e IO (Deployment b)\ndeploy' (SM name sm s0) d = do\n  q' \u003c- newTQueueIO\n  pid \u003c- async (go s0 q')\n  return Deployment { queue = q', pids = (name, pid) : pids d }\n  where\n    f = interpret sm\n\n    go s q' = do\n      x \u003c- atomically $ readTQueue (queue d)\n      (s', o) \u003c- f x s\n      atomically $ writeTQueue q' o\n      go s' q'\ndeploy' (sm :\u003e\u003e\u003e sm') d = do\n  d' \u003c- deploy' sm d\n  deploy' sm' d'\n```\n\nWe now have everything we need to run a simple benchmark comparing the\nsequential version of three swaps versus the pipelined version.\n\n```haskell\ndata PipelineKind = Sequential | Pipelined\n  deriving Show\n\nmain :: IO ()\nmain = do\n  mapM_ libMain [Sequential, Pipelined]\n\nlibMain :: PipelineKind -\u003e IO ()\nlibMain k = do\n  (q, d) \u003c- deploy $ case k of\n                       Sequential -\u003e swapsSequential\n                       Pipelined  -\u003e swapsPipelined\n  print k\n  putStrLn $ \"Pids: \" ++ names d\n  start \u003c- getCurrentTime\n  forM_ [(1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 7)] $ \\x -\u003e\n    atomically $ writeTQueue q x\n  resps \u003c- replicateM 6 $ atomically $ readTQueue (queue d)\n  end \u003c- getCurrentTime\n  putStrLn $ \"Responses: \" ++ show resps\n  putStrLn $ \"Time: \" ++ show (diffUTCTime end start)\n  putStrLn \"\"\n```\n\nWe can run the above with `cabal run readme-pipeline-example`, which results in\nsomething like the following being printed to the screen.\n\n```\nSequential\nPids: [three swaps]\nResponses: [(2,1),(3,2),(4,3),(5,4),(6,5),(7,6)]\nTime: 3.611045787s\n\nPipelined\nPids: [first swap,second swap,third swap]\nResponses: [(2,1),(3,2),(4,3),(5,4),(6,5),(7,6)]\nTime: 1.604990775s\n```\n\nCool, we managed to reduce the total running time by more than half! We can do\neven better though! In addition to pipelining we can also shard the queues by\nletting two state machines work on the same queue, the first processing the\nelements in the even positions of the queue and the second processing the\nelements in the odd positions.\n\n```diff\ndata P a b where\n  SM     :: String -\u003e SM s a b -\u003e s -\u003e P a b\n  (:\u003e\u003e\u003e) :: P a b -\u003e P b c -\u003e P a c\n+ Shard  :: P a b -\u003e P a b\n```\n\nHere's an example of a sharded pipeline, where each shard will spawn two state\nmachines (one working on the even indexes of the queue and the other on the\nodd).\n\n```haskell\nswapsSharded :: P (a, b) (b, a)\nswapsSharded =\n  Shard (SM \"first swap\"  swap ()) :\u003e\u003e\u003e\n  Shard (SM \"second swap\" swap ()) :\u003e\u003e\u003e\n  Shard (SM \"third swap\"  swap ())\n```\n\nIn the deployment of shards, we achieve the even-odd split by reading from the\ninput queue, `qIn`, and first writing to the even queue, `qEven`, and then\nswitching over to the odd queue, `qOdd`, when making the recursive call in\n`shardQIn`. Whereas `shardQOut` does the inverse and merges the two queues back\ninto the output queue:\n\n```diff\n+ deploy' (Shard p) d = do\n+   let qIn = queue d\n+   qEven  \u003c- newTQueueIO\n+   qOdd   \u003c- newTQueueIO\n+   pidIn  \u003c- async $ shardQIn qIn qEven qOdd\n+   dEven  \u003c- deploy' p (Deployment qEven [])\n+   dOdd   \u003c- deploy' p (Deployment qOdd [])\n+   qOut   \u003c- newTQueueIO\n+   pidOut \u003c- async $ shardQOut (queue dEven) (queue dOdd) qOut\n+   return (Deployment qOut ((\"shardIn:  \" ++ names dEven ++ \" \u0026 \" ++ names dOdd, pidIn) :\n+                            (\"shardOut: \" ++ names dEven ++ \" \u0026 \" ++ names dOdd, pidOut) :\n+                            pids dEven ++ pids dOdd ++ pids d))\n+   where\n+     shardQIn :: TQueue a -\u003e TQueue a -\u003e TQueue a -\u003e IO ()\n+     shardQIn  qIn qEven qOdd = do\n+       atomically (readTQueue qIn \u003e\u003e= writeTQueue qEven)\n+       shardQIn qIn qOdd qEven\n+\n+     shardQOut :: TQueue a -\u003e TQueue a -\u003e TQueue a -\u003e IO ()\n+     shardQOut qEven qOdd qOut = do\n+       atomically (readTQueue qEven \u003e\u003e= writeTQueue qOut)\n+       shardQOut qOdd qEven qOut\n```\n\nRunning this version we see more than 3.5x speed-up compared to the sequential\npipeline.\n\n```\nSharded\nPids: [first swap,first swap,shardOut: [first swap] \u0026 [first swap],shardIn:  [first swap] \u0026 [first swap],second swap,second swap,shardOut: [second swap] \u0026 [second swap],shardIn:  [second swap] \u0026 [second swap],third swap,third swap,shardOut: [third swap] \u0026 [third swap],shardIn:  [third swap] \u0026 [third swap]]\nResponses: [(2,1),(3,2),(4,3),(5,4),(6,5),(7,6)]\nTime: 1.00241912s\n```\n\nThere are still many more improvements to be made here:\n\n  * Avoid spawning threads for merely shuffling elements between queues, e.g.\n    `shardQ{In, Out}` above;\n  * Avoid copying elements between queues;\n  * Back-pressure;\n  * Batching.\n\nI believe all these problems can be solved by choosing a better concurrent queue\ndata structure than `TQueue`, so that's what we'll have a look at next.\n\n## Disruptor\n\nThe `Disruptor*` modules are a Haskell port of the [LMAX\nDisruptor](https://github.com/LMAX-Exchange/disruptor), which is a high\nperformance inter-thread messaging library. The developers at LMAX, which\noperates a financial exchange,\n[reported](https://www.infoq.com/presentations/LMAX/) in 2010 that they could\nprocess more than 100,000 transactions per second at less than 1 millisecond\nlatency.\n\nAt its core it's just a lock-free concurrent queue, but it also provides\nbuilding blocks for achieving several useful concurrent programming tasks that\ntypical queues don't (or at least don't make obvious how to do). The extra\nfeatures include:\n\n  * Multi-cast (many consumers can in parallel process the same event);\n  * Batching (both on producer and consumer side);\n  * Back-pressure;\n  * Sharding for scalability;\n  * Dependencies between consumers.\n\nIt's also performs better than most queues, as we shall see further down.\n\n### Example\n\n```haskell\nimport Control.Concurrent\nimport Control.Concurrent.Async\nimport Disruptor.SP\n\nmain :: IO ()\nmain = do\n\n  -- Create the shared ring buffer.\n  let bufferCapacity = 128\n  rb \u003c- newRingBuffer bufferCapacity\n\n  -- The producer keeps a counter and produces events that are merely the pretty\n  -- printed value as a string of that counter.\n  let produce :: Int -\u003e IO (String, Int)\n      produce n = return (show n, n + 1)\n\n      -- The counter starts at zero.\n      initialProducerState = 0\n\n      -- No back-pressure is applied in this example.\n      backPressure :: Int -\u003e IO ()\n      backPressure _ = return ()\n\n  producer \u003c- newEventProducer rb produce backPressure initialProducerState\n\n  -- The consumer merely prints the string event to the terminal.\n  let consume :: () -\u003e String -\u003e SequenceNumber -\u003e EndOfBatch -\u003e IO ()\n      consume () event snr endOfBatch =\n        putStrLn (event ++ if endOfBatch then \" (end of batch)\" else \"\")\n\n      -- The consumer doesn't need any state in this example.\n      initialConsumerState = ()\n\n      -- Which other consumers do we need to wait for before consuming an event?\n      dependencies = []\n\n      -- What to do in case there are no events to consume?\n      waitStrategy = Sleep 1\n\n  consumer \u003c- newEventConsumer rb consume initialConsumerState dependencies waitStrategy\n\n  -- Tell the ring buffer which the last consumer is, to avoid overwriting\n  -- events that haven't been consumed yet.\n  setGatingSequences rb [ecSequenceNumber consumer]\n\n  withEventProducer producer $ \\ap -\u003e\n    withEventConsumer consumer $ \\ac -\u003e do\n      threadDelay (3 * 1000 * 1000) -- 3 sec\n      cancel ap\n      cancel ac\n```\n\nYou can run the above example with `cabal run readme-disruptor-example`.\n\nA couple of things we could change to highlight the features we mentioned in the\nabove section:\n\n  1. Add a second consumer that saves the event to disk, this consumer would be\n     slower than the current one which logs to the terminal, but we could use\n     buffer up events in memory and only actually write when the end of batch\n     flag is set to speed things up;\n\n  2. We could also shard depending on the sequence number, e.g. have two slower\n     consumers that write to disk and have one of them handle even sequence numbers\n     while the other handles odd ones;\n\n  3. The above producer writes one event at the time to the ring buffer, but\n     since we know at which sequence number the last consumer is at we can\n     easily make writes in batches as well;\n\n  4. Currently the producer doesn't apply any back-pressure when the ring buffer\n     is full, in a more realistic example where the producer would, for example,\n     create events from requests made to a http server we could use\n     back-pressure to tell the http server to return status code 429 (too many\n     requests);\n\n  5. If we have one consumer that writes to the terminal and another one that\n     concurrently writes to disk, we could add a third consumer that does\n     something with the event only if it has both been logged and stored to disk\n     (i.e. the third consumer depends on both the first and the second).\n\n### How it works\n\nThe ring buffer is implemented using a bounded array, it keeps track of a\nmonotonically increasing sequence number and it knows its the capacity of the\narray, so to find out where to write the next value by simply taking the modulus\nof the sequence number and the capacity. This has several advantages over\ntraditional queues:\n\n  1. We never remove elements when dequeing, merely overwrite them once we gone\n     all way around the ring. This removes write\n     [contention](https://en.wikipedia.org/wiki/Resource_contention) between the\n     producer and the consumer, one could also imagine avoiding garbage\n     collection by only allocating memory the first time around the ring (but we\n     don't do this in Haskell);\n\n  2. Using an array rather than linked list increasing\n     [striding](https://en.wikipedia.org/wiki/Stride_of_an_array) due to\n     [spatial\n     locality](https://en.wikipedia.org/wiki/Locality_of_reference#Spatial_and_temporal_locality_usage).\n\nThe ring buffer also keeps track of up to which sequence number its last\nconsumer has consumed, in order to not overwrite events that haven't been handled\nyet.\n\nThis also means that producers can ask how much capacity left a ring buffer has,\nand do batched writes. If there's no capacity left the producer can apply\nback-pressure upstream as appropriate.\n\nConsumers need keep track of which sequence number they have processed, in order\nto avoid having the ring buffer overwrite unprocessed events as already\nmentioned, but this also allows consumers to depend on each other.\n\nWhen a consumer is done processing an event, it asks the ring buffer for the\nevent at its next sequence number, the ring buffer then replies that either\nthere are no new events, in which case the consumer applies it wait strategy, or\nthe ring buffer can reply that there are new events, the consumer the handles\neach one in turn and the last one will be have the end of batch flag set, so\nthat the consumer can effectively batch the processing.\n\n### Performance\n\nOur Disruptor implementation, which hasn't been optimised much yet, is about 2x\nslower than LMAX's Java version on their single-producer single-consumer\n[benchmark](https://github.com/LMAX-Exchange/disruptor/blob/master/src/perftest/java/com/lmax/disruptor/sequenced/OneToOneSequencedThroughputTest.java)\n(1P1C) (basically the above example) on a couple of years old Linux laptop.\n\nThe same benchmark compared to other Haskell libraries:\n\n  * 10.3x faster than\n    [`Control.Concurrent.Chan`](https://hackage.haskell.org/package/base-4.15.0.0/docs/Control-Concurrent-Chan.html);\n\n  * 8.3x faster than\n    [`Control.Concurrent.STM.TBQueue`](https://hackage.haskell.org/package/stm/docs/Control-Concurrent-STM-TBQueue.html);\n\n  * 1.7x faster than\n    [`unagi-chan`](https://hackage.haskell.org/package/unagi-chan);\n\n  * 25.5x faster than\n    [`chaselev-deque`](https://hackage.haskell.org/package/chaselev-deque);\n\n  * 700x faster than [`ring-buffer`](https://hackage.haskell.org/package/ring-buffer);\n\n  * 1.3x slower than\n    [`lockfree-queue`](https://hackage.haskell.org/package/lockfree-queue);\n\n  * TODO: Compare with\n    [`data-ringbuffer`](https://github.com/kim/data-ringbuffer/tree/master/src/Data/RingBuffer).\n\nIn the triple-producer single-consumer (3P1C)\n[benchmark](https://github.com/LMAX-Exchange/disruptor/blob/master/src/perftest/java/com/lmax/disruptor/sequenced/ThreeToOneSequencedThroughputTest.java),\nthe Java version is 5x slower than the Java 1P1C case. And our 3P1C is 4.6x\nslower than our 1P1C version and our 3P1C version is 2.7x slower than the Java\nversion.\n\nThe same benchmark compared to other Haskell libraries:\n\n  * 73x faster than\n    [`Control.Concurrent.Chan`](https://hackage.haskell.org/package/base-4.15.0.0/docs/Control-Concurrent-Chan.html);\n\n  * 3.5x faster than\n    [`Control.Concurrent.STM.TBQueue`](https://hackage.haskell.org/package/stm/docs/Control-Concurrent-STM-TBQueue.html);\n\n  * 1.3x faster than\n    [`unagi-chan`](https://hackage.haskell.org/package/unagi-chan);\n\n  * 1.9x faster than\n    [`lockfree-queue`](https://hackage.haskell.org/package/lockfree-queue).\n\nFor a slightly more \"real world\" example, we modified the 3P1C test to have\nthree producers that log messages while the consumer writes them to a log file\nand compared it to\n[`fast-logger`](https://hackage.haskell.org/package/fast-logger). The\n`pipelined-state-machines` benchmark has a throughput of 3:4 that of\n`fast-logger`. When we bump it to ten concurrently logging threads the\n`pipelined-state-machines` benchmark has a throughput of 10:7 that of\n`fast-logger`.\n\nSee the file [`benchmark.sh`](benchmark.sh) for full details about how the\nbenchmarks are run.\n\nAs always take benchmarks with a grain of salt, we've tried to make them as fair\nwith respect to each other and as true to the original Java versions as\npossible. If you see anything that seems unfair, or if you get very different\nresults when trying to reproduce the numbers, then please file an issue.\n\n## Contributing\n\nThere's a lot of possible paths to explore from here, including:\n\n- [ ] Can we swap out our use of `TQueue` for `Disruptor` in our `deploy` of\n      `P`ipelines?\n- [ ] Can we add something like a `FanOut :: P a b -\u003e P a c -\u003e P a (b, c)` and a\n      `Par :: P a c -\u003e P b d -\u003e P (a, b) (c, d)` combinator to allow two\n      parallel queues?\n- [ ] What about sum-types and error handling?\n- [ ] Our current, and the above just mentioned, pipeline combinators are all\n      binary to can we generalise this to N-ary?\n- [ ] Can we visualise pipelines using `dot` or similar?\n- [ ] Can we build a performance/cost simulator of pipelines?\n- [ ] Arrow syntax or monadic DSL for pipelines?\n- [ ] We've seen\n      [previously](https://github.com/stevana/hot-swapping-state-machines) how\n      we can hot-code upgrade state machines, what about hot-code upgrading\n      pipelines?\n- [ ] Can we implement the Erlang `gen_event` behaviour using Disruptor?\n- [ ] Would it make sense to use the spiritual successor of the Disruptor\n      instead, i.e. the different array queues from `aeron` and `agrona`:\n  + [Single-producer\n    single-consumer](https://github.com/real-logic/agrona/blob/master/agrona/src/main/java/org/agrona/concurrent/OneToOneConcurrentArrayQueue.java);\n  + [Multiple-producers\n    single-consumer](https://github.com/real-logic/agrona/blob/master/agrona/src/main/java/org/agrona/concurrent/ManyToOneConcurrentArrayQueue.java);\n  + [Multiple-producers\n    multiple-consumers](https://github.com/real-logic/agrona/blob/master/agrona/src/main/java/org/agrona/concurrent/ManyToManyConcurrentArrayQueue.java).\n- [ ] How exactly do these pipelines relate to the libraries\n      [`pipes`](https://hackage.haskell.org/package/pipes),\n      [`conduit`](https://hackage.haskell.org/package/conduit) and\n      [`streamly`](https://hackage.haskell.org/package/streamly)?\n- [ ] How does it relate to synchronous programming languages such as\n      [Esterel](https://en.wikipedia.org/wiki/Esterel),\n      [Lustre](https://en.wikipedia.org/wiki/Lustre_(programming_language)),\n      [ReactiveML](https://rml.lri.fr), etc? It seems to me that their main\n      motivation is to be concurrent or parallel while still determinstic, which\n      is what we'd like as well. Looking at ReactiveML's documentation for\n      [compositions](https://rml.lri.fr/documentation.html#compositions) we see\n      the same constructs as we've discussed: their `;` is our `Compose` (with\n      its arguments flipped), their `||` is our `FanOut`, their `|\u003e` is our\n      `:\u003e\u003e\u003e` and their `let-and` construct could be achived by adding projection\n      functions to our `P`ipelines similar to `Fst` and `Snd` for `SM`.\n      Interestingly they don't have any sum-types-like construct here, i.e.\n      something like `(:|||) :: P a c -\u003e P b c -\u003e P (Either a b) c`;\n- [ ] I like to think of how one constructs a pipeline, i.e. the choice of which\n      tasks should happen in parallel or should be sharded etc, as a choice of\n      how to best make use of the CPUs/cores of a single computer. If seen this\n      way then that begs the question: what about a network of multiple\n      computers? Perhaps there should be something like a `Topology` data type\n      which describes how multiple pipelines interact and a topology is deployed\n      by deploying multiple pipelines over multiple machines?\n\n## See also\n\n### Presentations\n\n  * [LMAX - How to Do 100K TPS at Less than 1ms\n    Latency](https://www.infoq.com/presentations/LMAX/) by Martin Thompson (QCon\n    2010);\n\n  * [LMAX Disruptor and the Concepts of Mechanical\n    Sympathy](https://youtube.com/watch?v=Qho1QNbXBso) by Jamie Allen (2011);\n\n  * [Concurrent Programming with the\n    Disruptor](https://www.infoq.com/presentations/Concurrent-Programming-Using-The-Disruptor/)\n    by Trisha Gee (2012);\n\n  * [Disruptor 3.0: Details and Advanced\n    Patterns](https://youtube.com/watch?v=2Be_Lqa35Y0) by Mike Barker (YOW!\n    2013);\n\n  * [Designing for Performance](https://youtube.com/watch?v=fDGWWpHlzvw) by\n    Martin Thompson (GOTO 2015);\n\n  * [A quest for predictable latency with Java\n    concurrency](https://vimeo.com/181814364) Martin Thompson\n    (JavaZone 2016);\n\n  * [Evolution of Financial Exchange\n     Architectures](https://www.youtube.com/watch?v=qDhTjE0XmkE) by Martin\n     Thompson (QCon 2020)\n      + 1,000,000 tx/s and less than 100 microseconds latency, he is no longer\n        at LMAX though so we don't know if these exchanges are using the\n        disruptor pattern.\n\n  * [*Aeron: Open-source high-performance\n    messaging*](https://youtube.com/watch?v=tM4YskS94b0) talk by Martin Thompson\n    (Strange Loop, 2014);\n\n  * *Aeron: What, Why and What Next?*\n    [talk](https://youtube.com/watch?v=p1bsloPeBzE) by Todd Montgomery (GOTO,\n    2015);\n\n  * *Cluster Consensus: when Aeron met Raft*\n    [talk](https://youtube.com/watch?v=GFfLCGW_5-w) by Martin Thompson (GOTO,\n    2018);\n\n  * *Fault Tolerant 24/7 Operations with Aeron Cluster*\n    [talk](https://youtube.com/watch?v=H9yqzfNiEb4) by Todd Montgomery (2022).\n\n### Writings\n\n  * Martin Thompson's [blog](https://mechanical-sympathy.blogspot.com/);\n  * The Disruptor [mailing list](https://groups.google.com/g/lmax-disruptor);\n  * The Mechanical Sympathy [mailing list](https://groups.google.com/g/mechanical-sympathy);\n  * [The LMAX Architecture](https://martinfowler.com/articles/lmax.html) by\n    Martin Fowler (2011);\n  * [Staged event-driven\n    architecture](https://en.wikipedia.org/wiki/Staged_event-driven_architecture);\n  * [The Reactive Manifesto](https://www.reactivemanifesto.org/);\n  * [Flow-based programming](https://en.wikipedia.org/wiki/Flow-based_programming).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstevana%2Fpipelined-state-machines","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstevana%2Fpipelined-state-machines","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstevana%2Fpipelined-state-machines/lists"}