[ANN] recollections: fixed-size representable collections

There is a certain pattern that I happen to reach for quite often:

data Collection a = Collection
  { this, that, something, else', entirely :: a
  }
  deriving stock (Eq, Show, Generic1, Functor, Foldable, Traversable)
  deriving Applicative via (Generically1 Collection) -- this one

That Applicative instance adds so much power for one simple line.

GHC 9.4 brought Generically1 into base, thanks to all the DerivingVia enjoyers out there.
Previously I had to go through distributive+representable, but now it’s almost free.

The only missing thing was the boilerplate to go between the fields and their indices.
The recollections package solves that with some TH:

It was actually even easier to start with the indices:

module Things where

data Things
  = This
  | That
  | Something
  | Else
  | Entirely
  deriving (Eq, Ord, Show, Enum, Bounded, Generic)

And get the Collection above out of it:

mkCollection ''Things
-- Fill the record with its indices:
-- indices :: Collection Things
mkIndices ''Things

The kmettoverse methods can also be derived (and used directly, without the extra deps):

-- Distributive
mkDistribute ''Things
-- Representable
mkIndex ''Things
mkTabulate ''Things
10 Likes

Fwiw I have trouble following the documentation here. Suggestions for how it could be clearer:

  • You show what mkCollection expands to in the readme, put that in the module-level docs too.
  • What does mkIndices expand to? (I think indices :: Collection Things; indices = Collection { this = This, that = That, ... }?) What’s it useful for?
  • What are the type signatures of the generated distribute, index and tabulate functions?
  • For mkDistribute, the generated html for distribute writes $ instead of <$>.

Thanks. The docs are updated: Data.Recollections.TH

It allows reattaching the field indices with the Applicative: (,) <$> indices <*> c. Then using that like indexed-traversable.
Or other silly things like:

sequence_ $ attachDebugLabels <$> fmap show indices <*> textures
1 Like

Rather than needing lots of TH, I’d suggest using the Generic to (at type-level) compute the size of the index type, and then use that along with Enum and Bounded as the basis for everything else. This is the approach I took on my previous work where I also reinvented this wheel (thinking of it more a memoisation problem than knowing about the Kmettoverse representable functors stuff :slight_smile:) and it worked very well, something like:

newtype Table k a = Table (SmallArray a)
  deriving stock (Functor, Foldable)

class (KnownNat (Size k)) => Tabulate k where
  type Size k :: Nat {- prior art https://stackoverflow.com/a/60169722 & finitary & probably others -}

fromFunction :: (Enum k, Bounded k, Tabulate k) => (k -> a) -> Table k a
fromFunction = createSmallArray (fromIntegral (natVal _)) (blah (universe @k))

type instance Index (Table k a) = k
type instance IxValue (Table k a) = a
instance Tabulate k => Ixed (Table k a) where
  ix k = _

Where a collection derived from k as in recollections is basically equivalent to a Table k, and instead of field accessor _something you use lens/optic ix Something. The main awkwardness of making this a package is whether you use lens or optics or microlens or all 3 or multiple packages… or public sublibraries… and so depending on one’s packaging decisions it becomes much ado about very little, or very little about very little :').

How would the Things example look like with this approach?
TBH it’s not obvious what is the gain here over TH.

Im gonna keep manually spelling them out hehehehehehe