recollections
The splicing module needs these extensions and imports:
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DerivingVia #-}
import GHC.Generics (Generically1(..))
Given an enumeration of things like
data Things
= This
| That
| Something
| Else
| Entirely
deriving (Eq, Ord, Show, Enum, Bounded, Generic)
We can derive a record that will behave like a homogenous zip-list of static size:
mkCollection ''Things
-- data Collection a = Collection
-- { this, that, something, else', entirely :: a
-- }
-- deriving stock (Eq, Show, Generic1, Functor, Foldable, Traversable)
-- deriving Applicative via (Generically1 Collection)
-- Fill the record with its indices
mkIndices ''Things
Nesting
A constructor may wrap the tag type of another collection.
The field then holds that whole collection:
import Some qualified
data Tag
= Something Some.Tag
| Local
deriving (Eq, Ord, Show)
mkCollection ''Tag
-- data Collection a = Collection
-- { something :: Some.Collection a
-- , local :: a
-- }
The nested collection is looked up in the module defining Some.Tag, so the
nested tag and its splices must live together in their own module (and be
exported from it). Every collection generator spliced for the outer tag needs
its counterpart spliced for the nested one. The class instance generators
delegate to the nested instances instead, so mkDistributive and
mkRepresentable need the same instances for Some.Collection.
Stock Enum and Bounded can't be derived for such tags, so there are
generators that number the nested tags in place:
mkBounded ''Tag
mkEnum ''Tag -- needs Bounded Tag, plus Enum and Bounded Some.Tag
-- [minBound .. maxBound] == toList indices
Kmettoverse
With adjunctions and distributive in the dependencies of the splicing package
(recollections itself doesn't depend on them), the classes can be instantiated directly:
{-# LANGUAGE TypeFamilies #-}
import Data.Distributive (Distributive(..))
import Data.Functor.Rep (Representable(..))
mkDistributive ''Things
mkRepresentable ''Things
-- instance Distributive Collection where ...
-- instance Representable Collection where
-- type Rep Collection = Things
-- ...
Representable requires the Distributive instance, so both splices are needed.
The classes are looked up in the splicing module scope, by their plain or module-qualified names.
When the modules are imported under an alias, pass the class names explicitly:
import qualified Data.Distributive as D
import qualified Data.Functor.Rep as R
mkDistributiveFor ''D.Distributive ''Things
mkRepresentableFor ''R.Representable ''Things
Kmettoverse-lite
Without the extra dependencies, the methods can be generated as top-level functions instead.
Those names clash with the class imports above.
-- Distributive
mkDistribute ''Things
-- collect is `distribute . fmap f`
-- Representable
mkIndex ''Things
mkTabulate ''Things
VS
- Vanilla monomorphic records: don't have functor/foldable/traversable/applicative goodies.
- Hand-rolling the functor: be my guest, hand-roll the field enum too, while you're at it.
Map Things a: All fields are mandatory, all keys are known -> lookups are total and O(1). The optional fields are recoverable, with Collection (Maybe a).
Things -> a: The collection can be stored and loaded.
Gotchas
All the constructors of the tag type must be nullary or wrap a single nested
tag type — anything else is a compile-time error, since a partial Collection
would make index partial too.
The generated names (Collection, indices, distribute, index,
tabulate) are fixed, so a tag named e.g. Index will clash with them.
For the same reason, a module with nested collections must be imported
qualified, or its generated names become ambiguous with the outer ones.