graphql-engine-1.0.0: GraphQL API over Postgres
Safe HaskellSafe-Inferred
LanguageHaskell2010

Control.Monad.Circular

Description

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

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

Instances details
MonadError e m => MonadError e (CircularT k v m) Source # 
Instance details

Defined in Control.Monad.Circular

Methods

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 # 
Instance details

Defined in Control.Monad.Circular

Methods

ask :: CircularT k v m r #

local :: (r -> r) -> CircularT k v m a -> CircularT k v m a #

reader :: (r -> a) -> CircularT k v m a #

MonadState s m => MonadState s (CircularT k v m) Source #

Allow code in CircularT to have access to any underlying state capabilities, hiding the fact that CircularT itself is a state monad.

Instance details

Defined in Control.Monad.Circular

Methods

get :: CircularT k v m s #

put :: s -> CircularT k v m () #

state :: (s -> (a, s)) -> CircularT k v m a #

MonadWriter w m => MonadWriter w (CircularT k v m) Source # 
Instance details

Defined in Control.Monad.Circular

Methods

writer :: (a, w) -> CircularT k v m a #

tell :: w -> CircularT k v m () #

listen :: CircularT k v m a -> CircularT k v m (a, w) #

pass :: CircularT k v m (a, w -> w) -> CircularT k v m a #

MonadTrans (CircularT k v) Source # 
Instance details

Defined in Control.Monad.Circular

Methods

lift :: Monad m => m a -> CircularT k v m a #

Monad m => Applicative (CircularT k v m) Source # 
Instance details

Defined in Control.Monad.Circular

Methods

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 # 
Instance details

Defined in Control.Monad.Circular

Methods

fmap :: (a -> b) -> CircularT k v m a -> CircularT k v m b #

(<$) :: a -> CircularT k v m b -> CircularT k v m a #

Monad m => Monad (CircularT k v m) Source # 
Instance details

Defined in Control.Monad.Circular

Methods

(>>=) :: CircularT k v m a -> (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 b #

return :: a -> CircularT k v m a #

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.