Safe Haskell | Safe-Inferred |
---|---|
Language | Haskell2010 |
Dispatch over backends.
Creating and consuming AnyBackend
Creating a new value of type AnyBackend
is done via mkAnyBackend
.
Consuming a value of type AnyBackend
is done via either runAnyBackend
or
any of the dispatch functions (dispatchAnyBackend
, dispatchAnyBackend'
,
dispatchAnyBackend''
).
For implementation details, or when trying to understand this module, start
from AnyBackend
.
Backend Architecture
Our multiple backend architecture uses type classes and associated types in order to share code, such as parsing graphql queries, building schemas and metadata, while still accounting for the differences between backends.
Each backend implements the Backend
type class from Hasura.RQL.Types.Backend
as well as instances for other classes such as BackendSchema
from
Hasura.GraphQL.Schema.Backend, and define the associated types and
functions, such as ScalarType
and parseScalarValue
, which fit the backend.
Whenever one of these associated types (ScalarType
, Column
, etc.) are
used, we need to either push the BackendType
to our caller (and making our
type BackendType -> Type
), or use AnyBackend
(and allow our type to be
Type
). This is particularly useful when we need to store a container of
any backend.
In order to actually program abstractly using type classes, we need the type class instances to be available for us to use. This module is a trick to enumerate all supported backends and their respective instances to convince GHC that they can be used.
Example usage
As an example of using this module, consider wanting to write a function that calculates metrics for each source. For example, we want to count the number of tables each source has.
The SchemaCache
(defined in Hasura.RQL.Types.SchemaCache) holds a hash map
from each source to their information.
The source information is parameterized by the BackendType
and is hidden
using an existential type inside AnyBackend
. It essentially looks like this:
data SourceInfo b = ... type SourceCache = HashMap SourceName (AnyBackend SourceInfo)
Our metrics calculation function cares which backend it receives, but only for its type class instances so it can call the relevant functions:
telemetryForSource :: forall (b :: BackendType). SourceInfo b -> TelemetryPayload
In order to apply this function to all backends and return the telemetry payload for each,
we need to map over the hash map and dispatch the function over the relevant backend.
we can do this with runBackend
:
telemetries = map (`runBackend` telemetryForSource) (scSources schemaCache)
If we want to be able to extract some information about the backend type
inside telemetryForSource
, we can do this using backendTag
:
let telemetryForSource :: forall (b :: BackendType). HasTag b => SourceInfo b -> TelemetryPayload telemetryForSource = let dbKind = reify (backendTag @b)
Note that we needed to add the HasTag
constraint, which now means we can't use runBackend
because our function has the wrong type (it has an extra constraint).
Instead, we can use dispatchAnyBackend
which allows us to have one constraint:
telemetries = fmap (\sourceinfo -> (Any.dispatchAnyBackend @HasTag) sourceinfo telemetryForSource) (scSources schemaCache)
Note that we had to add the constraint name as a type application, and we had
to explicitly add a lambda instead of using flip
.
Synopsis
- data AnyBackend (i :: BackendType -> Type)
- type SatisfiesForAllBackends (i :: BackendType -> Type) (c :: Type -> Constraint) = (c (i ('Postgres 'Vanilla)), c (i ('Postgres 'Citus)), c (i ('Postgres 'Cockroach)), c (i 'MSSQL), c (i 'BigQuery), c (i 'DataConnector))
- liftTag :: BackendType -> AnyBackend BackendTag
- lowerTag :: AnyBackend i -> BackendType
- mapBackend :: forall (i :: BackendType -> Type) (j :: BackendType -> Type). AnyBackend i -> (forall b. i b -> j b) -> AnyBackend j
- traverseBackend :: forall (c :: BackendType -> Constraint) (i :: BackendType -> Type) (j :: BackendType -> Type) f. (AllBackendsSatisfy c, Functor f) => AnyBackend i -> (forall b. c b => i b -> f (j b)) -> f (AnyBackend j)
- mkAnyBackend :: forall (b :: BackendType) (i :: BackendType -> Type). HasTag b => i b -> AnyBackend i
- runBackend :: forall (i :: BackendType -> Type) (r :: Type). AnyBackend i -> (forall (b :: BackendType). i b -> r) -> r
- dispatchAnyBackend :: forall (c :: BackendType -> Constraint) (i :: BackendType -> Type) (r :: Type). AllBackendsSatisfy c => AnyBackend i -> (forall (b :: BackendType). c b => i b -> r) -> r
- dispatchAnyBackendWithTwoConstraints :: forall (c1 :: BackendType -> Constraint) (c2 :: BackendType -> Constraint) (i :: BackendType -> Type) (r :: Type). AllBackendsSatisfy c1 => AllBackendsSatisfy c2 => AnyBackend i -> (forall (b :: BackendType). c1 b => c2 b => i b -> r) -> r
- dispatchAnyBackend' :: forall (c :: Type -> Constraint) (i :: BackendType -> Type) (r :: Type). i `SatisfiesForAllBackends` c => AnyBackend i -> (forall (b :: BackendType). c (i b) => i b -> r) -> r
- dispatchAnyBackend'' :: forall (c1 :: Type -> Constraint) (c2 :: BackendType -> Constraint) (i :: BackendType -> Type) (r :: Type). i `SatisfiesForAllBackends` c1 => AllBackendsSatisfy c2 => AnyBackend i -> (forall (b :: BackendType). c2 b => c1 (i b) => i b -> r) -> r
- composeAnyBackend :: forall (c :: BackendType -> Constraint) (i :: BackendType -> Type) (r :: Type). AllBackendsSatisfy c => (forall (b :: BackendType). c b => i b -> i b -> r) -> AnyBackend i -> AnyBackend i -> r -> r
- mergeAnyBackend :: forall (c :: Type -> Constraint) (i :: BackendType -> Type). i `SatisfiesForAllBackends` c => (forall (b :: BackendType). c (i b) => i b -> i b -> i b) -> AnyBackend i -> AnyBackend i -> AnyBackend i -> AnyBackend i
- unpackAnyBackend :: forall (b :: BackendType) (i :: BackendType -> Type). HasTag b => AnyBackend i -> Maybe (i b)
- dispatchAnyBackendArrow :: forall (c1 :: BackendType -> Constraint) (c2 :: BackendType -> Constraint) (i :: BackendType -> Type) (r :: Type) (arr :: Type -> Type -> Type) x. (ArrowChoice arr, AllBackendsSatisfy c1, AllBackendsSatisfy c2) => (forall b. c1 b => c2 b => arr (i b, x) r) -> arr (AnyBackend i, x) r
- parseAnyBackendFromJSON :: i `SatisfiesForAllBackends` FromJSON => BackendType -> Value -> Parser (AnyBackend i)
- anyBackendCodec :: forall i. i `SatisfiesForAllBackends` HasCodec => BackendType -> JSONCodec (AnyBackend i)
- debugAnyBackendToJSON :: i `SatisfiesForAllBackends` ToJSON => AnyBackend i -> Value
- backendSourceKindFromText :: Text -> Maybe (AnyBackend BackendSourceKind)
- parseBackendSourceKindFromJSON :: Value -> Parser (AnyBackend BackendSourceKind)
Types and constraints
data AnyBackend (i :: BackendType -> Type) Source #
Allows storing types of kind BackendType -> Type
heterogenously.
Adding a new constructor to BackendType
will automatically create a new
constructor here.
Given some type defined as data T (b :: BackendType) = ...
, we can define
AnyBackend T
without mentioning any BackendType
.
This is useful for having generic containers of potentially different types
of T. For instance, SourceCache
is defined as a
HashMap SourceName (AnyBackend SourceInfo)
.
Instances
type SatisfiesForAllBackends (i :: BackendType -> Type) (c :: Type -> Constraint) = (c (i ('Postgres 'Vanilla)), c (i ('Postgres 'Citus)), c (i ('Postgres 'Cockroach)), c (i 'MSSQL), c (i 'BigQuery), c (i 'DataConnector)) Source #
Generates a constraint for a generic type over all backends.
Functions on AnyBackend
liftTag :: BackendType -> AnyBackend BackendTag Source #
How to obtain a tag from a runtime value.
lowerTag :: AnyBackend i -> BackendType Source #
Obtain a BackendType
from a runtime value.
mapBackend :: forall (i :: BackendType -> Type) (j :: BackendType -> Type). AnyBackend i -> (forall b. i b -> j b) -> AnyBackend j Source #
Transforms an AnyBackend i
into an AnyBackend j
.
traverseBackend :: forall (c :: BackendType -> Constraint) (i :: BackendType -> Type) (j :: BackendType -> Type) f. (AllBackendsSatisfy c, Functor f) => AnyBackend i -> (forall b. c b => i b -> f (j b)) -> f (AnyBackend j) Source #
Traverse an AnyBackend i
into an f (AnyBackend j)
.
mkAnyBackend :: forall (b :: BackendType) (i :: BackendType -> Type). HasTag b => i b -> AnyBackend i Source #
Creates a new AnyBackend i
for a given backend b
by wrapping the given i b
.
runBackend :: forall (i :: BackendType -> Type) (r :: Type). AnyBackend i -> (forall (b :: BackendType). i b -> r) -> r Source #
Dispatch a function to the value inside the AnyBackend
, that does not
require bringing into scope a new class constraint.
dispatchAnyBackend :: forall (c :: BackendType -> Constraint) (i :: BackendType -> Type) (r :: Type). AllBackendsSatisfy c => AnyBackend i -> (forall (b :: BackendType). c b => i b -> r) -> r Source #
Dispatch an existential using an universally quantified function while
also resolving a different constraint.
Use this to dispatch Backend* instances.
This is essentially a wrapper around runAnyBackend f . repackAnyBackend @c
.
dispatchAnyBackendWithTwoConstraints :: forall (c1 :: BackendType -> Constraint) (c2 :: BackendType -> Constraint) (i :: BackendType -> Type) (r :: Type). AllBackendsSatisfy c1 => AllBackendsSatisfy c2 => AnyBackend i -> (forall (b :: BackendType). c1 b => c2 b => i b -> r) -> r Source #
dispatchAnyBackend' :: forall (c :: Type -> Constraint) (i :: BackendType -> Type) (r :: Type). i `SatisfiesForAllBackends` c => AnyBackend i -> (forall (b :: BackendType). c (i b) => i b -> r) -> r Source #
Unlike dispatchAnyBackend
, the expected constraint has a different kind.
Use for classes like Show
, ToJSON
, etc.
dispatchAnyBackend'' :: forall (c1 :: Type -> Constraint) (c2 :: BackendType -> Constraint) (i :: BackendType -> Type) (r :: Type). i `SatisfiesForAllBackends` c1 => AllBackendsSatisfy c2 => AnyBackend i -> (forall (b :: BackendType). c2 b => c1 (i b) => i b -> r) -> r Source #
This allows you to apply a constraint to the Backend instances (c2)
as well as a constraint on the higher-kinded i b
type (c1)
composeAnyBackend :: forall (c :: BackendType -> Constraint) (i :: BackendType -> Type) (r :: Type). AllBackendsSatisfy c => (forall (b :: BackendType). c b => i b -> i b -> r) -> AnyBackend i -> AnyBackend i -> r -> r Source #
Sometimes we need to run operations on two backends of the same type.
If the backends don't contain the same type, the given r
value is returned.
Otherwise, the function is called with the two wrapped values.
mergeAnyBackend :: forall (c :: Type -> Constraint) (i :: BackendType -> Type). i `SatisfiesForAllBackends` c => (forall (b :: BackendType). c (i b) => i b -> i b -> i b) -> AnyBackend i -> AnyBackend i -> AnyBackend i -> AnyBackend i Source #
Merge two matching backends, falling back on a default.
unpackAnyBackend :: forall (b :: BackendType) (i :: BackendType -> Type). HasTag b => AnyBackend i -> Maybe (i b) Source #
Try to unpack the type of an existential.
Returns Just x
upon a succesful match, Nothing
otherwise.
Special case for arrows
dispatchAnyBackendArrow :: forall (c1 :: BackendType -> Constraint) (c2 :: BackendType -> Constraint) (i :: BackendType -> Type) (r :: Type) (arr :: Type -> Type -> Type) x. (ArrowChoice arr, AllBackendsSatisfy c1, AllBackendsSatisfy c2) => (forall b. c1 b => c2 b => arr (i b, x) r) -> arr (AnyBackend i, x) r Source #
Dispatch variant for use with arrow syntax.
NOTE: The below function accepts two constraints, if the arrow you want to dispatch only has one constraint then repeat the constraint twice. For example:
AB.dispatchAnyBackendArrow @BackendMetadata @BackendMetadata (proc (sourceMetadata, invalidationKeys)
JSON functions
parseAnyBackendFromJSON :: i `SatisfiesForAllBackends` FromJSON => BackendType -> Value -> Parser (AnyBackend i) Source #
Attempts to parse an AnyBackend
from a JSON value, using the provided
backend information.
anyBackendCodec :: forall i. i `SatisfiesForAllBackends` HasCodec => BackendType -> JSONCodec (AnyBackend i) Source #
Codec that can be used to decode and encode AnyBackend i
values. Throws
an error when attempting to encode a value with a mismatched backendKind
argument.
debugAnyBackendToJSON :: i `SatisfiesForAllBackends` ToJSON => AnyBackend i -> Value Source #
Outputs a debug JSON value from an AnyBackend
. This function must only be
used for debug purposes, as it has no way of inserting the backend kind in
the output, since there's no guarantee that the output will be an object.