mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-22 15:01:39 +03:00
b0d4493b5c
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7066 GitOrigin-RevId: ef65253ad816d669d109cf45662817b3115b37c3
172 lines
5.7 KiB
Haskell
172 lines
5.7 KiB
Haskell
{-# LANGUAGE UndecidableInstances #-}
|
||
{-# OPTIONS_HADDOCK not-home #-}
|
||
|
||
module Hasura.Incremental.Select
|
||
( Select (..),
|
||
ConstS (..),
|
||
selectKey,
|
||
FMapS (..),
|
||
FieldS (..),
|
||
UniqueS,
|
||
newUniqueS,
|
||
DMapS (..),
|
||
|
||
-- * Re-exports
|
||
GEq (..),
|
||
GCompare (..),
|
||
GOrdering (..),
|
||
(:~:) (..),
|
||
)
|
||
where
|
||
|
||
import Control.Monad.Unique
|
||
import Data.Dependent.Map qualified as DM
|
||
import "some" Data.GADT.Compare
|
||
import Data.HashMap.Strict qualified as M
|
||
import Data.Kind
|
||
import Data.Proxy (Proxy (..))
|
||
import Data.Type.Equality
|
||
import GHC.OverloadedLabels (IsLabel (..))
|
||
import GHC.Records (HasField (..))
|
||
import GHC.TypeLits (KnownSymbol, sameSymbol, symbolVal)
|
||
import Hasura.Prelude
|
||
import Unsafe.Coerce (unsafeCoerce)
|
||
|
||
-- | The 'Select' class provides a way to access subparts of a product type using a reified
|
||
-- 'Selector'. A @'Selector' a b@ is essentially a function from @a@ to @b@, and indeed 'select'
|
||
-- converts a 'Selector' to such a function. However, unlike functions, 'Selector's can be compared
|
||
-- for equality using 'GEq' and ordered using 'GCompare'.
|
||
--
|
||
-- This is useful to implement dependency tracking, since it’s possible to track in a reified form
|
||
-- exactly which parts of a data structure are used.
|
||
--
|
||
-- Instances of 'Select' can be automatically derived for record types (just define an empty
|
||
-- instance). The instance uses the magical 'HasField' constraints, and 'Selector's for the type can
|
||
-- be written using @OverloadedLabels@.
|
||
class (GCompare (Selector a)) => Select a where
|
||
type Selector a :: Type -> Type
|
||
select :: Selector a b -> a -> b
|
||
|
||
type Selector r = FieldS r
|
||
default select :: Selector a ~ FieldS a => Selector a b -> a -> b
|
||
select (FieldS (_ :: Proxy s)) = getField @s
|
||
|
||
instance (Ord k, Hashable k) => Select (HashMap k v) where
|
||
type Selector (HashMap k v) = ConstS k (Maybe v)
|
||
select (ConstS k) = M.lookup k
|
||
|
||
instance (GCompare k) => Select (DM.DMap k f) where
|
||
type Selector (DM.DMap k f) = DMapS k f
|
||
select (DMapS k) = DM.lookup k
|
||
|
||
newtype FMap f x = FMap {unFMap :: f x}
|
||
deriving (Functor)
|
||
|
||
data FMapS f a b where
|
||
FMapS :: Selector a b -> FMapS f a (f b)
|
||
|
||
instance Select a => GEq (FMapS f a) where
|
||
FMapS sel1 `geq` FMapS sel2 =
|
||
case sel1 `geq` sel2 of
|
||
Just Refl -> Just Refl
|
||
Nothing -> Nothing
|
||
|
||
instance Select a => GCompare (FMapS f a) where
|
||
gcompare (FMapS sel1) (FMapS sel2) =
|
||
case gcompare sel1 sel2 of
|
||
GLT -> GLT
|
||
GEQ -> GEQ
|
||
GGT -> GGT
|
||
|
||
instance (Functor f, Select a) => Select (FMap f a) where
|
||
type Selector (FMap f a) = FMapS f a
|
||
select (FMapS s) = unFMap . fmap (select s)
|
||
|
||
deriving via FMap Maybe a instance Select a => Select (Maybe a)
|
||
|
||
-- | The constant selector, which is useful for representing selectors into data structures where
|
||
-- all fields have the same type. Matching on a value of type @'ConstS' k a b@ causes @a@ and @b@ to
|
||
-- unify, effectively “pinning” @b@ to @a@.
|
||
data ConstS k a b where
|
||
ConstS :: !k -> ConstS k a a
|
||
|
||
selectKey :: (Select a, Selector a ~ ConstS k v) => k -> a -> v
|
||
selectKey = select . ConstS
|
||
|
||
instance (Eq k) => GEq (ConstS k a) where
|
||
ConstS a `geq` ConstS b
|
||
| a == b = Just Refl
|
||
| otherwise = Nothing
|
||
|
||
instance (Ord k) => GCompare (ConstS k a) where
|
||
ConstS a `gcompare` ConstS b = case compare a b of
|
||
LT -> GLT
|
||
EQ -> GEQ
|
||
GT -> GGT
|
||
|
||
data FieldS r a where
|
||
FieldS :: (KnownSymbol s, HasField s r a) => !(Proxy s) -> FieldS r a
|
||
|
||
instance (KnownSymbol s, HasField s r a) => IsLabel s (FieldS r a) where
|
||
fromLabel = FieldS (Proxy @s)
|
||
|
||
instance GEq (FieldS r) where
|
||
FieldS a `geq` FieldS b = case sameSymbol a b of
|
||
-- If two fields of the same record have the same name, then their fields fundamentally must
|
||
-- have the same type! However, unfortunately, `HasField` constraints use a functional
|
||
-- dependency to enforce this rather than a type family, and functional dependencies don’t
|
||
-- provide evidence, so we have to use `unsafeCoerce` here. Yuck!
|
||
Just Refl -> Just (unsafeCoerce Refl)
|
||
Nothing -> Nothing
|
||
|
||
instance GCompare (FieldS r) where
|
||
FieldS a `gcompare` FieldS b = case sameSymbol a b of
|
||
-- See note about `HasField` and `unsafeCoerce` above.
|
||
Just Refl -> unsafeCoerce GEQ
|
||
Nothing
|
||
| symbolVal a < symbolVal b -> GLT
|
||
| otherwise -> GGT
|
||
|
||
-- | A 'UniqueS' is, as the name implies, a globally-unique 'Selector', which can be created using
|
||
-- 'newUniqueS'. If a value of type @'UniqueS' a@ is found to be equal (via 'geq') with another
|
||
-- value of type @'UniqueS' b@, then @a@ and @b@ must be the same type. This effectively allows the
|
||
-- creation of a dynamically-extensible sum type, where new constructors can be created at runtime
|
||
-- using 'newUniqueS'.
|
||
type role UniqueS nominal
|
||
|
||
newtype UniqueS a = UniqueS Unique
|
||
deriving (Eq)
|
||
|
||
newUniqueS :: (MonadUnique m) => m (UniqueS a)
|
||
newUniqueS = UniqueS <$> newUnique
|
||
{-# INLINE newUniqueS #-}
|
||
|
||
instance GEq UniqueS where
|
||
UniqueS a `geq` UniqueS b
|
||
-- This use of `unsafeCoerce` is safe as long as we don’t export the constructor of `UniqueS`.
|
||
-- Because a `UniqueS` is, in fact, unique, then we can be certain that equality of 'UniqueS's
|
||
-- implies equality of their argument types.
|
||
| a == b = Just (unsafeCoerce Refl)
|
||
| otherwise = Nothing
|
||
|
||
instance GCompare UniqueS where
|
||
UniqueS a `gcompare` UniqueS b = case compare a b of
|
||
LT -> GLT
|
||
-- See note about `unsafeCoerce` above.
|
||
EQ -> unsafeCoerce GEQ
|
||
GT -> GGT
|
||
|
||
data DMapS k f a where
|
||
DMapS :: !(k a) -> DMapS k f (Maybe (f a))
|
||
|
||
instance (GEq k) => GEq (DMapS k f) where
|
||
DMapS a `geq` DMapS b = case a `geq` b of
|
||
Just Refl -> Just Refl
|
||
Nothing -> Nothing
|
||
|
||
instance (GCompare k) => GCompare (DMapS k f) where
|
||
DMapS a `gcompare` DMapS b = case a `gcompare` b of
|
||
GLT -> GLT
|
||
GEQ -> GEQ
|
||
GGT -> GGT
|