Thu 26 Jun 2008
[Edit: re-introduced the domain function]
Recently Andy Gill posted a nice use of data families to memoize a narrow range of values to optimize Conal Elliott's functional linear maps.
I've tweaked Andy's definition slightly below to use a type family for D e instead of a data family, which avoids some of the syntactic noise of the domain function when D e = e, and lets it this be used transparently in some places. This change lacks substance however, and the source code links at the bottom include the code with and without this change.
Since I'm using data and type families, you'll need GHC 6.9.
class Applicative ((:~*) e) => NarrowMemo e where data (:~*) e :: * -> * type D e :: * apply :: (e :~* r) -> D e -> r memo :: (D e -> r) -> (e :~* r) domain :: D e -> e instance NarrowMemo Bool where data Bool :~* a = MemoBool a a type D Bool = Bool apply (MemoBool o1 o2) True = o1 apply (MemoBool o1 o2) False = o2 memo f = MemoBool (f True) (f False) domain = id
We can quickly add in the missing bits for the Bool demo he supplied:
instance Functor ((:~*) Bool) where fmap f (MemoBool a b) = MemoBool (f a) (f b) instance Applicative ((:~*) Bool) where pure x = MemoBool x x MemoBool f g < *> MemoBool a b = MemoBool (f a) (g b)
And we can automatically generate an instance of Monad for (:~*)e if we really want to:
instance NarrowMemo e => Monad ((:~*) e) where return = pure m >>= k = memo $ apply (fmap k m) >>= apply -- in (->)e
And we can drop in a couple more instances to make it interesting. The tensor product:
instance (NarrowMemo a, NarrowMemo b) => NarrowMemo (a,b) where data (a,b) :~* e = MemoBoth (a :~* (b :~* e)) type D (a,b) = (D a, D b) apply (MemoBoth f) = uncurry (apply . apply f) memo f = MemoBoth $ memo $ \a -> memo (f . (,) a) domain = domain *** domain instance (NarrowMemo a, NarrowMemo b) => Functor ((:~*) (a,b)) where fmap f (MemoBoth g) = MemoBoth (fmap (fmap f) g) instance (NarrowMemo a, NarrowMemo b) => Applicative ((:~*) (a,b)) where pure = MemoBoth . pure . pure f < *> a = MemoBoth $ memo $ \da -> memo $ \db -> let e = (da, db) in apply f e (apply a e)
and a disjoint sum, if only so we have some more memo tables to play with.
instance (NarrowMemo a, NarrowMemo b) => NarrowMemo (Either a b) where data Either a b :~* e = MemoEither (a :~* e) (b :~* e) type D (Either a b) = Either (D a) (D b) apply (MemoEither l _) (Left a) = apply l a apply (MemoEither _ r) (Right b) = apply r b memo f = MemoEither (memo (f . Left)) (memo (f . Right)) domain = domain +++ domain instance (NarrowMemo a, NarrowMemo b) => Functor ((:~*) (Either a b)) where fmap f (MemoEither a b) = MemoEither (fmap f a) (fmap f b) instance (NarrowMemo a, NarrowMemo b) => Applicative ((:~*) (Either a b)) where pure f = MemoEither (pure f) (pure f) MemoEither f g < *> MemoEither a b = MemoEither (f < *> a) (g < *> b)
Now, the reason I wanted to play with this was I hacked up a memoizing state-in-context comonad a couple of weeks back using unsafePerformIO and a memo table, but using Andy's trick we can build up a pure memoizing context comonad for smaller or more regular domains.
Recall the state-in-context comonad from category-extras.
class Comonad w => ComonadContext s w | w -> s where getC :: w a -> s modifyC :: (s -> s) -> w a -> a data Context s a = Context (s -> a) s instance ComonadContext s (Context s) where getC (Context _ s) = s modifyC m (Context f c) = f (m c) instance Functor (Context s) where fmap f (Context f' s) = Context (f . f') s instance Copointed (Context s) where extract (Context f a) = f a instance Comonad (Context s) where duplicate (Context f a) = Context (Context f) a
We can modify the definition to replace (->) with the version above of Andy's (:~*) to obtain a narrowly memoized version:
data NarrowContext e a = NarrowContext (e :~* a) (D e) instance NarrowMemo e => Functor (NarrowContext e) where fmap f (NarrowContext t d) = NarrowContext (memo (f . apply t)) d instance NarrowMemo e => Copointed (NarrowContext e) where extract (NarrowContext t d) = apply t d instance NarrowMemo e => Comonad (NarrowContext e) where duplicate (NarrowContext f a) = NarrowContext (memo (NarrowContext f)) a instance NarrowMemo e => ComonadContext (D e) (NarrowContext e a) where getC (NarrowContext _ e) = e modifyC f (NarrowContext g e) = apply g (f e)
Now any computation done in this modified state-in-context comonad is memoized and compositions of those computations are also memoized and built up from the memoized intermediate steps.
We should be able to play similar games with a lot of other (co)monads that use exponentials as well.
For instance (:~*) itself can play the role of a narrowly-memoized anonymous exponential comonad.
instance (NarrowMemo e, Monoid (D e)) => Copointed ((:~*) e) where extract t = apply t mempty instance (NarrowMemo e, Monoid (D e)) => Comonad ((:~*) e) where duplicate f = memo $ m -> memo (apply f . mappend m)
[ Source Code (using data families only) ]
[ Source Code (using data and type families) ]

June 27th, 2008 at 1:24 am
Thanks for picking up the idea!
The expectation is that the range of possible arguments will be smaller than the type would suggest, hence NarrowMemo. For a version that stores *all* possible results, the name Memo would be more appropriate.
Is it the Memoization or the narrowing of possible arguments you are looking for?
Andy
June 27th, 2008 at 6:59 am
I was interested in the narrowing of arguments and the fact that only a potentially smaller basis gets memoized.
The smaller memo-set is still entirely possible with this scheme, even though so far I’ll admit all the examples above memoize all combinations. (D can still reference a different type. It just doesn’t have to.)
I kept both versions since I wasn’t sure entirely where you were going with this.
Waking up this morning, I realized I didn’t have to remove the domain function from the type family version, so I added it back in.