Skip to content

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.

To upload designs, you'll need to enable LFS and have an admin enable hashed storage. More information