Validated Constants
As described in Render
and Parse
, the Parse
type class provides a
way to parse Textual
data types. Unlike Read
,
it supports returning error messages. TTC provides various functions
that use this functionality to validate constants at compile-time, using
Template
Haskell. All of these functions are used for compile-time constant
validation, but each one has different benefits and drawbacks. Choose
which one to use based on your project requirements.
Validation Using Typed Expressions
These validation functions make use of Template Haskell typed expressions. Unfortunately, some Haskell tools still do not support typed expressions. If this is a problem in your development environment, functions for validation using untyped expressions are also provided.
valid
The quintessential constant validation function is
valid
. It is used as follows:
name :: Name
= $$(TTC.valid "Bill") name
The Parse
instance of the Name
type is used
to parse the string within the splice. When parsing results in an error
(Left
), compilation fails with the error message. When the
string is parsed successfully (Right
), the
Name
value is assigned to name
.
The type signature of valid
is different for different
versions of GHC. The type signature in GHC 9 or later is as follows:
valid :: (MonadFail m, THS.Quote m, Parse a, THS.Lift a)
=> String
-> THS.Code m a
The type signature in older versions of GHC is as follows:
valid :: (Parse a, THS.Lift a)
=> String
-> TH.Q (TH.TExp a)
As specified in the type signatures, the type being parsed must have
a Lift
instance. In cases where you are unable to create a
Lift
instance for your type, consider using mkValid
or validOf
instead.
validOf
The validOf
function provides a way to validate
constants of types without a Lift
instance using typed
expressions. It works by parsing the string twice. First, it parses the
string during compile-time. When parsing results in an error
(Left
), compilation fails with the error message. When the
string is parsed successfully (Right
), an expression that
parses the string again, assuming that it is valid, is assigned to
name
. When the constant is first used at runtime, that
expression is evaluated, parsing the string again.
Unfortunately, the type signature is not sufficient to specify the
type when using this method. A Proxy
must be passed, as in
the following example:
name :: Name
= $$(TTC.validOf (Proxy :: Proxy Name) "Bill") name
The type signature in GHC 9 or later is as follows:
validOf :: (MonadFail m, THS.Quote m, Parse a)
=> Proxy a
-> String
-> THS.Code m a
The type signature in older versions of GHC is as follows:
validOf :: Parse a
=> Proxy a
-> String
-> TH.Q (TH.TExp a)
mkValid
Passing a Proxy
in every use of validOf
can
result in quite a bit of boilerplate. The mkValid
function
is a Template Haskell function that creates a validation function for a
specific type using validOf
. The type signature of this
function is as follows:
mkValid :: String -> Name -> DecsQ
For example, a type module that is used with a qualified import can
declare a valid
function for that type, as follows:
{-# LANGUAGE TemplateHaskell #-}
module Example.Name
Name
(
, validwhere
)
import Control.Monad (unless)
import Data.Char (isPrint)
import qualified Data.Text as T
import Data.Text (Text)
import qualified Data.TTC as TTC
newtype Name = Name Text
instance TTC.Parse Name where
= TTC.asT $ \t -> do
parse isPrint t) . Left $
unless (T.all "invalid Name: contains invalid characters"
TTC.fromS pure $ Name t
instance TTC.Render Name where
Name name) = TTC.fromT name
render (
$(TTC.mkValid "valid" ''Name)
The first argument is the name of the function to create, and the
second argument is the type. The type signature of the created function
is different for different versions of GHC. In GHC 9 or later, the type
signature of the valid
function created in the example
above is as follows:
valid :: forall m. (MonadFail m, THS.Quote m)
=> String
-> THS.Code m Name
The type signature in older versions of GHC is as follows:
valid :: String
-> TH.Q (TH.TExp Name)
When the type module is imported qualified, the created function can be used as follows:
name :: Name
= $$(Name.valid "Bill") name
This is a concise way to declare validated constants using typed
expressions for types without a Lift
instance.
Validation Using Untyped Expressions
The following validation functions make use of Template Haskell untyped expressions. They are useful if you rely on tools that do not yet support typed expressions.
When using untyped expressions, it is up to the developer to make sure that the type signature of a constant matches the type used by the validation function.
untypedValidOf
The untypedValidOf
function is a version of validOf
that uses untyped expressions.
It works in the same way, by parsing the string at compile-time to
validate it and then parsing the same string again at runtime when
valid. The type signature is as follows:
untypedValidOf :: Parse a
=> Proxy a
-> String
-> ExpQ
As with untypedValidOf
, a Proxy
must be
passed to specify the type, as shown in the following example:
name :: Name
= $(TTC.untypedValidOf (Proxy :: Proxy Name) "Bill") name
mkUntypedValidOf
The mkUntypedValidOf
function is a Template Haskell
function that creates a validation function for a specific type using
untypedValidOf
. The type signature of this function is as
follows:
mkUntypedValid :: String -> Name -> DecsQ
It has the same type signature as mkValid
but creates a function that
uses untyped expressions instead of typed expressions. The function is
used as follows:
$(TTC.mkUntypedValid "valid" ''Name)
The type signature of the valid
function created is as
follows:
valid :: String -> TH.ExpQ
When the type module is imported qualified, the created function can be used as follows:
name :: Name
= $(Name.valid "Bill") name
mkUntypedValidQQ
The mkUntypedValidQQ
function works the same as mkUntypedValidOf
except that
it creates a quasi-quote function instead of a splice function. The type
signature of this function is the same:
mkUntypedValidQQ :: String -> Name -> DecsQ
The function is used as follows:
$(TTC.mkUntypedValidQQ "valid" ''Name)
When the type module is imported qualified, the created function can be used as follows:
name :: Name
= [Name.valid|Bill|] name
Use of this function is a matter of taste. It does not provide any
benefits over mkUntypedValidOf
.