Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace dunai by automata/state machines #299

Merged
merged 10 commits into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CHEATSHEET.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ rhL -- A rhine that inputs some data `a` and outputs some data `b`, on some c

### Clocked signal functions (`ClSF`s)

Stream functions in [`dunai`](http://hackage.haskell.org/package/dunai) are usually valid clocked signal functions.
Here are some that are not in `dunai`.
Automata in [`automaton`](http://hackage.haskell.org/package/automaton) are usually valid clocked signal functions.
Here are some of the most used:

| Name | Type (abbreviated) | Meaning |
|--------------|------------------------------------------------------|---------------------------------------------------|
Expand Down
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@ Rhine is a library for synchronous and asynchronous Functional Reactive Programm
It separates the aspects of clocking, scheduling and resampling
from each other, and ensures clock-safety on the type level.

## Versions 1.* vs. 0.*
## Recent breakage?

Confused because some examples from the article don't work anymore?
As a big simplification and breaking change,
explicit schedules were removed in version 1.0.
For an overview of the required changes, see [this page](/version1.md).
Rhine went through a few bigger API simplifications and changes.
If this broke your code, have a look at [the versions readme](./versions.md) to fix it.

## Concept

Expand Down
5 changes: 5 additions & 0 deletions automaton/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Revision history for automaton

## 0.1.0.0

* Initial version ;)
20 changes: 20 additions & 0 deletions automaton/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Copyright (c) 2024 Manuel Bärenz

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
70 changes: 70 additions & 0 deletions automaton/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# `automaton`: Effectful streams and automata in initial encoding

This library defines effectful streams and automata, in initial encoding.
They are useful to define effectful automata, or state machines, transducers, monadic stream functions and similar streaming abstractions.
In comparison to most other libraries, they are implemented here with explicit state types,
and thus are amenable to GHC optimizations, often resulting in dramatically better performance.

## What?

The core concept is an effectful stream in initial encoding:
```haskell
data StreamT m a = forall s.
StreamT
{ state :: s
, step :: s -> m (s, a)
}
```
This is an stream because you can repeatedly call `step` on the `state` and produce output values `a`,
while mutating the internal state.
It is effectful because each step performs a side effect in `m`, typically a monad.

The definitions you will most often find in the wild is the "final encoding":
```haskell
data StreamT m a = StreamT (m (StreamT m a, a))
```
Semantically, there is no big difference between them, and in nearly all cases you can map the initial encoding onto the final one and vice versa.
(For the single edge case, see [the section in `Data.Automaton` about recursive definitions](hackage.haskell.org/package/automaton/docs/Data.Automaton.html).)
But when composing streams,
the initial encoding will often be more performant that than the final encoding because GHC can optimise the joint state and step functions of the streams.

### How are these automata?

Effectful streams are very versatile, because you can change the effect type `m` to get a number of different concepts.
When `m` contains a `Reader` effect, you get automata!
From the effectful stream alone, a side effect, a state transition and an output value is produced at every step.
If this effect includes reading an input value, you have all ingredients for an automaton (also known as a Mealy state machine, or a transducer).

Automata can be composed in many useful ways, and are very expressive.
A lot of reactive programs can be written with them,
by composing a big program out of many automaton components.

## Why?

Mostly, performance.
When composing a big automaton out of small ones, the final encoding is not very performant, as mentioned above:
Each step of each component contains a closure, which is basically opaque for the compiler.
In the initial encoding, the step functions of two composed automata are themselves composed, and the compiler can optimize them just like any regular function.
This often results in massive speedups.

### But really, why?

To serve as the basic building block in [`rhine`](https://hackage.haskell.org/package/rhine),
a library for Functional Reactive Programming.

## Doesn't this exist already?

Not quite.
There are many streaming libraries ([`streamt`](https://hackage.haskell.org/package/streamt), [`streaming`](https://hackage.haskell.org/package/streaming)),
and state machine libraries ([`machines`](https://hackage.haskell.org/package/machines)) that implement effectful streams.
Prominently, [`dunai`](https://hackage.haskell.org/package/dunai) implements monadic stream functions
(which are essentially effectful state machines)
and has inspired the design and API of this package to a great extent.
(Feel free to extend this list by other notable libraries.)
But all of these are implemented in the final encoding.

I am aware of only two fleshed-out implementations of effectful automata in the initial encoding,
both of which have been a big inspiration for this package:

* [`essence-of-live-coding`](https://hackage.haskell.org/package/essence-of-live-coding) restricts the state type to be serializable, gaining live coding capabilities, but sacrificing on expressivity.
* https://github.com/lexi-lambda/incremental/blob/master/src/Incremental/Fast.hs is unfortunately not published on Hackage, and doesn't seem maintained.
99 changes: 99 additions & 0 deletions automaton/automaton.cabal
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
cabal-version: 3.0
name: automaton
version: 0.1.0.0
synopsis: Effectful streams and automata in initial encoding
description:
Effectful streams have an internal state and a step function.
Varying the effect type, this gives many different useful concepts:
For example with a reader effect, it results in automata/transducers/state machines.

license: MIT
license-file: LICENSE
author: Manuel Bärenz
maintainer: [email protected]
category: Streaming
build-type: Simple
extra-doc-files:
CHANGELOG.md
README.md

common opts
build-depends:
MonadRandom >=0.5,
base >=4.14 && <4.18,
mmorph ^>=1.2,
mtl >=2.2 && <2.4,
profunctors ^>=5.6,
selective ^>=0.7,
semialign >=1.2 && <=1.4,
simple-affine-space ^>=0.2,
these >=1.1 && <=1.3,
transformers >=0.5,

if flag(dev)
ghc-options: -Werror
ghc-options:
-W

default-extensions:
Arrows
DataKinds
FlexibleContexts
FlexibleInstances
ImportQualifiedPost
MultiParamTypeClasses
NamedFieldPuns
NoStarIsType
TupleSections
TypeApplications
TypeFamilies
TypeOperators

default-language: Haskell2010

library
import: opts
exposed-modules:
Data.Automaton
Data.Automaton.Final
Data.Automaton.Trans.Except
Data.Automaton.Trans.Maybe
Data.Automaton.Trans.RWS
Data.Automaton.Trans.Random
Data.Automaton.Trans.Reader
Data.Automaton.Trans.State
Data.Automaton.Trans.Writer
Data.Stream
Data.Stream.Except
Data.Stream.Final
Data.Stream.Internal
Data.Stream.Optimized
Data.Stream.Result

other-modules:
Data.Automaton.Trans.Except.Internal
Data.Stream.Final.Except

hs-source-dirs: src

test-suite automaton-test
import: opts
type: exitcode-stdio-1.0
hs-source-dirs: test
main-is: Main.hs
other-modules:
Automaton
Automaton.Except
Stream

build-depends:
QuickCheck ^>=2.14,
automaton,
tasty ^>=1.4,
tasty-hunit ^>=0.10,
tasty-quickcheck ^>=0.10,

flag dev
description: Enable warnings as errors. Active on ci.
default: False
manual: True
Loading
Loading