New TTC Features
Today I released TTC version 1.3.0.0, which includes two new features.
Minimal Compile-Time Validation Syntax
TTC provides a number of ways to use Parse
instances to validate constants at compile-time. They are all
functionally equivalent, and developers may use whatever is most
convenient for their project (supported GHC versions) and specific
types. The valid
function is usually a great choice when the parsed typed has a Lift
instance. It uses typed Template Haskell, so a type annotation
determines how the string is parsed. The following works in all
supported versions of GHC:
alice :: Username
= $$(TTC.valid "alice") alice
This release adds an IsString
instance for typed Template Haskell expressions so that string syntax
automatically calls valid
.
The OverloadedStrings
extension must be enabled in the module where this functionality is
used. The following works in all supported versions of GHC:
alice :: Username
= $$("alice") alice
The following works from GHC 9:
alice :: Username
= $$"alice" alice
As noted in the documentation,
a drawback to using the minimal syntax is that it can make the code more
difficult to understand by somebody who is not already familiar with
this technique. Explicit TTC.valid
usage provides
developers a way to check the documentation and understand what is
happening, while the minimal syntax may leave some developers scratching
their head. As with all of the validation methods, developers should use
what is most appropriate for their project. Perhaps minimal syntax is a
good choice in a project where all developers are assumed to be
advanced, while explicit TTC.valid
usage may be more
appropriate in more beginner-friendly projects.
Thank you to Tal Walter for showing me Chris Done’s Statically checked overloaded strings gist, which inspired me to add support for this.
parseOrFail
Functions
The parse
method returns an Either
type, and TTC provides a number of convenience functions for parsing to
other types. The parseMaybe
functions return a Maybe
type, and the parseUnsafe
functions call error
on error. The following is an abridged overview. Note that the internal
parse'
helper function simply sets the error type to String
,
for use when the error is ignored.
class Parse a where
parse :: (Textual t, Textual e) => t -> Either e a
parse' :: (Parse a, Textual t) => t -> Either String a
= parse
parse'
parseMaybe :: (Parse a, Textual t) => t -> Maybe a
= either (const Nothing) Just . parse'
parseMaybe
parseUnsafe :: (HasCallStack, Parse a, Textual t) => t -> a
= either (error . ("parseUnsafe: " ++)) id . parse parseUnsafe
This release adds a set of parseOrFail
functions that fail using MonadFail
on error.
parseOrFail :: (MonadFail m, Parse a, Textual t) => t -> m a
= either fail pure . parse parseOrFail
I have been on the fence about including this for years, because it
is so trivial. I decided to go ahead and add it because it seems to fit,
regardless of the API bloat. MonadFail
is in Prelude
after all. Note that Maybe
has a MonadFail
instance, but I am keeping the parseMaybe
functions for backwards compatibility as well as because
parseMaybe
may be easier to understand than
parseOrFail
in some contexts.