Per-use strict dictionaries
Motivation
A dictionary with more than one element is implemented as a struct, and each of its elements is actually an accessor. The -fdicts-strict
option changes this, so that (for instance) a function which depends on a dictionary deconstructs it like a case statement would, but it applies to an entire compilation unit. It would be nice if it could be applied on a case-by-case basis without having to cut related functions into different files depending on whether they use strict dicts or not.
An example of where this would be helpful is in Edward Kmett's folds library. A common pattern is something like
import Data.Fold
-- This is turned into something like foldMap with
-- Profunctor functions like dimap.
toFoldM :: Monoid m => M m m
toFoldM = M id id mappend mempty
We know we have a Monoid
dictionary at that point, yet what is stored in the M
value is something along the lines of dict_mappend monoidDict
and dict_mempty monoidDict
, which is unnecessary indirection. -fdicts-strict
would make this an actual deconstruction, but it would apply to the whole file, whether you need it or not.
Proposal
There should be a pragma for forcing, or trying to force at least, a dictionary to be accessed strictly. If the pragma is called STRICTDICT
, then the previous example would become
import Data.Fold
toFoldM :: {-# STRICTDICT #-} Monoid m => M m m
toFoldM = M id id mappend mempty
And the core would become something like
toFoldM :: Monoid m -> M m m
toFoldM monoidDict = case monoidDict of
Monoid _semigroup memptyHere mappendHere _mconcat -> M id id mappendHere memptyHere
There would also be a LAZYDICT
pragma for turning off strict dictionaries at a declaration site, and perhaps for parent dictionaries (as described below).
STRICTDICT
could also be used in GADTs, where it would be like the !
modifier on a dictionary:
data IsOrd a where
IsOrd :: {-# STRICTDICT #-} Ord a => IsOrd a
It could also be used in instance declarations:
instance {-# STRICTDICT #-} Num a => Monoid (Sum a) where
mempty = Sum (fromInteger 0)
mappend = (coerce :: (a -> a -> a) -> Sum a -> Sum a -> Sum a) (+)
This would produce core which would resemble:
monoidDictSum :: Num a -> Monoid (Sum a)
monoidDictSum numDict = case numDict of
Num { fromInteger, (+) } = let
semigroupHere = semigroupDictSum numDict
memptyHere = Sum (fromInteger 0)
mappendHere = coerce (+)
mconcatHere = mconcatDefault monoidHere
monoidHere = Monoid semigroupHere memptyHere mappendHere mconcatHere
in monoidHere
It could also be used in class declarations, and would even unbox the parent dictionary if it was small enough:
class {-# STRICTDICT #-} Eq a => Ord a where
...
This would cause copies of (==)
and (/=)
to be included in the Ord
dictionary.
However, it shouldn't be allowed inside a rank N type:
-- Not allowed
newtype SBazaar a b t = SBazaar { runSBazaar :: forall f. {-# STRICTDICT #-} Applicative f => (a -> f b) -> f t }
Some of you may have noted that, at some point soon, mappend
will be removed from the Monoid
class, and the above example would use <>
from Semigroup
instead. So the question becomes, is the parent of a strictly accessed dictionary also accessed strictly? The principle of least surprise seems to say that it should, but it might possibly lead to an infinite regress.
If you want to load a dictionary strictly, but one of its parents lazily, you could do something like this:
traversingBar :: ({-# STRICTDICT #-} Applicative f, {-# LAZYDICT #-} Functor f) => (Foo -> f Foo) -> Bar -> f Bar
traversingBar = ...
However, if discussion leads to the parents not being used strictly, then you would put STRICTDICT
on both of those constraints in the declaration.
You could even hypothetically import fixed dictionaries strictly or lazily, like {-# STRICTDICT #-} Monoid (Sum Int)
This would trigger a warning without the pragma, but it probabaly shouldn't with it,, since by using it you presumably know what you're asking.
STRICTDICT
and LAZYDICT
shouldn't give a warning on dictionaries with one element, because it's possible for them to get different amounts during refactoring. However, it wouldn't force the dictionary itself, just like unwrapping a newtype
doesn't.
STRICTDICT
and LAZYDICT
should give a warning on the builtin zero-size constraints ~
, ~~
, and Coercible
, because that evidence is (a) not actually passed in as an argument, and (b) can't be acted on lazily anyway. However, even a declaration like {-# LAZYDICT #-} (~) a b
shouldn't alter the semantics of the program.