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

Add module documentation for Mealy #39

Merged
Merged
Changes from 1 commit
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
83 changes: 81 additions & 2 deletions src/Data/Machine/Mealy.purs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
-- | This module provides the building blocks required to create
-- | finite state machines.
module Data.Machine.Mealy
( MealyT
, runMealyT
Expand Down Expand Up @@ -47,62 +49,115 @@ import Data.Tuple (Tuple(..), fst, snd, swap)
import Data.Unfoldable (class Unfoldable, unfoldr)
import Effect.Class (class MonadEffect, liftEffect)

-- | Mealy is a finite state machine, where:
-- |
-- | - `f` is the effect under which we evaluate,
-- | - `s` is the input state, and
-- | - `a` is the output type.
newtype MealyT f s a = MealyT (s -> f (Step f s a))

runMealyT :: forall f s a. MealyT f s a -> s -> f (Step f s a)
runMealyT (MealyT f) = f

-- | Transforms a Mealy machine running in the context of `f` into one running
-- | Transforms a Mealy machine running in the context of `f` into one running
-- | in `g`, given a natural transformation from `f` to `g`.
hoistMealyT :: forall f g s . Functor g => (f ~> g) -> MealyT f s ~> MealyT g s
hoistMealyT f2g (MealyT goF) = MealyT goG
where goG s = hoistStep f2g <$> f2g (goF s)

-- | Step is the core for running machines. Machines can either stop
-- | via the `Halt` constructor, or emit a value and recursively
-- | construct the rest of the machine.
data Step f s a = Emit a (MealyT f s a) | Halt

-- | Transforms a step running in the context of `f` into one running
-- | in `g`, given a natural transformation from `f` to `g`.
hoistStep :: forall f g s . Functor g => (f ~> g) -> Step f s ~> Step g s
hoistStep f2g (Emit v nxt) = Emit v (hoistMealyT f2g nxt)
hoistStep _ Halt = Halt


-- | Sources are machines with trivial `Unit` input value.
type Source f a = MealyT f Unit a

-- | Sinks are machines with trivial `Unit` output values.
type Sink f s = MealyT f s Unit

-- | Wrap an effectful value into a source. The effect will be repeated
-- | indefinitely.
-- |
-- | For example, generating ten instances of the value 1:
-- | ```purescript
-- | take 10 $ source (pure 1)
-- | ```
source :: forall f s. (Monad f) => f s -> Source f s
source src = mealy $ \_ -> flip Emit (source src) <$> src

-- | Sinks are 'terminator nodes' in machines. They allow for an
thomashoneyman marked this conversation as resolved.
Show resolved Hide resolved
-- | effectful computation to be executed on the inputs.
-- |
-- | For example, logging could be used as a sink:
-- | ```purescript
-- | take 10 $ source (pure 1) >>> sink logShow
-- | ```
sink :: forall f a. (Monad f) => (a -> f Unit) -> Sink f a
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I look at these, this Monad constraint looks too powerful. Just a thought, but perhaps in a later PR we could relax this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I also noticed that but assumed it was a deliberate decision that I didn't understand...? If it's not, then I'll be happy to fix them.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we ought to, if you’re up for it!

sink f = mealy $ \a -> const (Emit unit (sink f)) <$> f a

-- | Run a machine as an effectful computatation.
-- |
-- | For example:
-- | ```purescript
-- | runMealy $ take 10 $ source (pure 1) >>> sink logShow
-- | ```
runMealy :: forall f. (Monad f) => MealyT f Unit Unit -> f Unit
runMealy m = stepMealy unit m >>= f
where f Halt = pure unit
f (Emit _ m') = runMealy m'

-- | Execute (unroll) a single step on a machine.
stepMealy :: forall f s a. (Monad f) => s -> MealyT f s a -> f (Step f s a)
stepMealy = flip runMealyT

-- | Wrap a pure function into a machine. The function can either
-- | terminate via `Halt`, or `Emit` a value and then decide whether
-- | to `Halt`, continue with a different function, or (usually) wrap
-- | itself via `pureMealy` recursively.
-- |
-- | For example, we can `Halt` on zero:
-- | ```purescript
-- | haltOn0 :: forall f. Applicative f => MealyT f Int Int
-- | haltOn0 = pureMealy go
-- | where
-- | go 0 = Halt
-- | go n = Emit n (pureMealy haltOn0)
-- | ```
pureMealy :: forall f s a. (Applicative f) => (s -> Step f s a ) -> MealyT f s a
pureMealy = MealyT <<< map pure

-- | Wrap an effectful function into a machine. See `pureMealy` for
-- | an example using pure functions.
mealy :: forall f s a. (Applicative f) => (s -> f (Step f s a)) -> MealyT f s a
mealy = MealyT

-- | A machine which halts for any input.
halt :: forall f s a. (Applicative f) => MealyT f s a
halt = pureMealy $ const Halt

-- | Limit the number of outputs of a machine. After using up the `n`
-- | allotted outputs, the machine will halt.
take :: forall f s a. (Monad f) => Int -> MealyT f s a -> MealyT f s a
take n m = if n <= 0 then halt
else mealy $ \s -> f <$> stepMealy s m
where f Halt = Halt
f (Emit a m') = Emit a (take (n - 1) m')

-- | Skip a number of outputs for a machine.
drop :: forall f s a. (Monad f) => Int -> MealyT f s a -> MealyT f s a
drop n m = if n <= 0 then m
else mealy $ \s -> let f Halt = pure Halt
f (Emit a m') = stepMealy s (drop (n - 1) m')
in stepMealy s m >>= f

-- | Loop a machine forever.
loop :: forall f s a. (Monad f) => MealyT f s a -> MealyT f s a
loop m0 = loop' m0
where
Expand All @@ -111,6 +166,7 @@ loop m0 = loop' m0
Halt -> stepMealy s (loop m0)
Emit a m' -> pure $ Emit a (loop' m')

-- | Extract all the outputs of a machine, given some input.
toUnfoldable
:: forall f g s a
. Unfoldable g
Expand All @@ -122,49 +178,69 @@ toUnfoldable s = unfoldr stepUnfold
Emit a m' -> Just $ Tuple a m'
Halt -> Nothing

-- | Zip two machines together under some function `f`.
zipWith :: forall f s a b c. (Monad f) => (a -> b -> c) -> MealyT f s a -> MealyT f s b -> MealyT f s c
zipWith f a b = f <$> a <*> b

-- | Accumulate the outputs of a machine into a new machine.
scanl :: forall f s a b. (Monad f) => (b -> a -> b) -> b -> MealyT f s a -> MealyT f s b
scanl f = go where
go b m = mealy $ \s -> let g Halt = Halt
g (Emit a m') = (let b' = f b a in Emit b' (go b' m'))
in g <$> stepMealy s m

-- | Accumulates the outputs of a machine as a `List`.
collect :: forall f s a. (Monad f) => MealyT f s a -> MealyT f s (List a)
collect = scanl (flip Cons) Nil

-- | Creates a machine which emits a single value before halting.
singleton :: forall f s a. (Monad f) => a -> MealyT f s a
singleton a = pureMealy $ \s -> Emit a halt

-- | Creates a machine which either emits a single value before halting
-- | (for `Just`), or just halts (in the case of `Nothing`).
fromMaybe :: forall f s a. (Monad f) => Maybe a -> MealyT f s a
fromMaybe Nothing = halt
fromMaybe (Just a) = singleton a

-- | Creates a machine whbich emits all the values of the array before
-- | halting.
fromArray :: forall f s a. (Monad f) => Array a -> MealyT f s a
fromArray a = let len = length a
go n | n < zero || n >= len = halt
go n = fromMaybe (a !! n) <> go (n + one)
in go zero

-- | Creates a machine which wraps an effectful computation and ignores
-- | its input.
wrapEffect :: forall f s a. (Monad f) => f a -> MealyT f s a
wrapEffect fa = MealyT $ const (flip Emit halt <$> fa)

-- MonadLogic -- TODO: Create a purescript-logic package
-- | Unwrap a machine such that its output is either `Nothign` in case
thomashoneyman marked this conversation as resolved.
Show resolved Hide resolved
-- | it would halt, or `Just` the output value and the next computation.
msplit :: forall f s a. (Monad f) => MealyT f s a -> MealyT f s (Maybe (Tuple a (MealyT f s a)))
msplit m = mealy $ \s -> f <$> stepMealy s m
where f Halt = Emit (Nothing) halt
f (Emit a m') = Emit (Just $ Tuple a m') (msplit m')

-- | Interleaves the values of two machines with matching inputs and
-- | outputs.
interleave :: forall f s a. (Monad f) => MealyT f s a -> MealyT f s a -> MealyT f s a
interleave m1 m2 = mealy $ \s ->
stepMealy s m1 >>= case _ of
Halt -> stepMealy s m2
Emit a m1' -> pure $ Emit a (interleave m2 m1')

-- | Takes a single output from a machine.
once :: forall f s a. (Monad f) => MealyT f s a -> MealyT f s a
once = take 1

-- | If then else: given a machine producing `a`, a continuation `f`,
-- | and a machine producing `b`, generate a machine which will
-- | grab outputs from the first machine and pass them over to the
-- | continuation as long as neither halts.
-- | Once the process halts, the second (`b`) machine is returned.
ifte :: forall f s a b. (Monad f) => MealyT f s a -> (a -> MealyT f s b) -> MealyT f s b -> MealyT f s b
ifte ma f mb = mealy $ \s ->
stepMealy s ma >>= case _ of
Expand All @@ -175,6 +251,9 @@ ifte ma f mb = mealy $ \s ->
Halt -> Halt
Emit b fb -> Emit b (fb <> ifte ma' f mb)

-- | Given a machine and a continuation, it will pass outputs from
-- | the machine to the continuation as long as possible until
-- | one of them halts.
when :: forall f s a b. (Monad f) => MealyT f s a -> (a -> MealyT f s b) -> MealyT f s b
when ma f = ifte ma f halt

Expand Down