Safe Haskell | Safe-Inferred |
---|---|
Language | Haskell2010 |
Knot-tying monad transformer for recursive graph building.
Some operations, such as building a graph, are inherently self-recursive; consider the following graph:
a -> b b -> a
To construct in Haskell, we might want to use the following type:
data Node = Node { nodeName :: Text , nodeNeighbours :: [Node] }
To construct our trivial graph, we need a
to know about b
and b
to know
about a
: this is fine as long as we can build them both at the same time:
graph = [nodeA, nodeB] where nodeA = Node "a" [nodeB] nodeB = Node "b" [nodeA]
But this falls apart as soon as building the nodes becomes more complicated; for instance, if it becomes monadic. This causes an infinite recursion:
graph = do a <- buildA b <- buildB pure [a,b] where buildA = do b <- buildB pure $ Node "a" [b] buildB = do a <- buildA pure $ Node "b" [a]
The reason why the non-monadic version works is laziness; and there is a way
to retrieve this laziness in a monadic context: it's what MonadFix
is for.
(https:/wiki.haskell.orgMonadFix)
However, MonadFix
is both powerful and unintuitive; the goal of this module
is to use its power, but to give it a more restricted interface, to make it
easier to use. Using CircularT
, the graph above can be built monadically
like so:
graph = runCircularT do a <- buildA b <- buildB pure [a,b] where buildA = withCircular "a" do b <- buildB pure $ Node "a" [b] buildB = withCircular "b" do a <- buildA pure $ Node "b" [a]
It allows each part of a recursive process to be given a name (the type of which is of the user's choosing), and it automatically breaks cycles. The only caveat is that we cannot violate temporal causality: if we attempt to make a cache-building decision based on the value obtained from the cache, then no amount of laziness can save us:
broken = runCircularT go where go = withCircular () do x <- go pure $ if odd x then 1 else 0
CircularT
is somewhat similar to TardisT
from Control.Monad.Tardis
and
SchemaT
from Hasura.GraphQL.Parser.Monad
, but simpler than both.
Synopsis
- data CircularT k v m a
- runCircularT :: (Hashable k, MonadFix m) => CircularT k v m a -> m a
- withCircular :: (Hashable k, MonadFix m) => k -> CircularT k v m v -> CircularT k v m v
Documentation
data CircularT k v m a Source #
CircularT is implemented as a state monad containing a lazy HashMap.
We use this state to both determine wether we have already encountered a
given key and to track the associated result. We use laziness and MonadFix to
tie the knot for us (see withCircular
).
- type
k
is the type of cache key, to which a given action is associated. - type
v
is the values we wish to cache in our process. - type
m
is the underlying monad on which this transformer operates. - type
a
is the result of the computation
Instances
MonadError e m => MonadError e (CircularT k v m) Source # | |
Defined in Control.Monad.Circular throwError :: e -> CircularT k v m a # catchError :: CircularT k v m a -> (e -> CircularT k v m a) -> CircularT k v m a # | |
MonadReader r m => MonadReader r (CircularT k v m) Source # | |
MonadState s m => MonadState s (CircularT k v m) Source # | Allow code in |
MonadWriter w m => MonadWriter w (CircularT k v m) Source # | |
MonadTrans (CircularT k v) Source # | |
Defined in Control.Monad.Circular | |
Monad m => Applicative (CircularT k v m) Source # | |
Defined in Control.Monad.Circular pure :: a -> CircularT k v m a # (<*>) :: CircularT k v m (a -> b) -> CircularT k v m a -> CircularT k v m b # liftA2 :: (a -> b -> c) -> CircularT k v m a -> CircularT k v m b -> CircularT k v m c # (*>) :: CircularT k v m a -> CircularT k v m b -> CircularT k v m b # (<*) :: CircularT k v m a -> CircularT k v m b -> CircularT k v m a # | |
Functor m => Functor (CircularT k v m) Source # | |
Monad m => Monad (CircularT k v m) Source # | |
runCircularT :: (Hashable k, MonadFix m) => CircularT k v m a -> m a Source #
Runs a computation in CircularT
.
withCircular :: (Hashable k, MonadFix m) => k -> CircularT k v m v -> CircularT k v m v Source #
Cache a computation under a given key.
For a given key k
, and a computation in CircularT
that yields a value of
type v
, return an action that builds said value v
but that prevents
cycles by looking into and populating a stateful cache.