packages feed

recollections (empty) → 0.1.0.0

raw patch · 6 files changed

+424/−0 lines, 6 filesdep +basedep +recollectionsdep +template-haskell

Dependencies added: base, recollections, template-haskell

Files

+ CHANGELOG.md view
@@ -0,0 +1,11 @@+# Changelog for `recollections`++All notable changes to this project will be documented in this file.++The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),+and this project adheres to the+[Haskell Package Versioning Policy](https://pvp.haskell.org/).++## 0.1.0.0 - 2026-07-26++Initial release.
+ LICENSE view
@@ -0,0 +1,26 @@+Copyright 2026 IC Rainbow++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++1.  Redistributions of source code must retain the above copyright notice, this+    list of conditions and the following disclaimer.++2.  Redistributions in binary form must reproduce the above copyright notice,+    this list of conditions and the following disclaimer in the documentation+    and/or other materials provided with the distribution.++3.  Neither the name of the copyright holder nor the names of its contributors+    may be used to endorse or promote products derived from this software+    without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,65 @@+# recollections++The splicing module needs these extensions and imports:++```haskell+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE DerivingVia #-}++import GHC.Generics (Generically1(..))+```++Given an enumeration of things like++```haskell+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:++```haskell+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+```++## Kmettoverse-lite++```haskell+-- Distributive+mkDistribute ''Things+-- collect is ++-- 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 — 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.
+ recollections.cabal view
@@ -0,0 +1,68 @@+cabal-version: 2.2++-- This file has been generated from package.yaml by hpack version 0.39.6.+--+-- see: https://github.com/sol/hpack++name:           recollections+version:        0.1.0.0+synopsis:       Fixed-size representable (Zippy Applicative) collections.+category:       Data Structures+author:         IC Rainbow+maintainer:     aenor.realm@gmail.com+copyright:      2026 IC Rainbow+license:        BSD-3-Clause+license-file:   LICENSE+build-type:     Simple+extra-source-files:+    README.md+    CHANGELOG.md++source-repository head+  type: git+  location: https://gitlab.com/dpwiz/recollections++library+  exposed-modules:+      Data.Recollections.TH+  other-modules:+      Paths_recollections+  autogen-modules:+      Paths_recollections+  hs-source-dirs:+      src+  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+  build-depends:+      base >=4.18 && <5+    , template-haskell >=2.20+  default-language: GHC2021++test-suite recollections-test+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  other-modules:+      Paths_recollections+  autogen-modules:+      Paths_recollections+  hs-source-dirs:+      test+  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:+      base >=4.18 && <5+    , recollections+    , template-haskell >=2.20+  default-language: GHC2021
+ src/Data/Recollections/TH.hs view
@@ -0,0 +1,211 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE TemplateHaskellQuotes #-}++module Data.Recollections.TH+  ( mkCollection+  , mkIndices++  , mkDistribute++  , mkIndex+  , mkTabulate+  ) where++import Data.Char+import Data.Foldable+import Data.Traversable+import GHC.Generics (Generic, Generic1, Generically1)+import Language.Haskell.TH++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"))+  let constr = recC collectionName fields+  let+    derivs =+      [ derivClause (Just StockStrategy)+          [ conT ''Show+          , conT ''Eq+          , conT ''Generic+          , conT ''Generic1+          , conT ''Functor+          , conT ''Foldable+          , conT ''Traversable+          ]+      , derivClause (Just $ ViaStrategy $ ConT ''Generically1 `AppT` ConT collectionName)+        [ conT ''Applicative+        ]+      ]+  pure <$> dataD mempty collectionName [collectionTyVar] Nothing [constr] derivs++-- | The @a@ binder of @data Collection a@.+--+-- @template-haskell-2.21@ (GHC 9.8) changed the binder flag of 'dataD'+-- from @()@ to 'BndrVis'.+#if MIN_VERSION_template_haskell(2,21,0)+collectionTyVar :: TyVarBndrVis+collectionTyVar = PlainTV (mkName "a") BndrReq+#else+collectionTyVar :: TyVarBndrUnit+collectionTyVar = PlainTV (mkName "a") ()+#endif++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)+  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+  }+@+-}+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) []+    ]+  inl <- inlineP distributeName+  pure [sig, fun, inl]++-- * Representable++{- |+@+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+    [ clause+        [ conP collectionName $ map (varP . snd) bounds+        ]+        (normalB body)+        []+    ]+  inl <- inlineP indexName+  pure [sig, fun, inl]++{- |+@+tabulate k = Collection (k This) (k That) (k Something) (k Else) (k Entirely)+@+-}+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+  f <- newName "_f"+  let+    body =+      foldl' appE (conE collectionName) do+        name <- names+        pure $ appE (varE f) $ conE (mkName name)+  fun <- funD tabulateName+    [ clause [varP f] (normalB body) []+    ]+  inl <- inlineP tabulateName+  pure [sig, fun, inl]++-- * Utils++tagNames :: Name -> Q [String]+tagNames tags =+ reify tags >>= \case+    TyConI (DataD _ _ _ _ constructors _) ->+      foldrM (flip extractTags) [] constructors+    _ ->+      fail "Expected a type constructor name"++extractTags :: [String] -> Con -> Q [String]+extractTags acc = \case+  NormalC name [] ->+    pure $ nameBase name : 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++-- | Lowercase the leading character and dodge reserved words.+fieldNameOf :: String -> String+fieldNameOf = \case+  [] -> []+  n : ame -> legalizeVariableName $ toLower n : ame++legalizeVariableName :: String -> String+legalizeVariableName name+  | name `elem` illegal = name ++ "'"+  | otherwise = name++-- | Haskell 2010 reserved words, which cannot be used as record field names.+illegal :: [String]+illegal =+  [ "case", "class", "data", "default", "deriving", "do"+  , "else", "foreign", "if", "import", "in"+  , "infix", "infixl", "infixr", "instance"+  , "let", "module", "newtype", "of"+  , "then", "type", "where", "_"+  ]++inlineP :: Name -> Q Dec+inlineP name = pragInlD name Inline FunLike AllPhases
+ test/Spec.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE TemplateHaskell #-}+{-# OPTIONS_GHC -ddump-splices #-}++module Main (main) where++import Data.Recollections.TH+import GHC.Generics++data Things+  = This+  | That+  | Something+  | Else+  | Entirely+  deriving (Eq, Ord, Show, Enum, Bounded, Generic)++mkCollection ''Things+mkIndices ''Things+mkDistribute ''Things+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 ())+  putStrLn ""+  putStrLn $ "indices: " <>  show indices+  putStrLn ""+  putStrLn $ "indices, numeric: " <>  show (fmap fromEnum indices)+  putStrLn ""+  putStrLn $ "Representable.index: " <>  show ([ (t, t == index indices t) | t <- [minBound .. maxBound] ])+  putStrLn ""+  putStrLn $ "Representable.tabulate show: " <>  show (tabulate show)+  putStrLn ""+  putStrLn $ "Ditributive.distribute: " <>  show (distribute [indices])+  putStrLn ""+  putStrLn $ "Ditributive.collect: " <>  show (distribute . fmap (\thing -> pure $ thing == This) $ indices)