Skip to content

Commit

Permalink
Fix naming of initial and final algebras
Browse files Browse the repository at this point in the history
  • Loading branch information
turion committed Aug 7, 2024
1 parent ea05fe2 commit 997c1ae
Show file tree
Hide file tree
Showing 16 changed files with 187 additions and 182 deletions.
21 changes: 11 additions & 10 deletions automaton/README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# `automaton`: Effectful streams and automata in initial encoding
# `automaton`: Effectful streams and automata as coalgebras

This library defines effectful streams and automata, in initial encoding.
This library defines effectful streams and automata, in coalgebraic 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:
The core concept is an effectful stream in coalgebraic encoding:
```haskell
data StreamT m a = forall s.
StreamT
Expand All @@ -19,14 +19,15 @@ This is an stream because you can repeatedly call `step` on the `state` and prod
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":
The definitions you will most often find in the wild is a direct fixpoint, or recursive datatype:
```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.
Semantically, there is no big difference between them, and in nearly all cases you can map the coalgebraic encoding onto the recursive one and vice versa,
by means of the final coalgebra.
(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.
the coalgebraic encoding will often be more performant that than the recursive one because GHC can optimise the joint state and step functions of the streams.

### How are these automata?

Expand All @@ -42,9 +43,9 @@ 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:
When composing a big automaton out of small ones, the recursive definition 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.
In the coalgebraic 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?
Expand All @@ -61,9 +62,9 @@ Prominently, [`dunai`](https://hackage.haskell.org/package/dunai) implements mon
(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.
But all of these are implemented recursively.

I am aware of only two fleshed-out implementations of effectful automata in the initial encoding,
I am aware of only two fleshed-out implementations of effectful automata in the coalgebraic 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.
Expand Down
8 changes: 4 additions & 4 deletions automaton/automaton.cabal
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
cabal-version: 3.0
name: automaton
version: 1.4
synopsis: Effectful streams and automata in initial encoding
synopsis: Effectful streams and automata in coalgebraic encoding
description:
Effectful streams have an internal state and a step function.
Varying the effect type, this gives many different useful concepts:
Expand Down Expand Up @@ -64,7 +64,7 @@ library
import: opts
exposed-modules:
Data.Automaton
Data.Automaton.Final
Data.Automaton.Recursive
Data.Automaton.Trans.Accum
Data.Automaton.Trans.Except
Data.Automaton.Trans.Maybe
Expand All @@ -75,14 +75,14 @@ library
Data.Automaton.Trans.Writer
Data.Stream
Data.Stream.Except
Data.Stream.Final
Data.Stream.Internal
Data.Stream.Optimized
Data.Stream.Recursive
Data.Stream.Result

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

hs-source-dirs: src

Expand Down
2 changes: 1 addition & 1 deletion automaton/src/Data/Automaton.hs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ import Data.Stream.Result

-- * Constructing automata

{- | An effectful automaton in initial encoding.
{- | An effectful automaton in coalgebraic encoding.
* @m@: The monad in which the automaton performs side effects.
* @a@: The type of inputs the automaton constantly consumes.
Expand Down
36 changes: 0 additions & 36 deletions automaton/src/Data/Automaton/Final.hs

This file was deleted.

39 changes: 39 additions & 0 deletions automaton/src/Data/Automaton/Recursive.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}

module Data.Automaton.Recursive where

-- base
import Control.Applicative (Alternative)
import Control.Arrow
import Control.Category
import Prelude hiding (id, (.))

-- transformers
import Control.Monad.Trans.Reader

-- automaton
import Data.Automaton
import Data.Stream.Optimized qualified as StreamOptimized
import Data.Stream.Recursive qualified as StreamRecursive

{- | Automata in direct recursive encoding.
This type is isomorphic to @MSF@ from @dunai@.
-}
newtype Recursive m a b = Recursive {getRecursive :: StreamRecursive.Recursive (ReaderT a m) b}
deriving newtype (Functor, Applicative, Alternative)

instance (Monad m) => Category (Recursive m) where
id = toRecursive id
f1 . f2 = toRecursive $ fromRecursive f1 . fromRecursive f2

instance (Monad m) => Arrow (Recursive m) where
arr = toRecursive . arr
first = toRecursive . first . fromRecursive

toRecursive :: (Functor m) => Automaton m a b -> Recursive m a b
toRecursive (Automaton automaton) = Recursive $ StreamOptimized.toRecursive automaton

fromRecursive :: Recursive m a b -> Automaton m a b
fromRecursive Recursive {getRecursive} = Automaton $ StreamOptimized.fromRecursive getRecursive
4 changes: 2 additions & 2 deletions automaton/src/Data/Automaton/Trans/Except.hs
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ runAutomatonExcept = Automaton . hoist commuteReaderBack . runStreamExcept . get
Typically used to enter the monad context of 'AutomatonExcept'.
-}
try :: (Monad m) => Automaton (ExceptT e m) a b -> AutomatonExcept a b m e
try = AutomatonExcept . InitialExcept . hoist commuteReader . getAutomaton
try = AutomatonExcept . CoalgebraicExcept . hoist commuteReader . getAutomaton

{- | Immediately throw the current input as an exception.
Expand Down Expand Up @@ -259,7 +259,7 @@ safe = try . liftS
This passes the last input value to the action, but doesn't advance a tick.
-}
once :: (Monad m) => (a -> m e) -> AutomatonExcept a b m e
once f = AutomatonExcept $ InitialExcept $ StreamOptimized.constM $ ExceptT $ ReaderT $ fmap Left <$> f
once f = AutomatonExcept $ CoalgebraicExcept $ StreamOptimized.constM $ ExceptT $ ReaderT $ fmap Left <$> f

-- | Variant of 'once' without input.
once_ :: (Monad m) => m e -> AutomatonExcept a b m e
Expand Down
18 changes: 9 additions & 9 deletions automaton/src/Data/Stream.hs
Original file line number Diff line number Diff line change
Expand Up @@ -39,27 +39,27 @@ import Data.Stream.Result

-- * Creating streams

{- | Effectful streams in initial encoding.
{- | Effectful streams in coalgebraic encoding.
A stream consists of an internal state @s@, and a step function.
This step can make use of an effect in @m@ (which is often a monad),
alter the state, and return a result value.
Its semantics is continuously outputting values of type @b@,
while performing side effects in @m@.
An initial encoding was chosen instead of the final encoding known from e.g. @list-transformer@, @dunai@, @machines@, @streaming@, ...,
because the initial encoding is much more amenable to compiler optimizations
than the final encoding, which is:
A coalgebraic encoding was chosen instead of the direct recursion known from e.g. @list-transformer@, @dunai@, @machines@, @streaming@, ...,
because the coalgebraic encoding is much more amenable to compiler optimizations
than the coalgebraic encoding, which is:
@
data StreamFinalT m b = StreamFinalT (m (b, StreamFinalT m b))
data StreamRecursiveT m b = StreamRecursiveT (m (b, StreamRecursiveT m b))
@
When two streams are composed, GHC can often optimize the combined step function,
resulting in a faster streams than what the final encoding can ever achieve,
because the final encoding has to step through every continuation.
resulting in a faster streams than what the coalgebraic encoding can ever achieve,
because the coalgebraic encoding has to step through every continuation.
Put differently, the compiler can perform static analysis on the state types of initially encoded state machines,
while the final encoding knows its state only at runtime.
while the coalgebraic encoding knows its state only at runtime.
This performance gain comes at a peculiar cost:
Recursive definitions /of/ streams are not possible, e.g. an equation like:
Expand Down Expand Up @@ -411,7 +411,7 @@ fixStream' transformState transformStep =
{- | The solution to the equation @'fixA stream = stream <*> 'fixA' stream@.
Such a fix point operator needs to be used instead of the above direct definition because recursive definitions of streams
loop at runtime due to the initial encoding of the state.
loop at runtime due to the coalgebraic encoding of the state.
-}
fixA :: (Applicative m) => StreamT m (a -> a) -> StreamT m a
fixA StreamT {state, step} = fixStream (JointState state) $
Expand Down
37 changes: 19 additions & 18 deletions automaton/src/Data/Stream/Except.hs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ import Control.Monad.Morph (MFunctor, hoist)
import Control.Selective

-- automaton
import Data.Stream.Final (Final (..))
import Data.Stream.Final.Except

import Data.Stream.Optimized (OptimizedStreamT, applyExcept, constM, selectExcept)
import Data.Stream.Optimized qualified as StreamOptimized
import Data.Stream.Recursive (Recursive (..))
import Data.Stream.Recursive.Except

{- | A stream that can terminate with an exception.
Expand All @@ -29,42 +30,42 @@ In @automaton@, such streams mainly serve as a vehicle to bring control flow to
-}
data StreamExcept a m e
= -- | When using '>>=', this encoding will be used.
FinalExcept (Final (ExceptT e m) a)
RecursiveExcept (Recursive (ExceptT e m) a)
| -- | This is usually the faster encoding, as it can be optimized by GHC.
InitialExcept (OptimizedStreamT (ExceptT e m) a)
CoalgebraicExcept (OptimizedStreamT (ExceptT e m) a)

toFinal :: (Functor m) => StreamExcept a m e -> Final (ExceptT e m) a
toFinal (FinalExcept final) = final
toFinal (InitialExcept initial) = StreamOptimized.toFinal initial
toRecursive :: (Functor m) => StreamExcept a m e -> Recursive (ExceptT e m) a
toRecursive (RecursiveExcept coalgebraic) = coalgebraic
toRecursive (CoalgebraicExcept coalgebraic) = StreamOptimized.toRecursive coalgebraic

runStreamExcept :: StreamExcept a m e -> OptimizedStreamT (ExceptT e m) a
runStreamExcept (FinalExcept final) = StreamOptimized.fromFinal final
runStreamExcept (InitialExcept initial) = initial
runStreamExcept (RecursiveExcept coalgebraic) = StreamOptimized.fromRecursive coalgebraic
runStreamExcept (CoalgebraicExcept coalgebraic) = coalgebraic

instance (Monad m) => Functor (StreamExcept a m) where
fmap f (FinalExcept fe) = FinalExcept $ hoist (withExceptT f) fe
fmap f (InitialExcept ae) = InitialExcept $ hoist (withExceptT f) ae
fmap f (RecursiveExcept fe) = RecursiveExcept $ hoist (withExceptT f) fe
fmap f (CoalgebraicExcept ae) = CoalgebraicExcept $ hoist (withExceptT f) ae

instance (Monad m) => Applicative (StreamExcept a m) where
pure = InitialExcept . constM . throwE
InitialExcept f <*> InitialExcept a = InitialExcept $ applyExcept f a
pure = CoalgebraicExcept . constM . throwE
CoalgebraicExcept f <*> CoalgebraicExcept a = CoalgebraicExcept $ applyExcept f a
f <*> a = ap f a

instance (Monad m) => Selective (StreamExcept a m) where
select (InitialExcept e) (InitialExcept f) = InitialExcept $ selectExcept e f
select (CoalgebraicExcept e) (CoalgebraicExcept f) = CoalgebraicExcept $ selectExcept e f
select e f = selectM e f

-- | 'return'/'pure' throw exceptions, '(>>=)' uses the last thrown exception as input for an exception handler.
instance (Monad m) => Monad (StreamExcept a m) where
(>>) = (*>)
ae >>= f = FinalExcept $ handleExceptT (toFinal ae) (toFinal . f)
ae >>= f = RecursiveExcept $ handleExceptT (toRecursive ae) (toRecursive . f)

instance MonadTrans (StreamExcept a) where
lift = InitialExcept . constM . ExceptT . fmap Left
lift = CoalgebraicExcept . constM . ExceptT . fmap Left

instance MFunctor (StreamExcept a) where
hoist morph (InitialExcept automaton) = InitialExcept $ hoist (mapExceptT morph) automaton
hoist morph (FinalExcept final) = FinalExcept $ hoist (mapExceptT morph) final
hoist morph (RecursiveExcept automaton) = RecursiveExcept $ hoist (mapExceptT morph) automaton
hoist morph (CoalgebraicExcept coalgebraic) = CoalgebraicExcept $ hoist (mapExceptT morph) coalgebraic

safely :: (Monad m) => StreamExcept a m Void -> OptimizedStreamT m a
safely = hoist (fmap (either absurd id) . runExceptT) . runStreamExcept
63 changes: 0 additions & 63 deletions automaton/src/Data/Stream/Final.hs

This file was deleted.

18 changes: 0 additions & 18 deletions automaton/src/Data/Stream/Final/Except.hs

This file was deleted.

Loading

0 comments on commit 997c1ae

Please sign in to comment.