packages feed

recollections 0.1.0.0 → 0.1.1.0

raw patch · 24 files changed

+1151/−80 lines, 24 filesdep +adjunctionsdep +distributivedep +tasty-benchPVP ok

version bump matches the API change (PVP)

Dependencies added: adjunctions, distributive, tasty-bench

API changes (from Hackage documentation)

+ Data.Recollections.TH: mkBounded :: Name -> Q [Dec]
+ Data.Recollections.TH: mkDistributive :: Name -> Q [Dec]
+ Data.Recollections.TH: mkDistributiveFor :: Name -> Name -> Q [Dec]
+ Data.Recollections.TH: mkEnum :: Name -> Q [Dec]
+ Data.Recollections.TH: mkRepresentable :: Name -> Q [Dec]
+ Data.Recollections.TH: mkRepresentableFor :: Name -> Name -> Q [Dec]

Files

CHANGELOG.md view
@@ -6,6 +6,14 @@ and this project adheres to the [Haskell Package Versioning Policy](https://pvp.haskell.org/). +## 0.1.1.0 - 2026-10-04++- Added `mkDistributive` and `mkRepresentable` instance generators.+  With `mkDistributiveFor` and `mkRepresentableFor` variants for aliased imports.+- Added nested collections.+  * A tag constructor may wrap the tag type of a collection from another module.+  * `mkBounded` and `mkEnum` generate tag instances that work with nested tags.+ ## 0.1.0.0 - 2026-07-26  Initial release.
README.md view
@@ -37,12 +37,86 @@ mkIndices ''Things ``` +## Nesting++A constructor may wrap the tag type of another collection.+The field then holds that whole collection:++```haskell+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:++```haskell+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:++```haskell+{-# 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:++```haskell+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.+ ```haskell -- Distributive mkDistribute ''Things--- collect is +-- collect is `distribute . fmap f`  -- Representable mkIndex ''Things@@ -58,8 +132,11 @@  ## Gotchas -All the constructors of the tag type must be nullary — anything else is a-compile-time error, since a partial `Collection` would make `index` partial too.+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.
+ bench/Bench.hs view
@@ -0,0 +1,35 @@+module Main (main) where++import Breadth.B4 qualified as Breadth4+import Breadth.B16 qualified as Breadth16+import Breadth.B64 qualified as Breadth64+import Depth.D1 qualified as Depth1+import Depth.D2 qualified as Depth2+import Depth.D4 qualified as Depth4+import Depth.D8 qualified as Depth8+import Data.Foldable (foldl', toList)+import Test.Tasty.Bench++main :: IO ()+main = defaultMain+  [ bgroup "breadth"+      [ bgroup "4" $ cases Breadth4.indices+      , bgroup "16" $ cases Breadth16.indices+      , bgroup "64" $ cases Breadth64.indices+      ]+  , bgroup "depth"+      [ bgroup "1" $ cases Depth1.indices+      , bgroup "2" $ cases Depth2.indices+      , bgroup "4" $ cases Depth4.indices+      , bgroup "8" $ cases Depth8.indices+      ]+  ]++cases :: forall f t. (Foldable f, Enum t) => f t -> [Benchmark]+cases collection =+  [ bench "fromEnum" $ whnf (foldl' (\acc t -> acc + fromEnum t) 0) tags+  , bench "toEnum" $ whnf (foldl' (\acc i -> toEnum @t i `seq` acc + 1) (0 :: Int)) ints+  ]+  where+    tags = toList collection+    ints = [0 .. length tags - 1]
+ bench/Breadth/B16.hs view
@@ -0,0 +1,35 @@+{-# LANGUAGE TemplateHaskell #-}++module Breadth.B16+  ( Tag(..)+  , Collection(..)+  , indices+  ) where++import Data.Recollections.TH+import GHC.Generics+import Leaf qualified++data Tag+  = N0 Leaf.Tag+  | N1 Leaf.Tag+  | N2 Leaf.Tag+  | N3 Leaf.Tag+  | N4 Leaf.Tag+  | N5 Leaf.Tag+  | N6 Leaf.Tag+  | N7 Leaf.Tag+  | N8 Leaf.Tag+  | N9 Leaf.Tag+  | N10 Leaf.Tag+  | N11 Leaf.Tag+  | N12 Leaf.Tag+  | N13 Leaf.Tag+  | N14 Leaf.Tag+  | N15 Leaf.Tag+  | End++mkCollection ''Tag+mkIndices ''Tag+mkBounded ''Tag+mkEnum ''Tag
+ bench/Breadth/B4.hs view
@@ -0,0 +1,23 @@+{-# LANGUAGE TemplateHaskell #-}++module Breadth.B4+  ( Tag(..)+  , Collection(..)+  , indices+  ) where++import Data.Recollections.TH+import GHC.Generics+import Leaf qualified++data Tag+  = N0 Leaf.Tag+  | N1 Leaf.Tag+  | N2 Leaf.Tag+  | N3 Leaf.Tag+  | End++mkCollection ''Tag+mkIndices ''Tag+mkBounded ''Tag+mkEnum ''Tag
+ bench/Breadth/B64.hs view
@@ -0,0 +1,83 @@+{-# LANGUAGE TemplateHaskell #-}++module Breadth.B64+  ( Tag(..)+  , Collection(..)+  , indices+  ) where++import Data.Recollections.TH+import GHC.Generics+import Leaf qualified++data Tag+  = N0 Leaf.Tag+  | N1 Leaf.Tag+  | N2 Leaf.Tag+  | N3 Leaf.Tag+  | N4 Leaf.Tag+  | N5 Leaf.Tag+  | N6 Leaf.Tag+  | N7 Leaf.Tag+  | N8 Leaf.Tag+  | N9 Leaf.Tag+  | N10 Leaf.Tag+  | N11 Leaf.Tag+  | N12 Leaf.Tag+  | N13 Leaf.Tag+  | N14 Leaf.Tag+  | N15 Leaf.Tag+  | N16 Leaf.Tag+  | N17 Leaf.Tag+  | N18 Leaf.Tag+  | N19 Leaf.Tag+  | N20 Leaf.Tag+  | N21 Leaf.Tag+  | N22 Leaf.Tag+  | N23 Leaf.Tag+  | N24 Leaf.Tag+  | N25 Leaf.Tag+  | N26 Leaf.Tag+  | N27 Leaf.Tag+  | N28 Leaf.Tag+  | N29 Leaf.Tag+  | N30 Leaf.Tag+  | N31 Leaf.Tag+  | N32 Leaf.Tag+  | N33 Leaf.Tag+  | N34 Leaf.Tag+  | N35 Leaf.Tag+  | N36 Leaf.Tag+  | N37 Leaf.Tag+  | N38 Leaf.Tag+  | N39 Leaf.Tag+  | N40 Leaf.Tag+  | N41 Leaf.Tag+  | N42 Leaf.Tag+  | N43 Leaf.Tag+  | N44 Leaf.Tag+  | N45 Leaf.Tag+  | N46 Leaf.Tag+  | N47 Leaf.Tag+  | N48 Leaf.Tag+  | N49 Leaf.Tag+  | N50 Leaf.Tag+  | N51 Leaf.Tag+  | N52 Leaf.Tag+  | N53 Leaf.Tag+  | N54 Leaf.Tag+  | N55 Leaf.Tag+  | N56 Leaf.Tag+  | N57 Leaf.Tag+  | N58 Leaf.Tag+  | N59 Leaf.Tag+  | N60 Leaf.Tag+  | N61 Leaf.Tag+  | N62 Leaf.Tag+  | N63 Leaf.Tag+  | End++mkCollection ''Tag+mkIndices ''Tag+mkBounded ''Tag+mkEnum ''Tag
+ bench/Depth/D1.hs view
@@ -0,0 +1,21 @@+{-# LANGUAGE TemplateHaskell #-}++module Depth.D1+  ( Tag(..)+  , Collection(..)+  , indices+  ) where++import Data.Recollections.TH+import GHC.Generics+import Leaf qualified as Lower++data Tag+  = First+  | Down Lower.Tag+  | Last++mkCollection ''Tag+mkIndices ''Tag+mkBounded ''Tag+mkEnum ''Tag
+ bench/Depth/D2.hs view
@@ -0,0 +1,21 @@+{-# LANGUAGE TemplateHaskell #-}++module Depth.D2+  ( Tag(..)+  , Collection(..)+  , indices+  ) where++import Data.Recollections.TH+import GHC.Generics+import Depth.D1 qualified as Lower++data Tag+  = First+  | Down Lower.Tag+  | Last++mkCollection ''Tag+mkIndices ''Tag+mkBounded ''Tag+mkEnum ''Tag
+ bench/Depth/D3.hs view
@@ -0,0 +1,21 @@+{-# LANGUAGE TemplateHaskell #-}++module Depth.D3+  ( Tag(..)+  , Collection(..)+  , indices+  ) where++import Data.Recollections.TH+import GHC.Generics+import Depth.D2 qualified as Lower++data Tag+  = First+  | Down Lower.Tag+  | Last++mkCollection ''Tag+mkIndices ''Tag+mkBounded ''Tag+mkEnum ''Tag
+ bench/Depth/D4.hs view
@@ -0,0 +1,21 @@+{-# LANGUAGE TemplateHaskell #-}++module Depth.D4+  ( Tag(..)+  , Collection(..)+  , indices+  ) where++import Data.Recollections.TH+import GHC.Generics+import Depth.D3 qualified as Lower++data Tag+  = First+  | Down Lower.Tag+  | Last++mkCollection ''Tag+mkIndices ''Tag+mkBounded ''Tag+mkEnum ''Tag
+ bench/Depth/D5.hs view
@@ -0,0 +1,21 @@+{-# LANGUAGE TemplateHaskell #-}++module Depth.D5+  ( Tag(..)+  , Collection(..)+  , indices+  ) where++import Data.Recollections.TH+import GHC.Generics+import Depth.D4 qualified as Lower++data Tag+  = First+  | Down Lower.Tag+  | Last++mkCollection ''Tag+mkIndices ''Tag+mkBounded ''Tag+mkEnum ''Tag
+ bench/Depth/D6.hs view
@@ -0,0 +1,21 @@+{-# LANGUAGE TemplateHaskell #-}++module Depth.D6+  ( Tag(..)+  , Collection(..)+  , indices+  ) where++import Data.Recollections.TH+import GHC.Generics+import Depth.D5 qualified as Lower++data Tag+  = First+  | Down Lower.Tag+  | Last++mkCollection ''Tag+mkIndices ''Tag+mkBounded ''Tag+mkEnum ''Tag
+ bench/Depth/D7.hs view
@@ -0,0 +1,21 @@+{-# LANGUAGE TemplateHaskell #-}++module Depth.D7+  ( Tag(..)+  , Collection(..)+  , indices+  ) where++import Data.Recollections.TH+import GHC.Generics+import Depth.D6 qualified as Lower++data Tag+  = First+  | Down Lower.Tag+  | Last++mkCollection ''Tag+mkIndices ''Tag+mkBounded ''Tag+mkEnum ''Tag
+ bench/Depth/D8.hs view
@@ -0,0 +1,21 @@+{-# LANGUAGE TemplateHaskell #-}++module Depth.D8+  ( Tag(..)+  , Collection(..)+  , indices+  ) where++import Data.Recollections.TH+import GHC.Generics+import Depth.D7 qualified as Lower++data Tag+  = First+  | Down Lower.Tag+  | Last++mkCollection ''Tag+mkIndices ''Tag+mkBounded ''Tag+mkEnum ''Tag
+ bench/Leaf.hs view
@@ -0,0 +1,17 @@+{-# LANGUAGE TemplateHaskell #-}++module Leaf+  ( Tag(..)+  , Collection(..)+  , indices+  ) where++import Data.Recollections.TH+import GHC.Generics+++data Tag = A | B | C | D+  deriving (Enum, Bounded)++mkCollection ''Tag+mkIndices ''Tag
recollections.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           recollections-version:        0.1.0.0+version:        0.1.1.0 synopsis:       Fixed-size representable (Zippy Applicative) collections. category:       Data Structures author:         IC Rainbow@@ -44,10 +44,40 @@     , template-haskell >=2.20   default-language: GHC2021 +test-suite recollections-kmettoverse+  type: exitcode-stdio-1.0+  main-is: Kmettoverse.hs+  other-modules:+      Aliased+      Nested.Inner+      Nested.Outer+      Paths_recollections+  autogen-modules:+      Paths_recollections+  hs-source-dirs:+      test-kmettoverse+  default-extensions:+      StrictData+      LambdaCase+      ApplicativeDo+      BlockArguments+      DerivingStrategies+      DerivingVia+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      adjunctions+    , base >=4.18 && <5+    , distributive+    , recollections+    , template-haskell >=2.20+  default-language: GHC2021+ test-suite recollections-test   type: exitcode-stdio-1.0   main-is: Spec.hs   other-modules:+      Nested.Inner+      Nested.Outer       Paths_recollections   autogen-modules:       Paths_recollections@@ -64,5 +94,41 @@   build-depends:       base >=4.18 && <5     , recollections+    , template-haskell >=2.20+  default-language: GHC2021++benchmark recollections-bench+  type: exitcode-stdio-1.0+  main-is: Bench.hs+  other-modules:+      Breadth.B16+      Breadth.B4+      Breadth.B64+      Depth.D1+      Depth.D2+      Depth.D3+      Depth.D4+      Depth.D5+      Depth.D6+      Depth.D7+      Depth.D8+      Leaf+      Paths_recollections+  autogen-modules:+      Paths_recollections+  hs-source-dirs:+      bench+  default-extensions:+      StrictData+      LambdaCase+      ApplicativeDo+      BlockArguments+      DerivingStrategies+      DerivingVia+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -fproc-alignment=64+  build-depends:+      base >=4.18 && <5+    , recollections+    , tasty-bench     , template-haskell >=2.20   default-language: GHC2021
src/Data/Recollections/TH.hs view
@@ -2,31 +2,79 @@ {-# LANGUAGE TemplateHaskellQuotes #-}  module Data.Recollections.TH-  ( mkCollection+  (  -- * Collections generator+    mkCollection   , mkIndices +    -- * Distributive+    -- $distributive+  , mkDistributive+  , mkDistributiveFor   , mkDistribute +    -- * Representable+    -- $representable+  , mkRepresentable+  , mkRepresentableFor   , mkIndex   , mkTabulate++    -- * Tag instances+    -- $tagInstances+  , mkBounded+  , mkEnum   ) where  import Data.Char import Data.Foldable import Data.Traversable+import GHC.Enum (boundedEnumFrom, boundedEnumFromThen) import GHC.Generics (Generic, Generic1, Generically1) import Language.Haskell.TH+import Language.Haskell.TH.Syntax (mkNameG_tc, mkNameG_v) +{- | Generate a @Collection a@ type from a enum-like type.++Every constructor is represented by a field.+Reserved words like @type@ get a @'@ suffix.'++> data Things = This | That+>   deriving (Eq, Ord, Show, Enum, Bounded)+>+> mkCollection ''Things+>+> -- resulting splice+> data Collection a = Collection { this, that :: a }+>   deriving (Eq, Show, Generic, Generic1, Functor, Foldable, Traversable)+>   deriving Applicative via (Generically1 Collection)++A constructor may wrap the tag type of another collection, generated in the module that defines that tag.+The field then holds that whole collection:++> import Some qualified+>+> data Tag = Something Some.Tag | Local+>+> mkCollection ''Tag+>+> -- resulting splice+> data Collection a = Collection { something :: Some.Collection a, local :: a }++Every other collection generator applied to a nested tag requires the same generator in the nested module.+The instance generators call the nested instances instead, so the nested module needs the same instance generators.+-} mkCollection :: Name -> Q [Dec] mkCollection tags = do   let nothingsBanger = bang noSourceUnpackedness noSourceStrictness-  let collectionName = mkName "Collection"    names <- tagNames tags-  let-    fields = do-      name <- names-      pure $ varBangType (mkName $ fieldNameOf name) $ (bangType nothingsBanger) (varT (mkName "a"))+  fields <- for names \tag -> do+    fieldType <- case tag of+      Leaf _ -> pure $ varT (mkName "a")+      Nested _ sub -> do+        subCollection <- siblingOf mkNameG_tc "Collection" sub+        pure $ conT subCollection `appT` varT (mkName "a")+    pure $ varBangType (mkName . fieldNameOf $ tagName tag) $ bangType nothingsBanger fieldType   let constr = recC collectionName fields   let     derivs =@@ -57,134 +105,439 @@ collectionTyVar = PlainTV (mkName "a") () #endif +{- | Generate a value filled with the indices matching the fields.++> indices :: Collection Things+> indices = Collection { this = This, that = That }++This is useful with the Applicative instance to provide indexed operations:++> indexed c :: Collection a -> Collection (Things, a)+> indexed c = (,) <$> indices <*> c+-} mkIndices :: Name -> Q [Dec] mkIndices tags = do-  let collectionName = mkName "Collection"   let indicesName = mkName "indices"   names <- tagNames tags   sig <- sigD indicesName $ conT collectionName `appT` conT tags-  let body = foldl' appE (conE collectionName) (map (conE . mkName) names)+  fields <- for names \case+    Leaf name -> pure . conE $ mkName name+    Nested name sub -> do+      subIndices <- siblingOf mkNameG_v "indices" sub+      pure $ appE (appE (varE 'fmap) (conE $ mkName name)) (varE subIndices)+  let body = foldl' appE (conE collectionName) fields   fun <- funD indicesName [ clause [] (normalB body) [] ]   pure [sig, fun]  -- * Distributive -{- |-@-distribute f = Collection-  { this      = this      <$> f-  , that      = that      <$> f-  , something = something <$> f-  , else'     = else'     <$> f-  , entirely  = entirely  <$> f-  }-@+{- $distributive++<https://hackage-content.haskell.org/package/distributive-0.6.3/docs/Data-Distributive.html>++The instance generators need the @distributive@ package as a dependency of the splicing package,+not of @recollections@.+The class is looked up in the splicing module scope, so it has to be imported there. -}++{- | Generate a Distributive instance.++The splicing module must import "Data.Distributive", unqualified or qualified without an alias.+Use 'mkDistributiveFor' when the module is imported under an alias.++> instance Distributive Collection where+>   distribute f = Collection+>     { this = this <$> f+>     , that = that <$> f+>     }+-}+mkDistributive :: Name -> Q [Dec]+mkDistributive tags = do+  distributive <- lookupClass "Data.Distributive" "Distributive"+  mkDistributiveFor distributive tags++{- | Generate a Distributive instance for the explicitly provided class name.++> import qualified Data.Distributive as D+>+> mkDistributiveFor ''D.Distributive ''Things+-}+mkDistributiveFor :: Name -> Name -> Q [Dec]+mkDistributiveFor distributive tags = do+  names <- tagNames tags+  distributeName <- classMember mkNameG_v distributive "distribute"+  pure <$> instanceD (cxt []) (conT distributive `appT` conT collectionName)+    [ funD distributeName [distributeClause (const $ pure distributeName) names]+    , inlineP distributeName+    ]++{- | Generate a dual of sequenceA.++Doesn't need the @distributive@ package, but clashes with the "Data.Distributive" import.++> distribute :: Functor f => f (Collection a) -> Collection (f a)+> distribute f = Collection+>   { this      = this      <$> f+>   , that      = that      <$> f+>   , something = something <$> f+>   , else'     = else'     <$> f+>   , entirely  = entirely  <$> f+>   }+-} mkDistribute :: Name -> Q [Dec] mkDistribute tags = do-  let collectionName = mkName "Collection"   let distributeName = mkName "distribute"   names <- tagNames tags   let f = mkName "f"   let a = mkName "a"-  {- The binder must not shadow any field selector the body refers to by-     'mkName' (those are resolved lexically, by occurrence name). Field names-     always start with a lowercase letter, so a leading underscore is safe;-     plain @f@ would break for a tag named @F@. -}-  arg <- newName "_f"   sig <- sigD distributeName $     forallT [] (cxt [conT ''Functor `appT` varT f]) $       appT (appT arrowT (varT f `appT` (conT collectionName `appT` (varT a)))) $         (conT collectionName `appT` (varT f `appT` varT a))-  let-    body =-      foldl' appE (conE collectionName) do-        name <- names-        let fieldSelector = varE . mkName $ fieldNameOf name-        pure $ appE (varE 'fmap) fieldSelector `appE` varE arg-  fun <- funD distributeName-    [ clause [varP arg] (normalB body) []-    ]+  fun <- funD distributeName [distributeClause (siblingOf mkNameG_v "distribute") names]   inl <- inlineP distributeName   pure [sig, fun, inl] +distributeClause :: (Name -> Q Name) -> [Tag] -> Q Clause+distributeClause nested names = do+  {- The binder must not shadow any field selector the body refers to by+     'mkName' (those are resolved lexically, by occurrence name). Field names+     always start with a lowercase letter, so a leading underscore is safe;+     plain @f@ would break for a tag named @F@. -}+  arg <- newName "_f"+  fields <- for names \tag -> do+    let projected = appE (varE 'fmap) (varE . mkName . fieldNameOf $ tagName tag) `appE` varE arg+    case tag of+      Leaf _ -> pure projected+      Nested _ sub -> do+        subDistribute <- nested sub+        pure $ appE (varE subDistribute) projected+  let body = foldl' appE (conE collectionName) fields+  clause [varP arg] (normalB body) []+ -- * Representable -{- |-@-index c = \case This -> this c; That -> that c; …-@+{- $representable++<https://hackage-content.haskell.org/package/adjunctions-4.4.4/docs/Data-Functor-Rep.html>++The instance generators need the @adjunctions@ package as a dependency of the splicing package,+not of @recollections@.+The class is looked up in the splicing module scope, so it has to be imported there.+The splicing module also needs @TypeFamilies@ for the @Rep@ instance. -}++{- | Generate a Representable instance.++The splicing module must import "Data.Functor.Rep", unqualified or qualified without an alias.+Use 'mkRepresentableFor' when the module is imported under an alias.++Representable requires a Distributive instance, see 'mkDistributive'.++> instance Representable Collection where+>   type Rep Collection = Things+>   index c = \case This -> this c; That -> that c+>   tabulate k = Collection { this = k This, that = k That }+-}+mkRepresentable :: Name -> Q [Dec]+mkRepresentable tags = do+  representable <- lookupClass "Data.Functor.Rep" "Representable"+  mkRepresentableFor representable tags++{- | Generate a Representable instance for the explicitly provided class name.++> import qualified Data.Functor.Rep as R+>+> mkRepresentableFor ''R.Representable ''Things+-}+mkRepresentableFor :: Name -> Name -> Q [Dec]+mkRepresentableFor representable tags = do+  names <- tagNames tags+  repName <- classMember mkNameG_tc representable "Rep"+  indexName <- classMember mkNameG_v representable "index"+  tabulateName <- classMember mkNameG_v representable "tabulate"+  pure <$> instanceD (cxt []) (conT representable `appT` conT collectionName)+    [ tySynInstD $ tySynEqn Nothing (conT repName `appT` conT collectionName) (conT tags)+    , funD indexName [indexClause (const $ pure indexName) names]+    , inlineP indexName+    , funD tabulateName [tabulateClause (const $ pure tabulateName) names]+    , inlineP tabulateName+    ]++{- | Read a collection field using an index value.++Doesn't need the @adjunctions@ package, but clashes with the "Data.Functor.Rep" import.++> index :: Collection a -> Things -> a+> index c = \case This -> this c; That -> that c; …+-} mkIndex :: Name -> Q [Dec] mkIndex tags = do-  let collectionName = mkName "Collection"   let indexName = mkName "index"   names <- tagNames tags   let a = mkName "a"   sig <- sigD indexName $     appT (appT arrowT (conT collectionName `appT` varT a)) $       appT (appT arrowT (conT tags)) (varT a)-  bounds <- for names \name ->-    (,) name <$> newName (fieldNameOf name)-  let-    body = lamCaseE do-      (name, bound) <- bounds-      pure $ match (conP (mkName name) []) (normalB $ varE bound) []+  fun <- funD indexName [indexClause (siblingOf mkNameG_v "index") names]+  inl <- inlineP indexName+  pure [sig, fun, inl] -  fun <- funD indexName-    [ clause-        [ conP collectionName $ map (varP . snd) bounds-        ]-        (normalB body)+indexClause :: (Name -> Q Name) -> [Tag] -> Q Clause+indexClause nested names = do+  bounds <- for names \tag ->+    (,) tag <$> newName (fieldNameOf $ tagName tag)+  matches <- for bounds \case+    (Leaf name, bound) ->+      pure $ match (conP (mkName name) []) (normalB $ varE bound) []+    (Nested name sub, bound) -> do+      subIndex <- nested sub+      subTag <- newName "t"+      pure $ match+        (conP (mkName name) [varP subTag])+        (normalB $ varE subIndex `appE` varE bound `appE` varE subTag)         []+  let body = lamCaseE matches+  clause+    [ conP collectionName $ map (varP . snd) bounds     ]-  inl <- inlineP indexName-  pure [sig, fun, inl]+    (normalB body)+    [] -{- |-@-tabulate k = Collection (k This) (k That) (k Something) (k Else) (k Entirely)-@+{- | Generate a collection with a function from its indices++Doesn't need the @adjunctions@ package, but clashes with the "Data.Functor.Rep" import.++> tabulate :: (Things -> a) -> Collection a+> tabulate k = Collection { this = k This, that = k That } -} mkTabulate :: Name -> Q [Dec] mkTabulate tags = do-  let collectionName = mkName "Collection"   let tabulateName = mkName "tabulate"   names <- tagNames tags   let a = mkName "a"   sig <- sigD tabulateName $     appT (appT arrowT (appT (appT arrowT (conT tags)) (varT a))) $       conT collectionName `appT` varT a+  fun <- funD tabulateName [tabulateClause (siblingOf mkNameG_v "tabulate") names]+  inl <- inlineP tabulateName+  pure [sig, fun, inl]++tabulateClause :: (Name -> Q Name) -> [Tag] -> Q Clause+tabulateClause nested names = do   f <- newName "_f"+  fields <- for names \case+    Leaf name -> pure $ appE (varE f) $ conE (mkName name)+    Nested name sub -> do+      subTabulate <- nested sub+      pure $ appE (varE subTabulate) $ infixE (Just $ varE f) (varE '(.)) (Just . conE $ mkName name)+  let body = foldl' appE (conE collectionName) fields+  clause [varP f] (normalB body) []++-- * Tag instances++{- $tagInstances++Stock 'Bounded' and 'Enum' only work for nullary constructors.+These generators also handle nested tags, using the 'Bounded' and 'Enum' instances of the nested tag types.+Those may be derived or generated.+-}++{- | Generate a 'Bounded' instance for the tag type.++> data Tag = Front Inner.Tag | Local | Back Inner.Tag+>+> instance Bounded Tag where+>   minBound = Front minBound+>   maxBound = Back maxBound+-}+mkBounded :: Name -> Q [Dec]+mkBounded tags = do+  names <- tagNames tags+  (first, final) <- case (names, reverse names) of+    (first : _, final : _) -> pure (first, final)+    _ -> fail $ "Can't bound a type without constructors: " <> show tags   let-    body =-      foldl' appE (conE collectionName) do-        name <- names-        pure $ appE (varE f) $ conE (mkName name)-  fun <- funD tabulateName-    [ clause [varP f] (normalB body) []+    bound method = \case+      Leaf name -> conE $ mkName name+      Nested name _ -> conE (mkName name) `appE` varE method+  pure <$> instanceD (cxt []) (conT ''Bounded `appT` conT tags)+    [ valD (varP 'minBound) (normalB $ bound 'minBound first) []+    , valD (varP 'maxBound) (normalB $ bound 'maxBound final) []     ]-  inl <- inlineP tabulateName-  pure [sig, fun, inl] +{- | Generate an 'Enum' instance for the tag type, numbering the nested tags in place.++> data Tag = Front Inner.Tag | Local | Back Inner.Tag+>+> instance Enum Tag where+>   fromEnum t = case t of+>     Front x -> o0 + (fromEnum x - lo)+>     Local -> o1+>     Back x -> o2 + (fromEnum x - lo)+>     where+>       o0 = 0+>       o1 = o0 + (hi - lo + 1)+>       o2 = o1 + 1+>   toEnum n+>     | n < 0 = errorWithoutStackTrace …+>     | n < o1 = Front (toEnum (n - o0 + lo))+>     | n < o2 = Local+>     | n < o3 = Back (toEnum (n - o2 + lo))+>     | otherwise = errorWithoutStackTrace …+>     where+>       …+>       o3 = o2 + (hi - lo + 1)+>   enumFrom = boundedEnumFrom+>   enumFromThen = boundedEnumFromThen++Here @lo@ and @hi@ stand for the inlined @fromEnum (minBound :: Inner.Tag)@ and @fromEnum (maxBound :: Inner.Tag)@.++The instance requires @Bounded Tag@, see 'mkBounded'.+-}+mkEnum :: Name -> Q [Dec]+mkEnum tags = do+  names <- tagNames tags+  fromEnumClause <- do+    (offsets, bindings) <- runningOffsets . reverse . drop 1 $ reverse names+    t <- newName "t"+    matches <- fromEnumMatches $ zip names offsets+    pure $ clause [varP t] (normalB $ caseE (varE t) matches) bindings+  toEnumClause <- do+    (offsets, bindings) <- runningOffsets names+    n <- newName "n"+    pure $ clause [varP n] (toEnumBody tags n $ zip3 names offsets (drop 1 offsets)) bindings+  pure <$> instanceD (cxt []) (conT ''Enum `appT` conT tags)+    [ funD 'fromEnum [fromEnumClause]+    , funD 'toEnum [toEnumClause]+    , valD (varP 'enumFrom) (normalB $ varE 'boundedEnumFrom) []+    , valD (varP 'enumFromThen) (normalB $ varE 'boundedEnumFromThen) []+    ]++fromEnumMatches :: [(Tag, Name)] -> Q [Q Match]+fromEnumMatches spans =+  for spans \case+    (Leaf name, before) ->+      pure $ match (conP (mkName name) []) (normalB $ varE before) []+    (Nested name sub, before) -> do+      subTag <- newName "t"+      pure $ match+        (conP (mkName name) [varP subTag])+        (normalB $ plusE (varE before) (minusE (varE 'fromEnum `appE` varE subTag) (fromEnumOf 'minBound sub)))+        []++toEnumBody :: Name -> Name -> [(Tag, Name, Name)] -> Q Body+toEnumBody tags n spans =+  guardedB $+    [normalGE (lessThan . litE $ integerL 0) outOfRange]+    <> map pick spans+    <> [normalGE (varE 'otherwise) outOfRange]+  where+    lessThan bound = infixE (Just $ varE n) (varE '(<)) (Just bound)+    pick = \case+      (Leaf name, _, after) ->+        normalGE (lessThan $ varE after) (conE $ mkName name)+      (Nested name sub, before, after) ->+        normalGE (lessThan $ varE after) $+          conE (mkName name) `appE`+            (varE 'toEnum `appE` plusE (minusE (varE n) (varE before)) (fromEnumOf 'minBound sub))+    outOfRange =+      varE 'errorWithoutStackTrace `appE`+        infixE+          (Just . stringE $ "toEnum{" <> nameBase tags <> "}: out of range: ")+          (varE '(<>))+          (Just $ varE 'show `appE` varE n)++runningOffsets :: [Tag] -> Q ([Name], [Q Dec])+runningOffsets names = do+  first <- newName "o0"+  rest <- for [1 .. length names] \i -> newName $ "o" <> show i+  let offsets = first : rest+  let+    sizeE = \case+      Leaf _ -> litE $ integerL 1+      Nested _ sub -> plusE (minusE (fromEnumOf 'maxBound sub) (fromEnumOf 'minBound sub)) (litE $ integerL 1)+    start = valD (varP first) (normalB . litE $ integerL 0) []+    steps = do+      (before, after, tag) <- zip3 offsets (drop 1 offsets) names+      pure $ valD (varP after) (normalB $ plusE (varE before) (sizeE tag)) []+  pure (offsets, start : steps)++fromEnumOf :: Name -> Name -> Q Exp+fromEnumOf bound sub = varE 'fromEnum `appE` sigE (varE bound) (conT sub)++plusE :: Q Exp -> Q Exp -> Q Exp+plusE a b = infixE (Just a) (varE '(+)) (Just b)++minusE :: Q Exp -> Q Exp -> Q Exp+minusE a b = infixE (Just a) (varE '(-)) (Just b)+ -- * Utils -tagNames :: Name -> Q [String]+collectionName :: Name+collectionName = mkName "Collection"++lookupClass :: String -> String -> Q Name+lookupClass moduleName className = do+  found <- traverse lookupTypeName [moduleName <> "." <> className, className]+  case asum found of+    Just name ->+      pure name+    Nothing ->+      fail $ concat+        [ className, " is not in scope. Import ", moduleName+        , " or use mk", className, "For with an explicit class name."+        ]++classMember :: (String -> String -> String -> Name) -> Name -> String -> Q Name+classMember mkMember className member =+  case (namePackage className, nameModule className) of+    (Just package, Just moduleName) ->+      pure $ mkMember package moduleName member+    _ ->+      fail $ "Expected an imported class name, got: " <> show className++data Tag+  = Leaf String+  | Nested String Name++tagName :: Tag -> String+tagName = \case+  Leaf name -> name+  Nested name _ -> name++tagNames :: Name -> Q [Tag] tagNames tags =  reify tags >>= \case     TyConI (DataD _ _ _ _ constructors _) ->-      foldrM (flip extractTags) [] constructors+      foldrM (flip $ extractTags tags) [] constructors     _ ->       fail "Expected a type constructor name" -extractTags :: [String] -> Con -> Q [String]-extractTags acc = \case+extractTags :: Name -> [Tag] -> Con -> Q [Tag]+extractTags tags acc = \case   NormalC name [] ->-    pure $ nameBase name : acc+    pure $ Leaf (nameBase name) : acc+  NormalC name [(_, ConT sub)]+    | sub == tags ->+        fail $ "A tag can't nest its own type: " <> show name+    | otherwise ->+        pure $ Nested (nameBase name) sub : acc   huh ->     -- Skipping would produce a 'Collection' with fewer fields than there are     -- tags, making the generated 'index' silently non-exhaustive.-    fail $ "Expected a nullary constructor, got: " <> show huh+    fail $ "Expected a nullary constructor or one wrapping a nested tag type, got: " <> show huh++siblingOf :: (String -> String -> String -> Name) -> String -> Name -> Q Name+siblingOf mkNameG occ sub =+  case (namePackage sub, nameModule sub) of+    (Just pkg, Just modName) -> do+      let sibling = mkNameG pkg modName occ+      recover+        (fail $ "Nested tag " <> show sub <> " needs " <> occ <> " generated in " <> modName)+        (sibling <$ reify sibling)+    _ ->+      fail $ "Nested tag " <> show sub <> " must be a top-level type from another module"  -- | Lowercase the leading character and dodge reserved words. fieldNameOf :: String -> String
+ test-kmettoverse/Aliased.hs view
@@ -0,0 +1,30 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# OPTIONS_GHC -ddump-splices #-}++module Aliased (checks) where++import Data.Recollections.TH+import GHC.Generics (Generically1(..))++import qualified Data.Distributive as D+import qualified Data.Functor.Rep as R++data Sides+  = North+  | South+  | East+  | West+  deriving (Eq, Ord, Show, Enum, Bounded)++mkCollection ''Sides+mkIndices ''Sides+mkDistributiveFor ''D.Distributive ''Sides+mkRepresentableFor ''R.Representable ''Sides++checks :: [(String, Bool)]+checks =+  [ ("aliased index . indices", all (\s -> R.index indices s == s) [minBound .. maxBound])+  , ("aliased tabulate id", R.tabulate id == indices)+  , ("aliased distribute", D.distribute (Just indices) == fmap Just indices)+  ]
+ test-kmettoverse/Kmettoverse.hs view
@@ -0,0 +1,52 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# OPTIONS_GHC -ddump-splices #-}++module Main (main) where++import Control.Monad (unless)+import Data.Foldable (traverse_)+import Data.Distributive+import Data.Functor.Rep+import Data.Recollections.TH+import GHC.Generics+import System.Exit (exitFailure)++import qualified Aliased+import qualified Nested.Outer++data Things+  = This+  | That+  | Something+  | Else+  | Entirely+  deriving (Eq, Ord, Show, Enum, Bounded, Generic)++mkCollection ''Things+mkIndices ''Things+mkDistributive ''Things+mkRepresentable ''Things++positions :: Collection (Data.Functor.Rep.Rep Collection)+positions = tabulate id++main :: IO ()+main = do+  putStrLn $ "Representable.tabulate show: " <> show (tabulate @Collection show)+  putStrLn ""+  putStrLn $ "Distributive.distribute: " <> show (distribute [indices])+  putStrLn ""+  putStrLn $ "imapRep: " <> show (imapRep (,) (fmap fromEnum indices))+  putStrLn ""+  traverse_ check $+    [ ("index . indices", all (\t -> index indices t == t) [minBound .. maxBound])+    , ("tabulate id", positions == indices)+    , ("distribute", distribute [indices] == fmap pure indices)+    , ("mzipWithRep", mzipWithRep (+) (fmap fromEnum indices) (pure 1) == fmap (succ . fromEnum) indices)+    , ("fmapRep", fmapRep fromEnum indices == fmap fromEnum indices)+    ] <> Aliased.checks <> Nested.Outer.checks+  where+    check (label, ok) = do+      putStrLn $ label <> ": " <> show ok+      unless ok exitFailure
+ test-kmettoverse/Nested/Inner.hs view
@@ -0,0 +1,23 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}++module Nested.Inner+  ( Tag(..)+  , Collection(..)+  , indices+  ) where++import Data.Distributive (Distributive(..))+import Data.Functor.Rep (Representable(..))+import Data.Recollections.TH+import GHC.Generics++data Tag = Foo | Bar | Baz+  deriving (Eq, Ord, Show)++mkCollection ''Tag+mkIndices ''Tag+mkDistributive ''Tag+mkRepresentable ''Tag+mkBounded ''Tag+mkEnum ''Tag
+ test-kmettoverse/Nested/Outer.hs view
@@ -0,0 +1,32 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}++module Nested.Outer (checks) where++import Data.Distributive (Distributive(..))+import Data.Foldable (toList)+import Data.Functor.Rep (Representable(..))+import Data.Recollections.TH+import GHC.Generics+import Nested.Inner qualified as Inner++data Tag+  = Front Inner.Tag+  | Local+  | Back Inner.Tag+  deriving (Eq, Ord, Show)++mkCollection ''Tag+mkIndices ''Tag+mkDistributive ''Tag+mkRepresentable ''Tag+mkBounded ''Tag+mkEnum ''Tag++checks :: [(String, Bool)]+checks =+  [ ("nested index . indices", all (\t -> index indices t == t) [minBound .. maxBound])+  , ("nested tabulate id", tabulate id == indices)+  , ("nested distribute", distribute (Just indices) == fmap Just indices)+  , ("nested enum order", toList indices == [minBound .. maxBound])+  ]
+ test/Nested/Inner.hs view
@@ -0,0 +1,24 @@+{-# LANGUAGE TemplateHaskell #-}++module Nested.Inner+  ( Tag(..)+  , Collection(..)+  , indices+  , distribute+  , index+  , tabulate+  ) where++import Data.Recollections.TH+import GHC.Generics++data Tag = Foo | Bar | Baz+  deriving (Eq, Ord, Show)++mkCollection ''Tag+mkIndices ''Tag+mkDistribute ''Tag+mkIndex ''Tag+mkTabulate ''Tag+mkBounded ''Tag+mkEnum ''Tag
+ test/Nested/Outer.hs view
@@ -0,0 +1,28 @@+{-# LANGUAGE TemplateHaskell #-}++module Nested.Outer+  ( Tag(..)+  , Collection(..)+  , indices+  , distribute+  , index+  , tabulate+  ) where++import Data.Recollections.TH+import GHC.Generics+import Nested.Inner qualified as Inner++data Tag+  = Front Inner.Tag+  | Local+  | Back Inner.Tag+  deriving (Eq, Ord, Show)++mkCollection ''Tag+mkIndices ''Tag+mkDistribute ''Tag+mkIndex ''Tag+mkTabulate ''Tag+mkBounded ''Tag+mkEnum ''Tag
test/Spec.hs view
@@ -4,7 +4,10 @@ module Main (main) where  import Data.Recollections.TH+import Data.Foldable (toList) import GHC.Generics+import Nested.Inner qualified as Inner+import Nested.Outer qualified as Outer  data Things   = This@@ -20,12 +23,6 @@ mkIndex ''Things mkTabulate ''Things --- data Collection a = Collection---   { this, that, something, else', entirely :: a---   }---   deriving stock (Eq, Show, Generic1, Functor, Foldable, Traversable)---   deriving Applicative via (Generically1 Collection)- main :: IO () main = do   putStrLn $ "Applicative: " <>  show (pure @Collection ())@@ -41,3 +38,23 @@   putStrLn $ "Ditributive.distribute: " <>  show (distribute [indices])   putStrLn ""   putStrLn $ "Ditributive.collect: " <>  show (distribute . fmap (\thing -> pure $ thing == This) $ indices)+  putStrLn ""+  putStrLn $ "Nested indices: " <> show Outer.indices+  putStrLn ""+  putStrLn $ "Nested index roundtrip: " <> show (all (\t -> Outer.index Outer.indices t == t) Outer.indices)+  putStrLn ""+  putStrLn $ "Nested tabulate show: " <> show (Outer.tabulate show)+  putStrLn ""+  putStrLn $ "Nested distribute: " <> show (Outer.distribute [Outer.indices, Outer.indices])+  putStrLn ""+  putStrLn $ "Nested applicative: " <> show ((,) <$> Outer.indices <*> pure ())+  putStrLn ""+  putStrLn $ "Nested bounds: " <> show (minBound @Outer.Tag, maxBound @Outer.Tag)+  putStrLn ""+  putStrLn $ "Nested enum matches indices: " <> show ([minBound .. maxBound] == toList Outer.indices)+  putStrLn ""+  putStrLn $ "Nested fromEnum: " <> show (fmap fromEnum Outer.indices)+  putStrLn ""+  putStrLn $ "Nested toEnum roundtrip: " <> show (all (\t -> toEnum (fromEnum t) == t) Outer.indices)+  putStrLn ""+  putStrLn $ "Nested enumFromThen: " <> show [Outer.Front Inner.Bar, Outer.Local ..]