auto-0.2.0.4: Denotative, locally stateful programming DSL & platform

Copyright (c) Justin Le 2015 MIT justin@jle.im unstable portable None Haskell2010

Control.Auto.Interval

Description

This module provides combinators and utilities for working with the semantic concept of "intervals": an Auto whose output stream is "on" or "off" for (conceputally) contiguous chunks of time.

Synopsis

# Intervals

An auto that exhibits this "interval" behavior is represented with the Interval type synonym:

type Interval m a b = Auto m a (Maybe b)
type Interval' a b  = Auto' a (Maybe b)

So, the compiler sees an Interval m a b as if it were an Auto m a (Maybe b). If it helps you reason about type signatures and type inference, you can make the substitution in your head too!

An Interval m a b takes an input stream of as and output stream of bs that are "on" and "off" for chunks at a time; Nothing is interpreted as "off", and Just x is interpreted as "on" with a value of x.

A classic example is onFor :: Int -> Interval m a a. With onFor n, the output stream behaves exactly like the input stream for the first n steps, then is "off" forever after:

>>> streamAuto' (onFor 3) [1..7]
[Just 1, Just 2, Just 3, Nothing, Nothing, Nothing, Nothing]

## Motivation

Intervals happen to particularly useful when used with the various switching combinators from Control.Auto.Switch.

You might find it useful to "sequence" Autos such that they "switch" from one to the other, dynamically. For example, an Auto that acts like pure 0 for three steps, and then like count for the rest:

>>> let a1 = (onFor 3 . pure 0) --> count
>>> take 8 . streamAuto' a1 \$ repeat ()
[0, 0, 0, 1, 2, 3, 4, 5]

(Recall that pure x is the Auto that ignores the input stream and gives an output stream of constant xs)

Or in reverse, an Auto that behaves like count until the count is above 3, then switches to pure 0

>>> let a2 = (whenI (<= 3) . count) --> pure 0
>>> take 8 . streamAuto' a2 \$ repeat ()
[1, 2, 3, 0, 0, 0, 0, 0]

That's just a small example using one switching combinator, -->. But hopefully it demonstrates that one powerful motivation behind "intervals" being a "thing" is because of how it works with switches.

Another neat motivation is that intervals work pretty well with the Blip semantic tool, as well.

The following Interval will be "off" and suppress all of its input (from count) until the blip stream produced by inB 3 emits something, then it'll allow count to pass.

>>> let a3 = after . (count &&& inB 3)
>>> let a3 = proc () -> do
c   <- count -< ()
blp <- inB 3 -< ()
after -< (c, blp)
>>> take 5 . streamAuto' a3 \$ repeat ()
[Nothing, Nothing, Just 3, Just 4, Just 4]

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" value. gather keeps a collection of Intervals, and removes them whenever they output a Nothing/turn "off".

## The Contract

So, why have an Interval type, and not always just use Auto?

You can say that, if you are given an Interval, then it comes with a "contract" (by documentation) that the Auto will obey /interval semantics/.

Auto m a (Maybe b) can mean a lot of things and represent a lot of things.

However, if you offer something of an Interval type, or if you find something of an Interval type, it comes with some sort of assurance that that Auto will behave like an interval: on and off for contiguous periods of time.

In addition, this allows us to further clarify /what our functions expect/. By saying that a function expects an Interval:

chooseInterval :: [Interval m a b]
-> Interval m a b

chooseInterval has the ability to "state" that it expects things that follow interval semantics in order to "function" properly and in order to properly "return" an Interval.

Of course, this is not enforced by the compiler. However, it's useful to create a way to clearly state that what you are offering or what you are expecting does indeed follow this useful pattern.

## Combinators

### Converting back into normal streams

You can take an incoming interval stream and output a "normal" "always-on" stream by using the fromInterval and fromIntervalWith Autos, analogous to fromMaybe and maybe from Data.Maybe, respectively:

>>> let a = fromIntervalWith "off" show . onFor 2
>>> streamAuto' a [1..5]
["1", "2", "off", "off", "off"]

You can also use <|!>, coming up next....

### Choice

You can "choose" between interval streams, with choice combinators like <|?> and <|!>.

>>> let a = onFor 2 . pure "hello"
<|!> onFor 4 . pure "world"
<|!> pure "goodbye!"
>>> take 6 . streamAuto' a \$ repeat ()
["hello", "hello", "world", "world", "goodbye!", "goodbye!"]

The above could also be written with choose:

>>> let a = choose (pure "goodbye!")
[ onFor 2 . pure "hello"
, onFor 4 . pure "world"
]

### Composition

Another tool that makes Intervals powerful is the ability to compose them.

If you have an Auto m a b and an Auto m b c, then you can compose them with ..

If you have an Auto m a b and an Interval m b c, then you can compose them by throwing in a toOn in the chain, or fmap Just:

a               :: Auto m a b
i               :: Interval m b c
i . toOn . a    :: Interval m a c
fmap Just a :: Interval m a b
i . fmap Just a :: Interval m a c

If you have an Interval m a b and an Auto m b c, you can "lift" the second Auto to be an Auto that only "acts" on "on"/Just outputs of the Interval:

i            :: Interval m a b
a            :: Auto m b c
during a     :: Auto m (Maybe a) (Maybe b)
during a . i :: Interval m a c

Finally, the kleisli composition: if you have an Interval m a b and an Interval m b c, you can use compI: (or also bindI)

i1            :: Interval m a b
i2            :: Interval m b c
i2 `'compI'` i1 :: Interval m a b c
bindI i2 . i1 :: Interval m a b c
>>> let a1        = when (< 5) `compI` offFor 2
>>> streamAuto' a1 [1..6]
[Nothing, Nothing, Just 3, Just 4, Nothing, Nothing]

The implementation works so that any "on"/Just inputs will step the lifted Auto like normal, with the contents of the Just, and any "off"/Nothing inputs cause the lifted Auto to be skipped.

compI adds a lot of power to Interval because now you can always work "with Intervals", bind them just like normal Autos, and then finally "exit" them after composing and combining many.

## Warning: Switching

Note that when any of these combinators "block" (or "inhibit" or "suppress", whatever you call it) their input as a part of a composition pipeline (as in for off, onFor, offFor, etc.), the input Autos are still stepped and "run". If the inputs had any monad effects, they would too be executed at every step. In order to "freeze" and not run or step an Auto at all, you have to use switches.

type Interval m a b = Auto m a (Maybe b) Source

Represents a relationship between an input and an output, where the output can be "on" or "off" (using Just and Nothing) for contiguous chunks of time.

Just a type alias for Auto m a (Maybe b). If you ended up here with a link...no worries! If you see Interval m a b, just think Auto m a (Maybe b) for type inference/type checking purposes.

If you see something of type Interval, you can rest assured that it has "interval semantics" --- it is on and off for meaningfully contiguous chunks of time, instead of just on and off willy nilly. If you have a function that expects an Interval, then the function expects its argument to behave in this way.

type Interval' a b = Auto' a (Maybe b) Source

Interval, specialized with Identity as its underlying Monad. Analogous to Auto' for Auto.

# Static Intervals

off :: Interval m a b Source

The output stream is alwayas off, regardless of the input.

Note that any monadic effects of the input Auto when composed with off are still executed, even though their result value is suppressed.

off == pure Nothing

toOn :: Interval m a a Source

The output stream is always on, with exactly the value of the corresponding input.

toOn == arr Just

Arguments

 :: a value to output for "off" periods -> Auto m (Maybe a) a

An "interval collapsing" Auto. A stream of on/off values comes in; the output is the value of the input when the input is on, and the "default value" when the input is off.

Much like fromMaybe from Data.Maybe.

fromInterval d = arr (fromMaybe d)

Arguments

 :: b default value, when input is off -> (a -> b) function to apply when input is on -> Auto m (Maybe a) b

An "interval collapsing" Auto. A stream of on/off values comes in; when the input is off, the output is the "default value". When the input is off, the output is the given function applied to the "on" value.

Much like maybe from Data.Maybe.

fromIntervalWith d f = arr (maybe d f)

Arguments

 :: Int amount of steps to stay "on" for -> Interval m a a

For onFor n, the first n items in the output stream are always "on" (passing through with exactly the value of the corresponding input); for the rest, the output stream is always "off", suppressing all input values forevermore.

If a number less than 0 is passed, 0 is used.

Arguments

 :: Int amount of steps to be "off" for. -> Interval m a a

For offFor n, the first n items in the output stream are always "off", suppressing all input; for the rest, the output stream is always "on", outputting exactly the value of the corresponding input.

Arguments

 :: Int start of window -> Int end of window (inclusive) -> Interval m a a

A combination of onFor and offFor; for window b e, the output stream will be "on" from item b to item e inclusive with the value of the corresponding input; for all other times, the output stream is always off, suppressing any input.

# Filter Intervals

Arguments

 :: (a -> Bool) interval predicate -> Interval m a a

The output is "on" with exactly the value of he corresponding input when the input passes the predicate, and is "off" otherwise.

>>> let a = whenI (\x -> x >= 2 && x <= 4)
>>> streamAuto' a [1..6]
[Nothing, Just 2, Just 3, Just 4, Nothing, Nothing]

Careful when using this; you could exactly create an Interval that "breaks" "interval semantics"; for example, 'whenI even', when you know your input stream does not consist of chunks of even numbers and odd numbers at a time.

Arguments

 :: (a -> Bool) interval predicate -> Interval m a a

Like whenI, but only allows values to pass whenever the input does not satisfy the predicate. Blocks whenever the predicate is true.

>>> let a = unlessI (\x -> x < 2 &&& x > 4)
>>> steamAuto' a [1..6]
>>> res
[Nothing, Just 2, Just 3, Just 4, Nothing, Nothing]

# Choice

(<|!>) infixr 3 Source

Arguments

 :: Monad m => Interval m a b interval Auto -> Auto m a b "normal" Auto -> Auto m a b

Forks a common input stream between an Interval and an Auto, and returns, itself, a normal non-interval Auto.. If the output of the first one is "on", the output of the whole thing is that "on" value. Otherwise, the output is exactly that of the second one.

>>> let a1 = (onFor 2 . pure "hello") <|!> pure "world"
>>> take 5 . streamAuto' a1 \$ repeat ()
["hello", "hello", "world", "world", "world"]

This one is neat because it associates from the right, so it can be "chained":

>>> let a2 = onFor 2 . pure "hello"
<|!> onFor 4 . pure "world"
<|!> pure "goodbye!"
>>> take 6 . streamAuto' a2 \$ repeat ()
["hello", "hello", "world", "world", "goodbye!", "goodbye!"]
a <|!> b <|!> c

associates as

a <|!> (b <|!> c)

So using this, you can "chain" a bunch of choices between intervals, and then at the right-most, "final" one, provide the default behavior.

Warning: If your underlying monad produces effects, remember that both Autos are run at every step, along with any monadic effects, regardless of whether they are "on" or "off".

(<|?>) infixr 3 Source

Arguments

 :: Monad m => Interval m a b choice 1 -> Interval m a b choice 2 -> Interval m a b

Forks a common input stream between the two Intervals and returns, itself, an Interval. If the output of the first one is "on", the whole thing is on with that output. Otherwise, the output is exactly that of the second one.

>>> let a = (onFor 2 . pure "hello") <|?> (onFor 4 . pure "world")
>>> take 5 . streamAuto' a \$ repeat ()
>>> res
[Just "hello", Just "hello", Just "world", Just "world", Nothing]

You can drop the parentheses, because of precedence; the above could have been written as:

>>> let a' = onFor 2 . pure "hello" <|?> onFor 4 . pure "world"

Warning: If your underlying monad produces effects, remember that both Autos are run at every step, along with any monadic effects, regardless of whether they are "on" or "off".

Note that more often than not, <|!> is probably more useful. This is useful only in the case that you really, really want an interval at the end of it all.

Arguments

 :: Monad m => [Interval m a b] the Autos to run and choose from -> Interval m a b

Forks an input stream between all Intervals in the list. The result is an Interval whose output is "on" when any of the original Intervals is on, with the value of the first "on" one.

chooseInterval == foldr (<|?>) off

Arguments

 :: Monad m => Auto m a b the Auto to behave like if all others are Nothing -> [Interval m a b] Autos to run and choose from -> Auto m a b

Forks an input stream between all Intervals in the list, plus a "default Auto. The output is the value of the first "on" Interval; if there isn't any, the output from the "default Auto" is used.

choose == foldr (<|!>)

# Blip-based Intervals

after :: Interval m (a, Blip b) a Source

Takes two input streams --- a stream of normal values, and a blip stream. Before the first emitted value of the input blip stream, the output is always "off", suppressing all inputs. After the first emitted value of the input blip stream, the output is always "on" with the corresponding value of the first input stream.

>>> let a = after . (count &&& inB 3)
>>> take 6 . streamAuto' a \$ repeat ()
>>> res
[Nothing, Nothing, Just 3, Just 4, Just 5, Just 6]

(count is the Auto that ignores its input and outputs the current step count at every step, and inB 3 is the Auto generating a blip stream that emits at the third step.)

Be careful to remember that in the above example, count is still "run" at every step, and is progressed (and if it were an Auto with monadic effects, they would still be executed). It just isn't allowed to pass its output values through after until the blip stream emits.

before :: Interval m (a, Blip b) a Source

Takes two input streams --- a stream of normal values, and a blip stream. Before the first emitted value of the input blip stream, the output is always "on" with the corresponding value of the first input stream. After the first emitted value of the input blip stream, the output will be "off" forever, suppressing all input.

>>> let a = before . (count &&& inB 3)
>>> take 5 . streamAuto' a \$ repeat ()
>>> res
[Just 1, Just 2, Nothing, Nothing, Nothing]

(count is the Auto that ignores its input and outputs the current step count at every step, and inB 3 is the Auto generating a blip stream that emits at the third step.)

Be careful to remember that in the above example, count is still "run" at every step, and is progressed (and if it were an Auto with monadic effects, they would still be executed). It just isn't allowed to pass its output values through before after the blip stream emits.

between :: Interval m (a, (Blip b, Blip c)) a Source

Takes three input streams: a stream of normal values, a blip stream of "turning-on" blips, and a blip stream of "turning-off" blips. After the first blip stream emits, the output will switch to "on" with the value of the first input stream. After the second blip stream emits, the output will switch to "off", supressing all inputs. An emission from the first stream toggles this "on"; an emission from the second stream toggles this "off".

>>> let a        = between . (count &&& (inB 3 &&& inB 5))
>>> take 7 . streamAuto' a \$ repeat ()
[Nothing, Nothing, Just 3, Just 4, Nothing, Nothing, Nothing]

hold :: Serialize a => Interval m (Blip a) a Source

The output is constantly "on" with the last emitted value of the input blip stream. However, before the first emitted value, it is "off". value of the input blip stream. From then on, the output is always the last emitted value

>>> let a = hold . inB 3
>>> streamAuto' a [1..5]
[Nothing, Nothing, Just 3, Just 3, Just 3]

If you want an Auto m (Blip a) a (no Nothing...just a "default value" before everything else), then you can use holdWith from Control.Auto.Blip...or also just hold with <|!> or fromInterval.

hold_ :: Interval m (Blip a) a Source

The non-serializing/non-resuming version of hold.

Arguments

 :: Serialize a => Int number of steps to hold the last emitted value for -> Interval m (Blip a) a

For holdFor n, The output is only "on" if there was an emitted value from the input blip stream in the last n steps. Otherwise, is off.

Like hold, but it only "holds" the last emitted value for the given number of steps.

>>> let a = holdFor 2 . inB 3
>>> streamAuto' 7 a [1..7]
>>> res
[Nothing, Nothing, Just 3, Just 3, Nothing, Nothing, Nothing]

Arguments

 :: Int number of steps to hold the last emitted value for -> Interval m (Blip a) a

The non-serializing/non-resuming version of holdFor.

# Composition with Interval

Arguments

 :: Monad m => Auto m a b Auto to lift to work over intervals -> Auto m (Maybe a) (Maybe b)

Lifts an Auto m a b (transforming as into bs) into an Auto m (Maybe a) (Maybe b) (or, Interval m (Maybe a) b, transforming intervals of as into intervals of b.

It does this by running the Auuto as normal when the input is "on", and freezing itbeing "off" when the input is off/.

>>> let a1 = during (sumFrom 0) . onFor 2 . pure 1
>>> take 5 . streamAuto' a1 \$ repeat ()
[Just 1, Just 2, Nothing, Nothing, Nothing]
>>> let a2 = during (sumFrom 0) . offFor 2 . pure 1
>>> take 5 . streamAuto' a2 \$ repeat ()
[Nothing, Nothing, Just 1, Just 2, Just 3]

(Remember that pure x is the Auto that ignores its input and constantly just pumps out x at every step)

Note the difference between putting the sumFrom "after" the offFor in the chain with during (like the previous example) and putting the sumFrom "before":

>>> let a3 = offFor 2 . sumFrom 0 . pure 1
>>> take 5 . streamAuto' a3 \$ repeat ()
[Nothing, Nothing, Just 3, Just 4, Just 5]

In the first case (with a2), the output of pure 1 was suppressed by offFor, and during (sumFrom 0) was only summing on the times that the 1's were "allowed through"...so it only "starts counting" on the third step.

In the second case (with a3), the output of the pure 1 is never suppressed, and went straight into the sumFrom 0. sumFrom is always summing, the entire time. The final output of that sumFrom 0 is suppressed at the end with offFor 2.

compI infixr 1 Source

Arguments

 :: Monad m => Interval m b c compose this Interval... -> Interval m a b ...to this one -> Interval m a c

Composes two Intervals, the same way that . composes two Autos:

(.)   :: Auto     m b c -> Auto     m a b -> Auto     m a c
compI :: Interval m b c -> Interval m a b -> Interval m a c

Basically, if any Interval in the chain is "off", then the entire rest of the chain is "skipped", short-circuiting a la Maybe.

(Users of libraries with built-in inhibition semantics like Yampa and netwire might recognize this as the "default" composition in those other libraries)

As a contrived example, how about an Auto that only allows values through during a window...between, say, the second and fourth steps:

>>> let window' start dur = onFor dur `compI` offFor (start - 1)
>>> streamAuto' (window' 2 3)
[Nothing, Just 2, Just 3, Just 4, Nothing, Nothing]

Arguments

 :: Monad m => Interval m a b Interval to bind -> Interval m (Maybe a) b

Lifts (more technically, "binds") an Interval m a b into an Interval m (Maybe a) b.

Does this by running the Auto as normal when the input is "on", and freezing itbeing "off" when the input is off/.

It's kind of like during, but the resulting Maybe (Maybe b)) is "joined" back into a Maybe b.

bindI a == fmap join (during a)

This is really an alternative formulation of compI; typically, you will be using compI more often, but this form can also be useful (and slightly more general). Note that:

bindI f == compI f id

This combinator allows you to properly "chain" ("bind") together series of inhibiting Autos. If you have an Interval m a b and an Interval m b c, you can chain them into an Interval m a c.

f             :: Interval m a b
g             :: Interval m b c
bindI g . f :: Interval m a c

(Users of libraries with built-in inhibition semantics like Yampa and netwire might recognize this as the "default" composition in those other libraries)

See compI for examples of this use case.