Tags

thunk-force category

A thunk-force category consists of

satisfying the following criteria:

  1. ,
  2. is a natural transformation,
  3. , and
  4. .

Intuitively:

  1. After a value has been thunked, thunking it again inside the thunk structure is the same as thunking the thunk structure itself.
  2. Mapping an arrow inside a thunk and then thunking the resulting thunk is the same as thunking the original thunk and then mapping two levels down.
  3. Thunking a value and then forcing that thunk gives back the original value.
  4. Thunking a thunk and then forcing inside the outer thunk structure gives back the original thunk structure.

Equivalently:

class Functor cat l => AbstractKleisli cat l where
  thunk :: cat a (l a)
  force :: cat (l a) a

It turns out that, for any monad , the Kleisli category of can be viewed as an abstract Kleisli category in a canonical way, with , , and .

instance Monad cat m => AbstractKleisli (Kleisli cat m) m where
  -- ignoring the Kleisli newtype wrapper:
  -- thunk :: cat a (m (m a))
  -- force :: cat (m a) (m a)
  thunk = Kleisli (return . return)
  force = id

Now we ask: how else can a Kleisli category be an abstract Kleisli category?

-- cat a (m (f a))
thunk' :: Kleisli a (f a)
-- cat (f a) (m a)
force' :: Kleisli (f a) a

-- Over Hask:
kthunk' :: a -> m (f a)
kforce' :: f a -> m a

In particular, we will focus on thunk-force Kleisli categories over Hask. That is, given a monad m on Hask, we consider functors f on the Kleisli category of m satisfying the thunk-force category laws. To start, we need to be able to pick out such functors.

class Monad m => KleisliFunctor m f where
  kmap :: (a -> m b) -> f a -> m (f b)

A Kleisli functor is subject to the usual functor laws (keeping in mind that it is a functor on a Kleisli category, not on Hask):

If we rephrase the second law in terms of >>=, we get

1. sidenote: kmap

The attentive reader may have noticed something interesting about the type of kmap.

~kmap
KleisliFunctor m f => (a -> m b) -> f a -> m (f b)~
~traverse
(Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)~

2. thunk-force on Kleisli on Hask

class KleisliFunctor m f => ThunkForce f m where
  thunk :: a -> m (f a)
  force :: f a -> m a
  • kmap thunk <=< thunk = thunk <=< thunk
  • thunk <=< kmap f = kmap (kmap f) <=< thunk
  • force <=< thunk = return
  • kmap force <=< thunk = return

Once again, we can rephrase these in terms of >>=, obtaining the following laws:

  • thunk x >>= kmap thunk = thunk x >>= thunk
  • kmap f r >>= thunk = thunk r >>= kmap (kmap f)
  • thunk x >>= force = return x
  • thunk r >>= kmap force = return r

3. what is it good for?

data Cell s a = Thunk (ST s a) | Value a

newtype LazyRef s a = LazyRef (STRef s (Cell s a))

instance KleisliFunctor (ST s) (LazyRef s) where
  kmap f (LazyRef r) = LazyRef <$> newSTRef (Thunk (force r >>= f))

instance ThunkForce (LazyRef s) (ST s) where
  thunk x = LazyRef <$> newSTRef (Value x)
  force (LazyRef r) = readSTRef r >>= \case
    Value x -> return x
    Thunk m -> do
      x <- m
      writeSTRef r (Value x)
      return x

Author: Nicholas Coltharp (mail@heraplem.xyz)

Last modified: 2026-10-08 Thu 10:05

Emacs 31.1 (Org mode 9.8.7)

Validate