effable-0.1.0.0: A data structure for emission plans
Safe HaskellNone
LanguageGHC2021

Effable

Description

An Effable m b is

  • a pure plan for the later emission of bs
  • a representation of an ordered sequence of bs, each annotated with an emission wrapper m () -> m ()
  • emitted to its eventual result m () with run (interpretation/elimination)
  • fairly opaque

    • its constructor is not exported
    • observing it is only supported through run/runWith
  • niche: see Caveats section

Why?

Compared to just working in the monadic m b context, Effable brings one particular distinguishing feature:

An Effable can undergo decoration with emission wrappers (wrap, wrapInside) after which it is still a pure Effable.

This means that even after having been modified with wrappers, or any other supported transformation...

  • ...it is still a Functor, Applicative and Monad in b
  • ...it can be transformed further and combined into more complex structures with <>

Emission wrappers can be applied to granular constituents of an Effable as the user code is building it. The Effable machinery will track the wrappers behind-the-scenes through all supported transformations so that the eventual emission respects them.

Caveats and usage scope

  • effectful predicates and emission wrappers typically run more than once when a plan is emitted

    • these actions should yield the same value across evaluations, otherwise the inclusion of branches will be inconsistent
    • therefore, Effable is only suitable when these actions are read-like (return the same value over repeated evaluations and are free of externally observable side-effects)
  • the nesting of combinators and use of <*> and >>= will grow the internal Effable representation, and the number of times actions are run at emission, combinatorially

Intuition

Effables are kept pure through all supported transformations by representing all possible outcomes of actions that affect structure. Running the actions is deferred to emission time. This comes with the cost of the internal representation carrying a complexity proportional to all possible outcomes.

Metaphorically, Effable is the many-worlds interpretation of actions meaning actions can be represented without them interacting with the actual world (= purity) and its representation is not collapsed to an outcome until it is observed (= run).

"Effable"?

Effable as in sayable or utterable.

Or, Eff-able as in able to be effected or effectuated - something with the potential of becoming effects.

Synopsis

Type

data Effable (m :: Type -> Type) b Source #

An ordered sequence of values, each with an associated emission wrapper (default: id).

Instances

Instances details
Alternative (Effable m) Source #

(<|>) == (<>).

Instance details

Defined in Effable

Methods

empty :: Effable m a #

(<|>) :: Effable m a -> Effable m a -> Effable m a #

some :: Effable m a -> Effable m [a] #

many :: Effable m a -> Effable m [a] #

Applicative (Effable m) Source #

Applicative models list-like indeterminism: the result of xs <*> fs is all combinations of embedded functions and embedded values.

In the result of fs <*> xs, emission wrappers of fs are composed on the outside of that of xs.

Examples

>>> import Data.Functor.Const (Const (..))
>>> runConst = run (\b -> Const [b])
>>> runConst $ (embed pred <> embed succ) <*> (embed '1' <> embed 'b')
Const "0a2c"
>>> runConst $ embed succ <*> (embed (1::Word8) <> whenA True (embed 5))
Const [2,6]
Instance details

Defined in Effable

Methods

pure :: a -> Effable m a #

(<*>) :: Effable m (a -> b) -> Effable m a -> Effable m b #

liftA2 :: (a -> b -> c) -> Effable m a -> Effable m b -> Effable m c #

(*>) :: Effable m a -> Effable m b -> Effable m b #

(<*) :: Effable m a -> Effable m b -> Effable m a #

Functor (Effable m) Source # 
Instance details

Defined in Effable

Methods

fmap :: (a -> b) -> Effable m a -> Effable m b #

(<$) :: a -> Effable m b -> Effable m a #

Monad (Effable m) Source #

Monad models list-like indeterminism.

The result of xs >>= f is the concatenation of the results of applying f to each value embedded in xs.

Note that the Monad instance of Effable is not related to the type parameter m in a value of type Effable m b - that m is a parameterization of the eventual effectful context (m ()) that will be used for emission. The Monad instance of Effable, on the other hand, allows for and defines monadic computations on Effable m b values themselves.

Instance details

Defined in Effable

Methods

(>>=) :: Effable m a -> (a -> Effable m b) -> Effable m b #

(>>) :: Effable m a -> Effable m b -> Effable m b #

return :: a -> Effable m a #

MonadPlus (Effable m) Source #

mplus == (<>).

Instance details

Defined in Effable

Methods

mzero :: Effable m a #

mplus :: Effable m a -> Effable m a -> Effable m a #

IsString b => IsString (Effable m b) Source # 
Instance details

Defined in Effable

Methods

fromString :: String -> Effable m b #

Monoid (Effable m b) Source # 
Instance details

Defined in Effable

Methods

mempty :: Effable m b #

mappend :: Effable m b -> Effable m b -> Effable m b #

mconcat :: [Effable m b] -> Effable m b #

Semigroup (Effable m b) Source # 
Instance details

Defined in Effable

Methods

(<>) :: Effable m b -> Effable m b -> Effable m b #

sconcat :: NonEmpty (Effable m b) -> Effable m b #

stimes :: Integral b0 => b0 -> Effable m b -> Effable m b #

type Wrap (m :: Type -> Type) = m () -> m () Source #

An emission wrapper.

Create

singleton :: Wrap m -> b -> Effable m b Source #

embed :: forall b (m :: Type -> Type). b -> Effable m b Source #

string :: forall b (m :: Type -> Type). IsString b => String -> Effable m b Source #

empty :: forall (m :: Type -> Type) b. Effable m b Source #

Transform items

mapItems :: forall b b' (m :: Type -> Type). (b -> b') -> Effable m b -> Effable m b' Source #

Map items.

mapItems == fmap

Example:

>>> evenEffect x = if even x then Just () else Nothing
>>> run evenEffect ((+1) <$> embed 3)
Just ()

Transform wraps

These hold for both wrap and wrapInside:

wrap f mempty    ==  mempty
wrap f (x <> y)  ==  wrap f x <> wrap f y    -- distributes over <>
g <$> wrap f x   ==  wrap f (g <$> x)        -- commutes with fmap

wrap :: Wrap m -> Effable m b -> Effable m b Source #

Add an additional emission wrapper to each b.

The given function composes outside of any existing wrappers.

wrap id          ==  id
wrap (f . g)     ==  wrap f . wrap g

run' (wrap f x)  ==  f <$> (run' x)      where run' = runWith emit

wrapInside :: Wrap m -> Effable m b -> Effable m b Source #

Add an additional emission wrapper to each b.

The given function composes inside of any existing wrappers.

wrapInside id              ==  id
wrapInside (f . g)         ==  wrapInside g . wrapInside f

run emit (wrapInside f x)  ==  run (f . emit) x

Branching

when' Source #

Arguments

:: Monad m 
=> m Bool 
-> Effable m b

to include if True (else nothing)

-> Effable m b 

Conditional inclusion.

Suppress the effects of emitting the value if an effectful predicate evaluates to False:

run emit (when' (pure False) x)  == pure ()
run emit (when' (pure True ) x)  == run emit x

when' has the distributive and commutation properties as those of wrap.

When emitted, the monadic action will be run once for each element of the internal representation.

whenA Source #

Arguments

:: forall (f :: Type -> Type) b. Applicative f 
=> Bool 
-> Effable f b

to include if True (else nothing)

-> Effable f b 

onlyIf infixl 7 Source #

Arguments

:: Monad m 
=> Effable m b

to include if True (else nothing)

-> m Bool 
-> Effable m b 

Flipped when'.

ifThenElse Source #

Arguments

:: Monad m 
=> m Bool 
-> Effable m b

to include if True

-> Effable m b

to include if False

-> Effable m b 

Binary choice.

When emitted, the monadic action will be run twice for each element of the internal representation.

type Enumerable a = (Enum a, Bounded a, Eq a) Source #

Constraining to types whose inhabitants can be enumerated with [minBound..maxBound].

Such types are types with a known and finite set of inhabitants, given that their Enum and Bounded instances behave within established social norms.

byAction Source #

Arguments

:: (Monad m, Enumerable a) 
=> m a

monadic evaluation point

-> (a -> Effable m b)

the function to evaluate

-> Effable m b 

Evaluation of finite-domain function for the daring.

When emitted, the monadic action will be run once for each element, for each inhabitant of the Enumerable type that the action yields.

Comparing with >>=

The signature can be compared to that of monadic bind (>>=):

byAction :: (Enumerable a, e b ~ Effable m b) =>
            Monad m => m a -> (a -> e b) -> e b
(>>=)    :: Monad m => m a -> (a -> m b) -> m b

A possible interpretation of the two signatures is:

  • >>= returns a value-yielding action that may depend on what value some other action yields
  • byAction returns a pure Effable that may depend on what value some action yields

Warning: should only be used with types of a few inhabitants

All outcomes of the action are reified in the internal representation. With IO-like monads, there is no short-circuiting; neither will we be saved by laziness - the full internal representation is likely to be forced at emission time.

Bool has two inhabitants so reifying IO Bool is not expensive.

--- GHCi session ---

λ> effable = byAction (pure True) embed

λ> -- make GHCi report evaluation time and allocated bytes:

λ> :set +s

λ> run print effable
True
(ran for 0.01 secs, allocated 1,014,600 bytes)

An Int is Enumerable but has many inhabitants so reifying IO Int becomes costly in both time and allocations.

--- GHCi session ---

λ> ineffable = byAction (pure (1::Int)) embed

λ> :set +s

λ> run print ineffable
1
(ran for 1.02 million years, allocated 3.06e10 gigabytes)

embedAction Source #

Arguments

:: (Monad m, Enumerable a) 
=> m a

monadic value

-> Effable m a 

Evaluation of finite-domain value for the daring.

When emitted, the monadic action will be run once for each value of the domain.

(The same warning as that for byAction applies.)

Effectuate

run Source #

Arguments

:: Applicative m 
=> (b -> m ())

emitting one b

-> Effable m b 
-> m () 

For each b of an Effable, emit it with the given function, then apply the composed emission wrapper associated with that b, and combine all results.

Laws

Monoid homomorphism
run emit mempty    ==  pure ()
run emit (x <> y)  ==  run emit x *> run emit y
Naturality
run emit (f <$> x)  ==  run (emit . f) x

Examples

These examples use Const [] whose emission effect is to accumulate emitted items into a list.

>>> import Data.Functor.Const (Const (..))
>>> emitConst b = Const [b]
>>> run emitConst (embed 'a' <> embed 'b')
Const "ab"

With emission wrapper:

>>> silence = wrap (\_ -> Const [])
>>> run emitConst (embed 'a' <> (silence $ embed 'b') <> embed 'c')
Const "ac"

data RunWith a Source #

Instances

Instances details
Foldable RunWith Source # 
Instance details

Defined in Effable

Methods

fold :: Monoid m => RunWith m -> m #

foldMap :: Monoid m => (a -> m) -> RunWith a -> m #

foldMap' :: Monoid m => (a -> m) -> RunWith a -> m #

foldr :: (a -> b -> b) -> b -> RunWith a -> b #

foldr' :: (a -> b -> b) -> b -> RunWith a -> b #

foldl :: (b -> a -> b) -> b -> RunWith a -> b #

foldl' :: (b -> a -> b) -> b -> RunWith a -> b #

foldr1 :: (a -> a -> a) -> RunWith a -> a #

foldl1 :: (a -> a -> a) -> RunWith a -> a #

toList :: RunWith a -> [a] #

null :: RunWith a -> Bool #

length :: RunWith a -> Int #

elem :: Eq a => a -> RunWith a -> Bool #

maximum :: Ord a => RunWith a -> a #

minimum :: Ord a => RunWith a -> a #

sum :: Num a => RunWith a -> a #

product :: Num a => RunWith a -> a #

Traversable RunWith Source # 
Instance details

Defined in Effable

Methods

traverse :: Applicative f => (a -> f b) -> RunWith a -> f (RunWith b) #

sequenceA :: Applicative f => RunWith (f a) -> f (RunWith a) #

mapM :: Monad m => (a -> m b) -> RunWith a -> m (RunWith b) #

sequence :: Monad m => RunWith (m a) -> m (RunWith a) #

Functor RunWith Source # 
Instance details

Defined in Effable

Methods

fmap :: (a -> b) -> RunWith a -> RunWith b #

(<$) :: a -> RunWith b -> RunWith a #

runWith Source #

Arguments

:: (b -> m ())

emitting one b

-> Effable m b 
-> RunWith (m ()) 

Create a representation of the individual emission results of an Effable.

Methods of the Foldable and Traversable instances of the result type can be used e.g. to customize how the individual emission results are combined.

sequenceA_ (runWith emit x)  == run emit x

Example

The following uses the foldr method of RunWith's Foldable instance to create a run-like function with a foldr-style API:

>>> :{
  let
    run_foldr
      :: (b -> m ())
      -> (m () -> c -> c)
      -> c
      -> Effable m b
      -> c
    run_foldr emit comb z = foldr comb z . runWith emit
:}