Textual Type Classes.
This library provides type classes for conversion between data types and textual data types (strings). Please see the README on GitHub at https://github.com/ExtremaIS/ttc-haskell/tree/main/ttc#readme.
TTC: Textual Type Classes
Overview
TTC, an initialism of Textual Type Classes, is a library that provides Render
and Parse
type classes for conversion between data types and textual data types (strings). Use the Show
and Read
type classes for debugging/development, and use the Render
and Parse
type classes for your own purposes. The library also provides various ways to validate constants at compile-time.
This package (ttc
) uses a Textual
type class for conversion between textual data types. This package uses simple types and compiles quickly, but the supported textual data types are fixed. It is not possible for users to add support for additional textual data types.
Since a type may have at most one instance of a given type class, special care must be taken when defining type class instances in a shared library. In particular, orphan instances should generally not be used in shared libraries since they prevent users of the libraries from writing their own instances.
Render
and Parse
are best used with types that have canonical textual representations, such as textual identifiers. When there is more than one way to create a textual representation, such as configurable formatting, using a normal function is probably more appropriate. Such a function can make use of the Textual
type class to support multiple textual data types.
This overview includes a brief introduction of the library. The following resources are also available:
- API documentation is viewable on Hackage.
- A series of articles gives a guided tour of the library.
- The
ttc-examples
directory in the repository contains usage examples.
Textual
The Textual
type class is used to convert between the following textual data types:
String
- Strict
Text
- Lazy
Text
Text Builder
ShortText
- Strict
ByteString
- Lazy
ByteString
ByteString Builder
(andData.Binary.Builder
)ShortByteString
This type class has two key features:
- Type conversion is not done through a fixed type (such as
String
orText
). - It has a single type variable, making it easy to write functions that accept arguments and/or return values that may be any of the supported textual data types.
For more details, see the Textual Type Class article.
Render
The Render
type class renders a data type as a Textual
data type:
class Render a where
render :: Textual t => a -> t
It is analogous to the Show
type class, which can be reserved for debugging/development.
The render
function returns any of the supported textual data types. Use the textual data type that is most natural in the implementation of render
instances, and return values are converted to other textual data types when necessary. The Show
and IsString
type classes are not used, so use of the String
type is not required.
As a simple example, consider a Username
type that is implemented as a newtype
over Text
:
module Username (Username) where
import Control.Monad (unless, when)
import Data.Char (isAsciiLower)
import qualified Data.Text as T
import Data.Text (Text)
import qualified Data.TTC as TTC
newtype Username = Username Text
deriving (Eq, Ord, Show)
instance TTC.Render Username where
render (Username t) = TTC.convert t
If a username needs to be included in a String
error message, conversion is automatic:
putStrLn $ "user not found: " ++ TTC.render uname
For more details, see the Render and Parse article.
Parse
The Parse
type class parses a data type from a Textual
data type:
class Parse a where
parse :: (Textual t, Textual e) => t -> Either e a
It is analogous to the Read
type class, which can be reserved for debugging/development.
The parse
function takes any of the supported textual data types as an argument. Use the textual data type that is most natural in the implementation of parse
instances, and arguments are converted from other textual data types when necessary. The IsString
type class is not used, so use of the String
type is not required.
Here is an example instance for Username
, implementing some restrictions:
instance TTC.Parse Username where
parse = TTC.asT $ \t -> TTC.prefixErrorS "invalid username: " $ do
unless (T.all isAsciiLower t) $ Left "not only lowercase ASCII letters"
let len = T.length t
when (len < 3) $ Left "fewer than 3 characters"
when (len > 12) $ Left "more than 12 characters"
pure $ Username t
If a username needs to be parsed from a String
, conversion is automatic:
case TTC.parse s :: Either String Username of
Right uname -> "valid username: " ++ TTC.render uname
Left err -> err
For more details, see the Render and Parse article.
Constant Validation
TTC provides functions to validate constants at compile-time, using Template Haskell. For example, a Username
constant can be defined as follows:
user :: Username
user = $$(TTC.valid "tcard")
For more details, see the Validated Constants article.
Project
See the project README for general project information.