Welcome to the tutorial for getting started with Auto!
This is actually just a basic overview of the library and some basic programs, enough to get started, hopefully; for more, check out the All About Auto series on my blog where I break down real world projects and auto-examples for more real-world examples, Up-to-date documentation is, at the moment, hosted on github...and the latest version of this tutorial itself can be found on the development branch, normally!
Before we start, let's remember our imports!
import Control.Auto -- the main entry point import Prelude hiding ((.), id) -- we use generalized versions from -- Control.Category, so we have to hide -- these.
Auto describes a relationship between an input and an
output that is preserved over multiple steps.
In a way, you can think about
Autos as value stream transformers. A
stream of sequential input values come in one at a time, and a stream of
output values pop out one at a time as well. You can think of
Auto' a b and "unwrapping" its internal
[a] -> [b].
(We say a "value stream" to contrast from an "effect stream", a stream from an effectful process like IO)
Auto is a relationship; the simplest relationship is probably a straight
up apply-a-function-to-each-input-to-get-each-output relationship. For that,
check out the
arr (*2), where the outputs are the doubles of the
-- streamAuto' :: Auto' a b -> [a] -> [b] -- [ 1, 2, 3, 4, 5, 6, 7, 8, 9,10... -- the inputs ghci> take 10 $ streamAuto' (arr (*2)) [1..] [ 2, 4, 6, 8,10,12,14,16,18,20] -- the outputs
streamAuto' (arr f) is just
map f, as you can see!
In general, the input-output relationship is allowed to depend on the history
of the inputs, as well. For example, we have the
sumFrom 0 --- the
relationship is that the output is always the cumulative sum of the inputs
received so far:
-- [ 1, 2, 3, 4, 5, 6, 7, 8, 9,10... -- the inputs ghci> take 10 $ streamAuto' (sumFrom 0) [1..] [ 1, 3, 6,10,15,21,28,36,45,55] -- the outputs
A bit on types ---
sumFrom n is
Num a => Auto m a a ... or, if
specializing it helps,
Auto' Int Int. You can read this as "a relationship
Ints fixed over the stream", or "a one-by-one mapping of an
Int stream to another
Int stream". For
sumFrom n, the relationship is
that the output is always the cumulative sum of the inputs.
Note that these relationships are always causal; the nth item of the output can only depend on the first n items of the input. We say that they are "real-time" stream transformers in that every time you get an input, exactly one output pops out.
That's what they are semantically, and an
Auto denotes exactly such an
input-output relationship that is maintained over several steps.
Auto does this by acting as a "stateful function" that we
can "run" with
stepAuto. A function with "internal state".
-- stepAuto' :: Auto' a b -> a -> (b, Auto' a b) ghci> let (x, nextAuto ) = stepAuto' (sumFrom 0) 5 ghci> x 5 ghci> let (y, nextAuto2) = stepAuto' nextAuto 3 ghci> y 8 ghci> evalAuto' nextAuto2 4 12
stepAuto' lets you take an
Auto' a b, give it an
a as an input, and
b as the output, and a "next/updated
Auto'", which is the
Auto' with an updated internal state. Running the "next
Auto'" given will
continue along with the new updated state. (
evalAuto' is like
but throws away the "next
Auto") In this case, the "internal state" is an
accumulator, the sum of all received elements so far.
In practice, this is usually going to be your "main loop", or "game loop":
- Collect input from the world (using IO, or whatever you need)
- Step the
Autoyou have with that input.
- Get the output from that
Auto, and the next
- Show or render your output to the world however you want.
- Repeat all again, but with the new
Autofrom step 3.
(If your program doesn't need any outside input, then you can just use
streamAuto' with an infinite list.)
There are some built-in "loops" like this in the Control.Auto.Run
module, for running in
IO by reading and showing inputs and ouputs
interactRS) if you want to try these out!
What's in a type?
Enough handwaving! What do these types even mean? What are the type parameters?
Auto' a b describes a relationship between a stream of inputs
a and a
stream of outputs
b that is maintained over several steps of inputs.
One way to look at it is that, with
Auto' a b gives you the
[a] -> [b].
From an operational perspective, you can think of an
Auto' a b as a function
with internal state that, when fed an
a, gives you a
b and a "next/updated
Auto' a b gives you an
a -> (b, Auto' a b).
Auto' a b is basically a
a -> b with "internal state".
The more general type is actually
Auto m a b --- an
Auto' a b is actually
just a type alias for
Auto Identity a b.
Auto m a b describes a relationship, again, between a stream of inputs
a and a stream of outputs
b maintained over several steps of inputs...and
maintains this relationship with within an underlying monadic context
streamAuto' from an
Auto' a b gives you an
[a] -> [b], then
streamAuto from an
Auto m a b gives you the "unwrapped"
[a] -> m [b].
Auto' a b is a
a -> b with internal state, then
Auto m a b is a
a -> m b with internal state. If you feed it an
a, it'll return
b and a "next/updated
Auto" in a monadic context --- with
you get a
a -> m (b, Auto m a b).
This monadic context means that in the process of "stepping" or "running" the
Auto, you can perform effects and get input from an outside world.
For the most part, real-life
Autos will be written parameterized over
Monad or some
myAuto :: Monad m => Auto m Int Bool
Monad m => Auto m a b is practically identical to working with
Auto' a b, so there really isn't ever a real point to actually write an
Auto'. However, specializing to
Auto' lets us use simple "running"
While we're on the subject, there is another type alias for
Interval m a b is an
Auto m a (Maybe b) (they're just type aliases).
Semantically, it represents an
Auto that is "on" or "off" for durations of
Interval' a b is an
Auto' a (Maybe b). You get the
picture, I hope! We'll learn more about
Building up Autos
So of course, having simple
Autos like this being your whole program isn't
very reasonable...do you think I have a
the library? :)
The "magic" of this library is that you have the ability to build up complex
and intricate relationships and behaviors (and programs) by composing small
Autos. These combinators are exposed both through familiar
typeclasses we know and love, and also through functions in this library.
Modifying and combining
For example, with the
Functor instance, you can apply functions to the
"output" of an
ghci> streamAuto' (sumFrom 0) [1..5] [ 1 , 3 , 6 , 10 , 15 ] ghci> streamAuto' (show <$> sumFrom 0) [1..5] ["1","3","6","10","15"]
Profunctor lets you apply functions "before" the input of the
-- mappender :: Monoid m => Auto' m m ghci> streamAuto' mappender ["1","2","3"] ["1","12","123"] ghci> streamAuto' (lmap show mappender) [1,2,3] ["1","12","123"]
mappender is an
Auto where the output is always the cumulative
of all of the inputs so far)
Applicative instance gives you a "constant
Auto", which ignores its
input and whose output is always a constant value:
ghci> take 10 $ streamAuto' (pure 4) [1..] [4, 4, 4, 4, 4, 4, 4, 4, 4, 4]
Applicative instance also gives you the ability to "fork" the input
streams of two
Autos and then re-combine their output streams later:
ghci> streamAuto' (sumFrom 0) [1..5] [ 1, 3, 6, 10, 15] ghci> streamAuto' (productFrom 1) [1..5] [ 1, 2, 6, 24, 120] ghci> streamAuto' (liftA2 (+) (sumFrom 0) (productFrom 1)) [1..5] [ 2, 5, 12, 34, 135]
You can also "fork" an input stream to two
Autos, and then throw away the
output stream of one: (very useful for
effect, which we will
see later, where we only care about the monadic effects and not about the
actual output stream)
ghci> streamAuto' (sumFrom 0 *> productFrom 1) [1..5] [ 1, 2, 6, 24, 120] ghci> streamAuto' (sumFrom 0 <* productFrom 1) [1..5] [ 1, 3, 6, 10, 15]
Heck, you can even
sequenceA :: [Auto m a b] -> Auto m a [b]
It will take a list of
Autos and return an
Auto that "forks" the input
stream into all of the original
Autos and aggregates together all of the
output streams. A multi-way fork.
We also have the Applicative-derived instances like
Monoid, so any
Auto m a b is a
b is a
mconcat :: Monoid b => [Auto m a b] -> Auto m a b
A lot of times you'll have a lot of things handling the same input in
different ways, and you'll want to recombine them all at the end. Well,
sequence, etc. are at your service!
This is the principle of "scalable program architectures" at work!
mappend of two
Of course there the Applicative-derived
Num (and assorted numerical
ghci> streamAuto' (0 * sumFrom 0) [1..5] [0, 0, 0, 0, 0,] ghci> streamAuto' (negate (sumFrom 0)) [1..5] [-1, -3, -6, -10, -15] ghci> streamAuto' (10 + sumFrom 0) [1..5] [11, 13, 16, 20, 25] ghci> streamAuto' (sumFrom 0 + productFrom 1) [1..5] [ 2, 5, 12, 34, 135]
Just don't go too crazy with these, okay?
Category instance is probably the most powerful tool at your
disposal. As a first treat, it gives you
id :: Auto m a a, an
output is always exactly the corresponding input.
But more importantly, you can "chain together"
Autos end-to-end. Compose
them as if they were functions.
You know how an
Auto takes a stream and outputs a stream? Well,
Autos will "pipe together" the streams.
a2 . a1 will be a new
Auto that runs an input stream through both
ghci> streamAuto' (sumFrom 0) [1..5] [1,3,6,10,15] ghci> streamAuto' (productFrom 1) [1,3,6,10,15] [1,3,18,180,2700] ghci> streamAuto' (productFrom 1 . sumFrom 0) [1..5] [1,3,18,180,2700]
sumFrom 0's output stream is the cumulative sum of the input stream.
productFrom 1's output stream is the cumulative product of the input stream.
So their chaining/piping/composition is the cumulative product of the
(.) :: Auto m b c -> Auto m a b -> Auto m a c
If you imagine an
Auto' as an
[a] -> [b], then you can think of this as
[a] -> [b] functions:
-- streamAuto' gives us an [Int] -> [Int], so we can compose them using normal -- function composition: ghci> streamAuto' (productFrom 1) . streamAuto' (sumFrom 0) $ [1..5] [1,3,18,180,2700] -- composing `Auto`s is like composing their resulting `[a] -> [b]`s ghci> streamAuto' (productFrom 1 . sumFrom 0) $ [1..5] [1,3,18,180,2700]
(Math nuts might recognize this as saying that
streamAuto' is a "category
homomorphism"...aka, a functor :) Seeing that
streamAuto' (id :: Auto' a a) == (id :: [a] -> [a]), of course!)
Operationally, at every "step", it passes in each input to the first
and gets the output of that and passes it into the second
Auto, and uses the
output of the second
Auto as the result, updating each internal state.
Another example, here we have an
Auto that takes an input stream and
Blip stream (more on that later) that emits whenever there is a
multiple of 5:
-- emitOn5s :: Auto' Int (Blip Int) ghci> let emitOn5s = emitOn (\x -> x `mod` 5 == 0) ghci> streamAuto' emitOn5s [1,5,9,3,10,2] [NoBlip, Blip 5, NoBlip, NoBlip, Blip 10, NoBlip] ghci> streamAuto' (hold . emitOn5s) [1,5,9,3,10,2] [Nothing, Just 5, Just 5, Just 5, Just 10, Just 10]
hold :: Auto' (Blip a) (Maybe a) takes a stream of
Blips and returns a
stream that is
Maybe a, where it is
Nothing until the first emitted
Just x as the last received
So here, we "chain"
emitOn5s emits on everything
that is a multiple of
hold "holds on" to all of the emitted values.
This can be used in conjunction with the
Applicative instance for great
power. In the end, your programs will really just be
with forks and re-combinings from
Arrow, we also have a neat interface exposed by
ArrowLoop. First of all, we get
arr :: (a -> b) -> Auto m a b, which basically an
Auto that is a constant, pure function (the output
is the corresponding input applied to the given function). We get the ability
to make an
Auto run on "only the first item in a tuple" (
first), or "only
Lefts that come in" (
left). Also, we get proc notation!
foo :: Auto' Int (Int, Maybe Int) foo = proc x -> do sumX <- sumFrom 0 -< x prodX <- productFrom 1 -< x + sumX lastEven <- hold . emitOn even -< x id -< (prodX, lastEven)
ghci> streamAuto' foo [4,7,3,6,5,1] [ ( 8, Just 4), ( 144, Just 4), ( 2448, Just 4) , (63648, Just 6), (1909440, Just 6), (51554880, Just 6) ]
Most of what was just done could be written with the
instance as well...but in this way, the entire thing looks a lot like a
dependency graph, and it's pretty expressive and powerful.
Brief Primer on Proc Notation
An explanation on the syntax; when you see:
sumX <- sumFrom 0 -< x
This reads as you are defining a binding
sumX, and the relationship between
sumX and x is that
sumX is the cumulative sum of
(from the first line,
foo = proc x -> do,
x is the input of the entire
When we see:
prodX <- productFrom 1 -< x + sumX
This reads as you are defining a binding
prodX is maintained as
the cumulative product of
x + sumX.
All "values" in your proc block are actually streams.
prodX is a stream
x is a stream,
sumX is a stream...and
productFrom 1 lets you
describe the (static) relationship between those three streams.
The result of the last line of the proc block is the result of the entire block:
id -< (prodX, lastEven)
Means that the output stream of the entire block is just echoing the tuple
(Operationally, you can imagine that, at every step,
x is "fed into"
sumFrom 0, and the result is named
x + sumX is "fed into"
productFrom 1, etc.)
The power here is that it really reads like a straight-up dependency graph...a
graph of relationships to names. Lay out your relationships explicitly and
declaratively, and the library takes care of the rest! The semantic model of
Auto representing a maintained relationship is made very clear in
Later on you can see that
proc blocks can be pretty expressive --- using
if/then's and case statements, and also recursive bindings (so you can even
declare recursive graphs of concepts, and the library will figure out how to
solve it for you).
By the way, there are some "scoping" issues to be aware of. Remember that
proc more or less builds a graph of relationships between values using
at compile-time; the whole graph and chaining-together-of-
Autos is done at
compile time. So, the
Autos themselves have to be known at compile time.
We can't do something like this:
foo :: Auto' Int Int foo = proc x -> do y <- productFrom 1 -< x z <- sumFrom y -< x id -< y + z
We can't do
sumFrom y, because
y isn't actually a "value" we have at
y is the stream that is the cumulative product
of the stream
y changes at every "point in time". Remember,
relationships in a proc block are "fixed", and "forever";
productFrom 1 is
the "static relationship" between
y. So the
is...it has to be a fixed thing that never changes. But
y changes every
You can however do something like:
bar :: Int -> Auto' Int Int bar x0 = proc x -> do y <- productFrom 1 -< x z <- sumFrom x0 -< x id -< y + z
Because when we are "building"
bar x0, we have
x0! It'll be
sumFrom x0, forever!
Anyways! Those are the primary typeclass based interfaces; explore the library for more!
If you have to, when creating
Autos from scratch, we have:
pure :: b -> Auto m a b effect :: m b -> Auto m a b arr :: (a -> b) -> Auto m a b arrM :: (a -> m b) -> Auto m a b
effect give you "constant-producing
Auto"s that ignore their
pure x is an
Auto that ignores its input and always outputs
effect m is an
Auto that ignores its input and executes/sequences
every "step", and outputs the result at every step.
arr is an
maps every input to an output by running a pure function, and
arrM is an
Auto that does the same but with a "monadic" function.
Here is a handy little summary!
streamAuto' (pure x) == map (const x) streamAuto (effect m) == mapM (const m) streamAuto' (arr f) == map f streamauto (arrM f) == mapM f
None of these
Autos have "internal state"; however, we can make our own
Autos from scratch:
iterator :: (b -> b) -> b -> Auto m a b iteratorM :: (b -> m b) -> b -> Auto m a b accum :: (b -> a -> b) -> b -> Auto m a b accumM :: (b -> a -> m b) -> b -> Auto m a b mkState :: (a -> s -> (b, s)) -> s -> Auto m a b mkStateM :: (a -> s -> m (b, s)) -> s -> Auto m a b mkAuto_ :: (a -> (b, Auto m a b)) -> Auto m a b mkAutoM_ :: (a -> m (b, Auto m a b)) -> Auto m a b
You can look at the documentation for all of these, but these all basically
work with "internal state" ---
iterator ignores its input and repeatedly
applies a function to a value and pops it out at every step.
maintains that the output is always the result of "folding together" (a la
foldl) all of the inputs so far, with a starting value.
like a more powerful
accum, which keeps an internal state that is updated
at every step.
mkAuto_ lets you describe an
Auto by its behavior under
ghci> take 10 $ streamAuto' (iterator (+1) 0) (repeat ()) [0,1,2,3,4,5,6,7,8,9] ghci> take 10 $ streamAuto' (accum (+) 0) [1..] [1,3,6,10,15,21,28,36,45,55]
It is recommended to only use
mkAuto only when
absolutely necessary; usually you can make what you want from combining
smaller, simple, pre-made
Autos. But sometimes the case does arrive.
The Big Picture
So, at this point, let's look at the "big picture". A program written with
Auto will involve, at every "step", gathering input, feeding into the
Auto", getting the output, rendering it somehow, and
repeating. But how do we build our
Auto? What is the advantage of using
Auto instead of
Auto lets you compose little meaning-bits into more complex meaning bits, by
specifying invariant relationships between items of streams. These are
"forever-relationships" --- they don't just describe step-by-step, iterative,
stateful actions --- they describe invariant relationships. And you can
create your own by composing, modifying, chaining, etc. all of the primitives.
Building a program in
Auto is basically specifying relationships that are
maintained "forever"...and thinking about your program in that manner.
sumAndProd :: Auto' Int Int sumAndProd = proc x -> do sumX <- sumFrom 0 -< x prodX <- productFrom 1 -< x id -< sumX + prodX -- sumAndProd = liftA2 (+) (sumFrom 0) (productFrom 1)
sumX is a "forever" quantity...and so is
x. We say that the relationship
x is that
sumX is the cumulative sum (
sumFrom 0) of
x. The relationship between
x is that
prodX is the
cumulative product...and the relationship between
x and the output is that
the output is the sum of
prodX at every point in time.
Operationally, you also have a huge advantage here over using something like
State in that each
Auto really contains its own "internal state" that is
inaccessible by the world. For example, in that last example,
works by maintaining its own internal state.
productFrom 1 also maintains
its own internal state.
Nobody can ever "touch" or "inspect" the internal state of
sumFrom 0 and
prudctFrom 1. It maintains it on its own. This is in big contrast to
State-based solutions, which necessarily work on "global state", and
managing global vs. local state with monad morphisms.
Note that this "composes"; we can use
sumAndProd in another
foo :: Auto' Int String foo = proc x -> do sp <- sumAndProd -< x y <- blah blah -< sp + x id -< show y
sumAndProd now is its own "internally stateful" thing...you can take it
and pop it onto any other chain. In
State, you'd open yourself up to having
to create new sum types for extra state...whenever you combined any two
stateful operations on different states.
This locally stateful property truly allows us to "compose" ideas together and
relationships together and think of them as fixed invariants in a big picture.
Auto "denotes" a relationship, and we build up bigger
by combining small denotative primitives to create bigger things that denote
more complex relationships, it really allows us to create a denotative
"language", where we declare relationships by building up smaller units of
meaning into bigger units of meaning.
Now...how do we actually implement the behavior that we want? This is a job
for the primitive
Autos, but also really much a big job for ... the semantic
tools that come with the library!
Auto represents a relationship between an input stream and an output
stream, but in order to build more expressive programs, this library also
comes with more semantic tools to work with in characterizing your streams
with "meaning", and tools to manipulate them and compose them in powerful ways
(within this framework of meaning) to express your programs.
The two main ones are
We say that, in the context of inputs/outputs of
Blip a represents
a "blip stream" that occasionally, in isolated incidents, emits a value of
Auto' a (Blip b) is an
Auto' that a stream of
a's as input
and outputs a blip stream that occasionally emits with a
Auto' (Blip a) b is an
Auto' that takes a blip stream that occasionally emits
a and outputs a stream of
Auto takes or outputs a "blip stream", it comes with some "semantic"
contracts on to how the stream behaves. The main contract is that your
stream should only output on (meaningfully) "isolated" incidents, and never on
continuous regions of the input stream.
This isn't enforced by the type system, but almost all of the
in this library will preserve this property! And we encourage any that you
make to also preserve this property, in order to make "blip streams" useful
in the first place.
We saw an example earlier,
ghci> let emitOn5s = emitOn (\x -> x `mod` 5 == 0) ghci> streamAuto' emitOn5s [1,5,9,3,10,2] [NoBlip, Blip 5, NoBlip, NoBlip, Blip 10, NoBlip]
Let's see if we can play around with it! Well, we can "tag" blip emissions:
ghci> streamAuto' (tagBlips "hey" . emitOn5s) [1,5,9,3,10,2] [NoBlip, Blip "hey", NoBlip, NoBlip, Blip "hey", NoBlip]
And with proc blocks, we can even "name" blip streams and manipulate them as
streams! Oh, also,
Blip is a
Functor, so you can use
blippy :: Monad m => Auto m Int String blippy = proc x -> do on3s <- tagBlips "3!" . emitOn3s -< x on5s' <- emitOn5s -< x let on5s = "5!" <$ on5s -- from Data.Functor: replace all emitted -- values with the string "5!" on35s = on3s `mergeL` on5s -- merge the streams, favoring the left intro <- immediately -< "hello!" middle <- inB 6 -< "#6!" wut <- never -< "this should never happen!" id -< mergeLs [never, intro, middle, on35s] -- merge all, favoring firsts
ghci> streamAuto' blippy [5,7,15,10,13,15,2] [Blip "hello!", NoBlip, Blip "3!", Blip "5!", NoBlip, Blip "#6!", NoBlip] -- ^ intro ^ on3s ^ on5s ^ middle
Blip streams and "blip contracts"/"blip semantics" are useful because a lot of
the other semantic abstractions in
Auto (like switches, and
work with the "idea" of a "discrete", occasional, conceptually
"non-contiguous" blip stream.
Check out all of the built-in blip stream combinators at Control.Auto.Blip.
The "opposite" of
Blip and blip streams are "intervaled"
that are "on" or "off" for (conceptually) contiguous chunks of steps.
Interval' a b represents an
Auto that takes a stream of
as as input,
and outputs a stream of
bs that is "on" or "off", at contiguous swaths.
Interval' a b is just a type synonym for
Auto' a (Maybe b), and
Interval m a b is just a type synonym for
Auto m a (Maybe b). But, if you
see a library auto with type
Interval, or if you make an auto with type
Interval, it comes with "contracts". These contracts help us really use
Intervals in a meaningful way --- that they are supposed to represent
Autos that output things that are "on" or "off" for contiguous steps.
Blips are "blippy",
Intervals are "chunky".
We've already seen an
ghci> streamAuto' (hold . emitOn5s) [1,5,9,3,10,2] [Nothing, Just 5, Just 5, Just 5, Just 10, Just 10]
hold :: Interval' (Blip a) a, so it turns a blip stream into a stream of
as that are on and off. In this case, it starts off "off", and is "on"
after the first emitted value, with the last emitted value.
Intervals are nice because you can have "choices" between two "on-off"
ghci> let a1 = (onFor 3 . arr (+ 100)) <|!> whenI (> 6) <|!> arr (+ 200) ghci> take 10 $ streamAuto' a1 [1..] [101, 102, 203, 204, 205, 206, 7, 8, 9, 10] ghci> let a2 = chooseInterval [offFor 8, onFor 3 . arr (+ 100)] ghci> take 10 $ streamAuto' a2 [1..] [Just 101, Just 102, Just 103, Nothing, Nothing, Just 6, Just 7]
<|!>) forks the input into both
Intervals, and the outputted one is the
first one that is "on". You can chain them as long as the "final"
Auto, and not an
(<|!>) :: Interval m a b -> Auto m a b -> Auto m a b
onFor n lets the input pass for
whenI lets the input "pass
through" when the predicate is true (being sure to pick a meaningful predicate
based on the expected input for "chunky" output)
You can also "chain"
ghci> streamAuto' (whenI (< 3) `compI` whenI (> 6)) [1..8] [Just 1, Just 2, Nothing, Nothing, Nothing, Just 6, Just 7, Just 8] ghci> streamAuto' (bindI (whenI (< 3)) . whenI (> 6)) [1..8] [Just 1, Just 2, Nothing, Nothing, Nothing, Just 6, Just 7, Just 8]
Intervals are also used for things that want their
Autos to "signal" when
they are "off".
Interval is the universal language for, "you can be done
with me", when it is needed. For example, the
interactAuto loop takes an
Interval String String, and "turns off" on the first
Nothing or "off"
ghci> interactAuto (onFor 4 . arr (++ "!!!")) > hello hello!!! > how how!!! > are are!!! > you you!!! > today --- (end of output)
Like with blip streams, intervals are used to great effect with switches, like
ghci> let a1 = whileI (<= 4) --> pure 0 ghci> streamAuto' a1 [1..10] [1, 2, 3, 4, 0, 0, 0, 0, 0, 0] -- look, recursion! ghci> let a2 = (onFor 3 . pure "hi") --> (onFor 2 . pure "bye") --> a2 ghci> take 10 $ streamAuto' a2 (repeat ()) ["hi", "hi", "hi", "bye", "bye", "hi", "hi", "hi", "bye", "bye"]
You can see all of the built-in
Interval combinators in
A powerful grab-bag of tools that can be used with intervals and blip streams
is the idea of "switching", as mentioned earlier.
Autos that behave like
Auto for a while, and then another afterwards.
switchOnF lets you have an
Auto that behaves
Auto, until the blip stream it is receiving emits something ---
then, it behaves like a totally new one, based on the emitted value.
switchFromF also gives you an
Auto that behaves like one
Auto has the ability to "replace itself" by having its
output blip stream emit a value. The value determines what it wants to
replace itself with.
These are really useful for implementing things like "modes" --- your program
has different modes of behavior, which you can represent with a different
Auto for each mode...and you can switch between them with these switches!
See the documentation for these at the Control.Auto.Switch module for more information!
In Control.Auto.Collection, we have a bunch of "
Auto boxes" and
Auto collections", which maintain
Autos that are dynamic collections of
For example, you have
zipAuto, which takes a list of
Autos and returns an
Auto taking in a list, that feeds each item in the input list into each
Auto. It's like running multiple
Autos in parallel on
For example, you have
mux f :: Auto m (k, a) b, which stores a bunch of
Auto m a bs indexed by a key
k. At every step, it takes a
looks up the
Auto at that
k, feeds in the
a, and outputs that output
b. You can use this to store several
Autos in parallel and really just
run the one you want at any given time.
gather f :: Auto m (k, a) (Map k b), which again stores a bunch
Auto m a bs indexed by a key
k. At every step, it updates only the
Auto at that key
k, but outputs a
Map of all the outputs so far by all
of the internal
See the documentation at Control.Auto.Collection for more!
Not exactly a tool per se, but the auto library has the ability to state and "solve" for recursive relationships.
We can define an
Auto that "chases" its input:
chaseFrom :: Num a => a -> Auto' a a chaseFrom x0 = proc target -> do rec let step = signum (target - x) -- 1 if target is bigger -- 0 if matches -- -1 if smaller x <- sumFromD x0 -< step id -< x
ghci> streamAuto' (chaseFrom 0) [3,3,3,3,3,-1,-1,-1,-1,-1] [0,1,2,3,3,3,2,1,0,-1] -- ^ chasing 3 ^ -- ^ chasing -1
x is the cumulative sum of each
step and the
step is determined based on
target and the current position
x's relationship is that it
is the cumulative sum of
step's relationship is that it is the
target. It's a recursive relationship!
The auto library will attempt to find a "fixed point" of the recursive
relationship...sort of "solving for" the output stream that will match this
recursive relationship. However, it needs a little help. For every step, it
needs a way to get a "first value" from something without needing any input.
That is, at least one of the
Autos in your proc block has to be able to
pop out its first result without an input.
This is what
sumFromD is for...we don't use
sumFromD will always output its original accumulator first, before taking
into account the inputs:
ghci> streamAuto' (sumFrom 0) [1..10] [1,3,6,10,15,21,28,36,45,55] ghci> streamAuto' (sumFromD 0) [1..10] [0,1,3,6,10,15,21,28,36,45]
This is how the auto library will "tie" the loop and find the fixed point. Have this, and everything works! Cyclic relationships and feedback loops... just like in real life!
One of this library's features is that the
Auto type offers an interface in
which you can serialize ("freeze") and "resume" an Auto, in
You can "freeze" any
Auto into a
encodeAuto, and you
can "resume" any
Auto from a
loadAuto "resume" a given
Auto. That is, if
decodeAuto on a "fresh
Auto", it'll decode a
Auto, but "resumed". That is, it'll "fast forward" that
Auto into the state it was when it was saved.
For example, let's look at
sumFrom 0. If it is fed 3 and 10, it'll have its
internal accumulator as 13, keeping track of all the numbers it has seen so
ghci> let a = sumFrom 0 ghci> let (_, a') = stepAuto' a 3 ghci> let (_, a'') = stepAuto' a' 10
encodeAuto can be used to "freeze"/"save" the
Auto into the
ghci> let bs = encodeAuto a''
decodeAuto can be used to "resume" from the original
was the original
Auto, the summer
Auto with a starting accumulator of 0.
decodeAuto will "resume" it, with and resume it with its internal
accumulator at 13.
ghci> let Right resumed = decodeAuto a bs ghci> let (y, _) = stepAuto' resumed 0 13
Note that not all
Autos in this library can be resumed. By default, you can
assume that they can...while those that can't will by naming convention be
suffixed with a
sumFrom_, for example. This means that
when you "save" the
Auto, you don't really save any state...and when you
"resume" it, nothing is really resumed, and resuming is a no-op:
-- sumFrom_ can't be saved/resumed, so it "goes nowhere" when resumed. decodeAuto (sumFrom_ 0) bs = Right (sumFrom_ 0)
This feature is useful for "save states" of certain
Autos, or just for
serialization and resuming in general.
You can play some fun tricks with the Control.Auto.Serialize
saving "foo.dat" will turn any
Auto into an
that serializes itself at every step to "foo.dat"
ghci> let a1 = saving "foo.dat" (sumFrom 0) :: Auto IO Int Int ghci> streamAuto a1 [1..10] -- saves the Auto as it goes along [ 1, 3, 6,10,15,21,28,36, 45, 55] ghci> a2 <- readAutoErr "foo.dat" a1 :: Auto IO Int Int ghci> streamAuto a2 [1..10] -- a2 is resumed to where a1 was last [56,58,61,65,70,76,83,91,100,110]
The magic of implicit serialization is that the serialization of complex
Autos is preserved under combination and manipulation with the various
instances and combinators in this library. For example, serializing the
blippy example above, or a huge complex application, is all done
automatically! The overall serialization structure is implicitly built and
inferred. Think of it like the library analyzing what needs to be serialized
in your program, and coming up with a serialization and reloading strategy.
This is used to great effect in auto-examples, where entire applications and chat bots are serialized..."for free". Build complex chat bots, and the serialization is handled implicitly.
There is one slightly drawback however...the "safecopy" problem. If you
alter the structure of your
Auto by adding another aspect that needs to be
Auto can no longer "read"/resume from the binary
serialization of its older version, because it'll expect the previous
serialization strategy, and be unable to read it. This means that, if you
publish programs, save files might become unloadable by new versions of your
One solution is to serialize individual portions only of your program ---
portions that you know will stay fixed. You can do this by techniques in
chatbot, where each individual module of the chatbot is serialized to its
own place on disk using
serializing, a variation of
saving from above.
That way, if you add more modules to the chat bot, it can still individually
resume its smaller modules without caring about the rest.
(I'll admit that this is not a perfect solution; more research and experiments are continually being done. Feel free to talk to me if you have any ideas or leads!)
One last note before finishing up...if you ever want to implement a low-level
library, or implement a "back-end", defining your own
Autos and working with
them has its own rules. You're a bit "on your own", in this sense; the
optimization game might take you to places that really get rid of the nice
semantic denotative ideals of this library. I plan on writing a
framework/low-level guide soon (for writing, say, a GUI framework, or hooking
However, one good principle is just to separate your "two hats" as much as
possible. There's the hat you wear when you are thinking about your program
logic, dealing with compositions of ideas ... and there's the hat you wear
when you are at the nitty-gritty interface between your system and the real
world. One goal in Haskell is always to be able to create as clear a divide as
possible...so you can really enjoy the best of both worlds. So just make sure
Autos and API that you export behave in meaningful ways that you
can reason about...just what we expect from using
Anyways, I recommend just looking over the combinators available to you in the various modules, like Control.Auto.Blip, Control.Auto.Interval, and Control.Auto.Switch. We didn't go over anything close to all of them in this tutorial, so it's nice for getting a good overview. The most up-to-date documentation at this point in time is on the github pages, but there's also the hackage docs as well.
A good next step too would be to check out the All About Auto series on my blog, where I break down approaching and finishing real-world problems with the library using the tools described here. You can also look at the auto-examples directory and peruse over the examples, which each highlight a different aspect of the library, so you can see how all of these ideas work together.
Help is always available on the #haskell-auto channel on freenode IRC, and there's also a gitter channel if IRC is not your thing; you can also email me at firstname.lastname@example.org, or find me on twitter as mstk. There is no mailing list or message board yet, but for now, feel free to abuse the github issue tracker.
Now go forth and make locally stateful, denotative, declarative programs!