Search code examples
haskellhaskell-lenstraversable

Is it possible to get all contexts of a Traversable lazily?


lens offers holesOf, which is a somewhat more general and powerful version of this hypothetical function:

holesList :: Traversable t
          => t a -> [(a, a -> t a)]

Given a container, holesList produces a list of elements of the container along with functions for replacing those elements.

The type of holesList, like that of the real holesOf, fails to capture the fact that the number of pairs produced will equal the number of elements of the container. A much more beautiful type, therefore, would be

holes :: Traversable t
      => t a -> t (a, a -> t a)

We could implement holes by using holesList to make a list and then traversing in State to slurp the elements back in. But this is unsatisfactory for two reasons, one of which has practical consequences:

  1. The slurping code will have an unreachable error call to handle the case where the list runs empty before the traversal is complete. This is disgusting, but probably doesn't matter much to someone using the function.

  2. Containers that extend infinitely to the left, or that bottom out on the left, won't work at all. Containers that extend very far to the left will be very inefficient to handle.

I'm wondering if there's any way around these problems. It's quite possible to capture the shape of the traversal using something like Magma in lens:

data FT a r where
  Pure :: r -> FT a r
  Single :: a -> FT a a
  Map :: (r -> s) -> FT a r -> FT a s
  Ap :: FT a (r -> s) -> FT a r -> FT a s

instance Functor (FT a) where
  fmap = Map
instance Applicative (FT a) where
  pure = Pure
  (<*>) = Ap

runFT :: FT a t -> t
runFT (Pure t) = t
runFT (Single a) = a
runFT (Map f x) = f (runFT x)
runFT (Ap fs xs) = runFT fs (runFT xs)

Now we have

runFT . traverse Single = id

traverse Single makes a tree full of elements along with the function applications needed to build them into a container. If we replace an element in the tree, we can runFT the result to get a container with that element replaced. Unfortunately, I am stuck: I don't know what the next step might look like.


Vague thoughts: adding another type parameter might help change element types. The Magma type does something like this, and it goes back at least as far as Zemyla's comment on Van Laarhoven's blog post about FunList.


Solution

  • Your existing solution calls runMag once for every branch in the tree defined by Ap constructors.

    I haven't profiled anything, but as runMag is itself recursive, this might slow things down in a large tree.

    An alternative would be to tie the knot so you're only (in effect) calling runMag once for the entire tree:

    data Mag a b c where
      One :: a -> Mag a b b
      Pure :: c -> Mag a b c
      Ap :: Mag a b (c -> d) -> Mag a b c -> Mag a b d
    
    instance Functor (Mag a b) where
      fmap = Ap . Pure
    
    instance Applicative (Mag a b) where
      pure = Pure
      (<*>) = Ap
    
    holes :: forall t a. Traversable t => t a -> t (a, a -> t a)
    holes = \t -> 
        let m :: Mag a b (t b)
            m = traverse One t 
        in fst $ go id m m
      where
        go :: (x -> y)
           -> Mag a (a, a -> y) z
           -> Mag a a x
           -> (z, x)
        go f (One a)    (One _)    = ((a, f), a)
        go _ (Pure z)   (Pure x)   = (z, x)
        go f (Ap mg mi) (Ap mh mj) = 
          let ~(g, h) = go (f . ($j)) mg mh
              ~(i, j) = go (f .   h ) mi mj
          in (g i, h j)
        go _ _ _ = error "only called with same value twice, constructors must match"