Skip to content

Commit 774de56

Browse files
Merge pull request #4476 from JacquesCarette/derivingHasChunkRefs
`declareHasChunkRefs`: Generate instances of `HasChunkRefs` where possible
2 parents 9bd5636 + dd84436 commit 774de56

File tree

10 files changed

+183
-30
lines changed

10 files changed

+183
-30
lines changed

code/drasil-database/lib/Drasil/Database.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ module Drasil.Database (
44
module Drasil.Database.ChunkDB,
55
module Drasil.Database.Dump,
66
module Drasil.Database.UID,
7+
module Drasil.Database.TH,
78
module Drasil.Database.TypedUIDRef
89
) where
910

1011
import Drasil.Database.Chunk
1112
import Drasil.Database.ChunkDB
1213
import Drasil.Database.Dump
14+
import Drasil.Database.TH
1315
import Drasil.Database.TypedUIDRef
1416
import Drasil.Database.UID

code/drasil-database/lib/Drasil/Database/Chunk.hs

Lines changed: 107 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,28 @@
1-
{-# LANGUAGE ExistentialQuantification #-}
2-
{-# LANGUAGE TypeApplications #-}
3-
{-# LANGUAGE ConstraintKinds #-}
4-
{-# LANGUAGE InstanceSigs #-}
5-
6-
module Drasil.Database.Chunk
7-
( Chunk,
8-
IsChunk,
9-
HasChunkRefs (..),
10-
mkChunk, -- FIXME: mkChunk should not be exported but is temporarily because this module is NOT in `drasil-database`
11-
unChunk,
12-
chunkType,
13-
)
14-
where
1+
{-# LANGUAGE ExistentialQuantification,
2+
ConstraintKinds,
3+
InstanceSigs,
4+
TypeOperators,
5+
TypeApplications,
6+
FlexibleContexts,
7+
UndecidableInstances,
8+
FlexibleInstances #-}
9+
module Drasil.Database.Chunk (
10+
Chunk,
11+
IsChunk,
12+
HasChunkRefs(..),
13+
mkChunk, -- FIXME: mkChunk should not be exported but is temporarily because this module is NOT in `drasil-database`
14+
unChunk,
15+
chunkType
16+
) where
1517

1618
import Control.Lens ((^.), to, Getter)
17-
import Data.Typeable (Proxy (Proxy), TypeRep, Typeable, cast, typeOf, typeRep)
1819
import qualified Data.Set as S
20+
import Data.Typeable (Proxy (Proxy), TypeRep, Typeable, cast, typeOf, typeRep)
21+
import GHC.Generics (Generic (Rep, from), M1 (..), K1 (..), type (:*:) (..),
22+
type (:+:) (..), U1, Generically(..))
1923

2024
import Drasil.Database.UID (HasUID (..), UID)
2125

22-
-- | All chunks should expose what chunks they reference/rely on, so that we can
23-
-- test 'ChunkDB's to ensure all presupposed chunks are already registered.
24-
class HasChunkRefs a where
25-
chunkRefs :: a -> S.Set UID
26-
2726
-- | Constraint for anything that may be considered a valid chunk type.
2827
type IsChunk a = (HasUID a, HasChunkRefs a, Typeable a)
2928

@@ -56,3 +55,91 @@ unChunk (Chunk c) = cast c
5655
-- | Ask a 'Chunk' for the type of data it codifies.
5756
chunkType :: Chunk -> TypeRep
5857
chunkType (Chunk c) = typeOf c
58+
59+
-- | The majority of chunks will relate other chunks in some way. In other
60+
-- words, the majority of our chunks *depend* on others. 'HasChunkRefs' is meant
61+
-- as a way to capture what things a chunk *directly* refers to (i.e., depends
62+
-- on directly).
63+
class HasChunkRefs a where
64+
chunkRefs :: a -> S.Set UID
65+
66+
instance HasChunkRefs UID where
67+
-- | 'UID's are meant to be "owned" (i.e., they are the unique identifier of
68+
-- the chunk being defined), not *carried as references to other chunks*.
69+
-- 'TypedUIDRef t' exists to be used as a *reference to another chunk of type
70+
-- 't'*. Therefore, `UID` has no chunk references.
71+
chunkRefs _ = S.empty
72+
{-# INLINABLE chunkRefs #-}
73+
74+
instance HasChunkRefs Int where
75+
chunkRefs _ = S.empty
76+
{-# INLINABLE chunkRefs #-}
77+
78+
instance HasChunkRefs Integer where
79+
chunkRefs _ = S.empty
80+
{-# INLINABLE chunkRefs #-}
81+
82+
instance HasChunkRefs Double where
83+
chunkRefs _ = S.empty
84+
{-# INLINABLE chunkRefs #-}
85+
86+
instance HasChunkRefs Bool where
87+
chunkRefs _ = S.empty
88+
{-# INLINABLE chunkRefs #-}
89+
90+
instance HasChunkRefs Char where
91+
chunkRefs _ = S.empty
92+
{-# INLINABLE chunkRefs #-}
93+
94+
-- NOTE: 'OVERLAPPING' instance here because [Char] is instantiated with
95+
-- `HasChunkRefs [a]`, but very inefficient. We already know the result will be
96+
-- empty.
97+
instance {-# OVERLAPPING #-} HasChunkRefs String where
98+
chunkRefs _ = S.empty
99+
{-# INLINABLE chunkRefs #-}
100+
101+
instance HasChunkRefs a => HasChunkRefs [a] where
102+
chunkRefs = S.unions . map chunkRefs
103+
{-# INLINABLE chunkRefs #-}
104+
105+
instance HasChunkRefs a => HasChunkRefs (Maybe a) where
106+
chunkRefs Nothing = S.empty
107+
chunkRefs (Just v) = chunkRefs v
108+
{-# INLINABLE chunkRefs #-}
109+
110+
instance (HasChunkRefs l, HasChunkRefs r) => HasChunkRefs (Either l r) where
111+
chunkRefs = either chunkRefs chunkRefs
112+
{-# INLINABLE chunkRefs #-}
113+
114+
instance (Generic a, GHasCRefs (Rep a)) => HasChunkRefs (Generically a) where
115+
chunkRefs (Generically a) = gChunkRefs $ from a
116+
{-# INLINABLE chunkRefs #-}
117+
118+
class GHasCRefs f where
119+
gChunkRefs :: f p -> S.Set UID
120+
121+
-- Meta-information (constructors, selectors): pass through
122+
instance GHasCRefs f => GHasCRefs (M1 i c f) where
123+
gChunkRefs (M1 x) = gChunkRefs x
124+
{-# INLINABLE gChunkRefs #-}
125+
126+
-- Products: Union
127+
instance (GHasCRefs a, GHasCRefs b) => GHasCRefs (a :*: b) where
128+
gChunkRefs (a :*: b) = gChunkRefs a `S.union` gChunkRefs b
129+
{-# INLINABLE gChunkRefs #-}
130+
131+
-- Sums: Depends on variant
132+
instance (GHasCRefs a, GHasCRefs b) => GHasCRefs (a :+: b) where
133+
gChunkRefs (L1 x) = gChunkRefs x
134+
gChunkRefs (R1 x) = gChunkRefs x
135+
{-# INLINABLE gChunkRefs #-}
136+
137+
-- Fields: Delegate
138+
instance HasChunkRefs c => GHasCRefs (K1 i c) where
139+
gChunkRefs (K1 x) = chunkRefs x
140+
{-# INLINABLE gChunkRefs #-}
141+
142+
-- Unit: Nothing!
143+
instance GHasCRefs U1 where
144+
gChunkRefs _ = S.empty
145+
{-# INLINABLE gChunkRefs #-}

code/drasil-database/lib/Drasil/Database/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ _Last updated: Dec. 4th, 2025_
66
* `ChunkDB.hs`: Want to aggregate all your chunks? Use this database.
77
* `Dump.hs`: Simple tool for dumping all known chunks in a database (grouped by type).
88
* `UID.hs`: Defines the structure of universally unique identifiers we use for our chunk database.
9+
* `TH.hs`: Defines an automation tool for declaring that a 'chunk type' has 'chunk references'.
910
* `TypedUIDRef.hs`: `UID`s are great! But _untyped._ Creates a chunk reference data type that carries type information at the Haskell-type-level for type-safe `UID` references!
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{-# LANGUAGE TemplateHaskellQuotes #-}
2+
module Drasil.Database.TH (
3+
-- * The Magic
4+
declareHasChunkRefs,
5+
-- * Re-exports from GHC.Generics for convenience
6+
Generic,
7+
Generically(..)
8+
) where
9+
10+
import GHC.Generics (Generic, Generically(..))
11+
import Language.Haskell.TH (Name, Q, Dec(..), Type(..), DerivStrategy(..))
12+
13+
import Drasil.Database.Chunk (HasChunkRefs)
14+
15+
-- | Declares that a type is a chunk type; Generates an instance of
16+
-- 'HasChunkRefs'.
17+
declareHasChunkRefs :: Name -> Q [Dec]
18+
declareHasChunkRefs = deriveGenerically [''HasChunkRefs]
19+
20+
-- | Generates:
21+
--
22+
-- 1. A 'Generic' instance for the type:
23+
-- @
24+
-- deriving stock instance Generic Ty
25+
-- @
26+
--
27+
-- 2. For all type classes to be derived generically:
28+
-- @
29+
-- deriving via Generically Ty instance TheClass Ty
30+
-- @
31+
deriveGenerically :: [Name] -> Name -> Q [Dec]
32+
deriveGenerically clss ty = do
33+
let typeCon = ConT ty
34+
35+
-- deriving stock instance Generic Ty
36+
drvGeneric = StandaloneDerivD
37+
(Just StockStrategy)
38+
[]
39+
(AppT (ConT ''Generic) typeCon)
40+
41+
-- deriving via Generically Ty instance TheClass Ty
42+
drvCls cls = StandaloneDerivD
43+
(Just (ViaStrategy (AppT (ConT ''Generically) typeCon)))
44+
[]
45+
(AppT (ConT cls) typeCon)
46+
47+
-- Gather all classes we want to derive generically
48+
clsDrvs = map drvCls clss
49+
50+
return $ drvGeneric : clsDrvs

code/drasil-database/lib/Drasil/Database/TypedUIDRef.hs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,20 @@ module Drasil.Database.TypedUIDRef (
66
import Control.Lens ((^.))
77
import Data.Maybe (fromMaybe)
88

9-
import Drasil.Database.Chunk (IsChunk)
9+
import Drasil.Database.Chunk (IsChunk, HasChunkRefs (..))
1010
import Drasil.Database.ChunkDB (ChunkDB, find)
1111
import Drasil.Database.UID (HasUID(..), UID)
12+
import qualified Data.Set as S (singleton)
1213

1314
-- | 'UID' references that contain information about the type of data the 'UID'
1415
-- refers to, useful for type-safe dereferencing.
1516
newtype TypedUIDRef typ = TypedUIDRef UID
1617

18+
instance HasChunkRefs (TypedUIDRef t) where
19+
-- | A 'TypedUIDRef t' carries a 'UID' referring to a chunk of type 't'.
20+
chunkRefs (TypedUIDRef u) = S.singleton u
21+
{-# INLINABLE chunkRefs #-}
22+
1723
-- | Create a 'TypedUIDRef' to a chunk.
1824
mkRef :: IsChunk t => t -> TypedUIDRef t
1925
mkRef t = TypedUIDRef $ t ^. uid

code/drasil-database/lib/Drasil/Database/UID.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ import GHC.Generics
1818

1919
import Control.Lens (Getter, makeLenses, (^.), view, over)
2020

21-
-- | The most basic item: having a unique identifier key, here a UID.
21+
-- | The most basic item: having a unique identifier key, a 'UID'.
2222
class HasUID c where
23-
-- | Provides a /unique/ id for internal Drasil use.
23+
-- | The /unique/ id of the chunk (for internal Drasil use only).
2424
uid :: Getter c UID
2525

2626
-- | A @UID@ is a 'unique identifier' for things that we will put into our

code/drasil-database/package.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ dependencies:
1818
- aeson
1919
- containers
2020
- lens
21+
- template-haskell
2122
- text
2223
- drasil-utils
2324

code/drasil-lang/lib/Language/Drasil/Chunk/NamedIdea.hs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@ module Language.Drasil.Chunk.NamedIdea (
99
nc, ncUID, nw, mkIdea, mkIdeaUID
1010
) where
1111

12-
import Control.Lens ((^.), makeLenses)
13-
import Control.Lens.Lens (Lens')
12+
import Control.Lens ((^.), makeLenses, Lens')
1413

15-
import Drasil.Database (HasChunkRefs(..), mkUid, UID, HasUID(..))
14+
import Drasil.Database (mkUid, UID, HasUID(..), declareHasChunkRefs, Generically(..))
1615
import Language.Drasil.NounPhrase.Core (NP)
1716

1817
-- TODO: Why does a NamedIdea need a UID? It might need a UID to be registered in the chunk map.
@@ -53,11 +52,9 @@ data IdeaDict = IdeaDict {
5352
_np :: NP,
5453
mabbr :: Maybe String
5554
}
55+
declareHasChunkRefs ''IdeaDict
5656
makeLenses ''IdeaDict
5757

58-
instance HasChunkRefs IdeaDict where
59-
chunkRefs = const mempty -- FIXME: `chunkRefs` should actually collect the referenced chunks.
60-
6158
-- | Equal if 'UID's are equal.
6259
instance Eq IdeaDict where a == b = a ^. uid == b ^. uid
6360
-- | Finds the 'UID' of the 'IdeaDict' used to make the 'IdeaDict'.

code/drasil-lang/lib/Language/Drasil/NounPhrase/Core.hs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ module Language.Drasil.NounPhrase.Core (
33
-- * Types
44
CapitalizationRule(..), NP(..),
55
PluralForm, PluralRule(..),
6-
NPStruct(S,(:-:),(:+:),P)) where
6+
NPStruct(S,(:-:),(:+:),P)
7+
) where
8+
9+
import Drasil.Database (HasChunkRefs(..))
710

811
import Language.Drasil.Symbol (Symbol)
912

@@ -47,3 +50,6 @@ data NP =
4750
--capitalization, one of the two cannot be capitalized right now.
4851
--The two capitalization rules are for sentenceCase / titleCase respectively
4952

53+
instance HasChunkRefs NP where
54+
chunkRefs _ = mempty
55+
{-# INLINABLE chunkRefs #-}

code/drasil-lang/package.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ description: Please see the README on GitHub at <https://github.com/JacquesCaret
1010
language: Haskell2010
1111
default-extensions:
1212
- StrictData
13+
- StandaloneDeriving
14+
- DerivingVia
15+
- DeriveGeneric
1316

1417
extra-source-files: []
1518

0 commit comments

Comments
 (0)