@hackage tangle0.1

Heterogenous memoisation monad

tangle

This package implements an abstraction of record construction where each field may depend on other fields. It is a reimplementation of extensible's Tangle refined for more general HKDs.

evalTangleT :: Tangle t m a -- ^ computation
  -> t (Tangle t m) -- ^ collection of tangles
  -> m a

hitch
 :: Monad m
  => (forall h. Lens' (t h) (h a)) -- ^ the lens of the field
  -> Tangle t m a

A computation tangle is a higher-kinded record of Tangle t. Each field may fetch other fields by using hitch; the result is memoised so the computation is performed at most once for each field. Recursive hitch is not supported (becomes an infinite loop).

examples/weight.hs demonstrates a simple program that calculates a body mass index.

First, we define a highed-kinded record of parameters.

data Info h = Info
  { _height :: h Double
  , _mass :: h Double
  , _bmi :: h Double
  , _isHealthy :: h Bool
  } deriving Generic
instance FunctorB Info
instance ApplicativeB Info
makeLenses ''Info

Then describe how to compute each field as a record of TangleTs. Note the _bmi field fetching height and mass.

buildInfo :: Info (TangleT Info IO)
buildInfo = Info
  { _height = liftIO $ prompt "Height(m): "
  , _mass = liftIO $ prompt "Mass(kg): "
  , _bmi = do
    h <- hitch height
    m <- hitch mass
    return $! m / (h * h)
  , _isHealthy = do
    x <- hitch bmi
    pure $ x >= 18 && x < 22
  }

Finally, we can run a computation go with evalTangleT. Behold, it asks for height and mass only once, even though the bmi field is used twice (via isHealthy).

main :: IO ()
main = evalTangleT go buildInfo >>= print where
  go = (,) <$> hitch bmi <*> hitch isHealthy

What are tangles?

Tangle is a memoisation monad for heterogenous collections such as records. If the collection were just a homogenous container, it is equivalent to

type Key = String
type Container = Map.Map Key

newtype Tangle b a = Tangle { unTangle :: ReaderT (Container (Tangle b b)) (State (Container b)) a }
  deriving (Functor, Applicative, Monad)

where Container (Tangle b) is a collection of computations which would yield the final results, and Container b is the cache of intermediate results.

With these in mind, the following function can be defined:

hitch :: Key -> Tangle b b
hitch key = Tangle $ ReaderT $ \env -> state $ \cache -> case Map.lookup key cache of
  -- If the requested key exists in the cache, just return the value
  Just a -> (a, cache)
  Nothing -> case Map.lookup key env of
    Nothing -> error "Not found"
    Just m ->
      -- Compute the result
      let (result, cache') = unTangle m `runReaderT` env `runState` cache
      -- Insert the result to the cache
      in (result, Map.insert key result cache')

Each computation in Container (Tangle b b) may hitch a value from other keys. Since hitch caches its result, subsequent calls just return the same value; that's how Tangle makes dependent computation composable.

In Control.Monad.Tangle, the container type is generalised to any higher-kinded datatypes. HKD allows the container type to be in two forms: t (Tangle t) (collection of computations) and t Maybe (cache of results). Not just it's much more flexible, the ApplicativeB constaint also ensures that the collection and the cache have the same shape.

Application

Tangles can split dependent computations into smaller pieces of code without explicity passing values by function arguments. It implies that it's trivial to add/remove dependencies between computation, because they are automatically handled in the memoisation mechanism. One useful property is that the cache can be reused as the second argument of runTangleFT. If some fields are expensive to calculate and the rest is expensive to serialise, the server application could compute the former, distribute the cache, and then let the clients fill the rest.

See also