diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,1 @@
+# d10
diff --git a/d10.cabal b/d10.cabal
new file mode 100644
--- /dev/null
+++ b/d10.cabal
@@ -0,0 +1,93 @@
+name: d10
+version: 0.1.0.0
+category: Data
+synopsis: Digits 0-9
+
+description:
+    Data types representing the digits zero through nine.
+    .
+    == Modules
+    .
+    Each of the following modules defines a different type named
+    @D10@, all of which are different representations of the same
+    concept:
+    .
+    * "Data.D10.Char" - Defines a @D10@ type as a newtype for @Char@,
+      where the values are restricted to characters between @'0'@ and
+      @'9'@.
+    * "Data.D10.Num" - Defines a @D10@ type as a newtype for any
+      type with an instance of the @Num@ class, where the values
+      are restricted to numbers between @fromInteger 0@ and
+      @fromInteger 9@.
+    * "Data.D10.Safe" - Defines a @D10@ type as
+      @D0 | D1 | D2 | ... | D9@.
+    .
+    Other modules:
+    .
+    * "Data.D10.Predicate" - Functions to test whether values of
+      various types represent digits in the range /0/ to /9/.
+    .
+    == Quasi-quoters
+    .
+    Each module that defines a @D10@ type also defines quasi-quoters
+    for it. With the @QuasiQuotes@ GHC extension enabled, a single
+    digit like /7/ can be written as @[d10|7|]@, and a list of digits
+    like /[4,5,6]/ can be written as @[d10|456|]@. For "Data.D10.Char"
+    and "Data.D10.Num", the quasi-quoters are an important feature,
+    because the @D10@ types defined in these modules have unsafe
+    constructors, and the quasi-quoters provide compile-time assurance
+    that we never construct a @D10@ that represents a value outside
+    the range /0/ to /9/. For "Data.D10.Safe", the quasi-quoter is
+    offered merely as a possible convenience, allowing you to write
+    @[d10|456789|]@ in place of the somewhat longer expression
+    @[D4,D5,D6,D7,D8,D9]@.
+
+homepage:    https://github.com/typeclasses/d10
+bug-reports: https://github.com/typeclasses/d10/issues
+
+author:     Chris Martin
+maintainer: Chris Martin, Julie Moronuki
+
+copyright: 2018 Typeclass Consulting, LLC
+license: MIT
+license-file: license.txt
+
+build-type: Simple
+cabal-version: >= 1.10
+
+tested-with:
+    GHC == 8.2.2
+  , GHC == 8.4.3
+  , GHC == 8.6.1
+
+extra-source-files:
+    README.md
+
+source-repository head
+  type: git
+  location: https://github.com/typeclasses/d10
+
+library
+  default-language: Haskell2010
+  hs-source-dirs: src
+
+  build-depends:
+      base             >= 4.10 && < 4.13
+    , template-haskell >= 2.12 && < 2.15
+
+  exposed-modules:
+      Data.D10.Char
+    , Data.D10.Num
+    , Data.D10.Predicate
+    , Data.D10.Safe
+
+test-suite doctest
+  default-language: Haskell2010
+  hs-source-dirs: test
+  type: exitcode-stdio-1.0
+  main-is: doctest.hs
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+
+  build-depends:
+      base             >= 4.10 && < 4.13
+    , doctest          >= 0.13 && < 0.17
diff --git a/license.txt b/license.txt
new file mode 100644
--- /dev/null
+++ b/license.txt
@@ -0,0 +1,7 @@
+Copyright 2018 Typeclass Consulting, LLC
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/src/Data/D10/Char.hs b/src/Data/D10/Char.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/D10/Char.hs
@@ -0,0 +1,740 @@
+{-# LANGUAGE DeriveLift      #-}
+{-# LANGUAGE InstanceSigs    #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+-- | Defines a 'D10' type as a newtype for 'Char', where the
+-- values are restricted to characters between @'0'@ and @'9'@.
+
+module Data.D10.Char
+    (
+    -- * Type
+      D10 (..)
+    -- $bounded
+    -- $enum
+
+    -- * Quasi-quoters
+    , d10
+    , d10list
+
+    -- * Converting between D10 and Char
+    , d10Char
+    , charD10Maybe
+    , charD10Either
+    , charD10Fail
+
+    -- * Converting between D10 and String
+    , d10Str
+    , strD10Maybe
+    , strD10Either
+    , strD10Fail
+    , isD10Str
+
+    -- * Converting between [D10] and String
+    , strD10ListMaybe
+    , strD10ListEither
+    , strD10ListFail
+
+    -- * Converting between D10 and Natural
+    , d10Nat
+    , natD10Maybe
+    , natD10Either
+    , natD10Fail
+    , natMod10
+
+    -- * Converting between D10 and Integer
+    , d10Integer
+    , integerD10Maybe
+    , integerD10Either
+    , integerD10Fail
+    , integerMod10
+
+    -- * Converting between D10 and Int
+    , d10Int
+    , intD10Maybe
+    , intD10Either
+    , intD10Fail
+    , intMod10
+
+    -- * Converting between D10 and general numeric types
+    , d10Num
+    , integralD10Maybe
+    , integralD10Either
+    , integralD10Fail
+    , integralMod10
+
+    ) where
+
+import Data.D10.Predicate
+
+-- base
+import           Control.Monad              ((>=>))
+import           Control.Monad.Fail         (MonadFail (fail))
+import           Data.Char                  (chr, ord)
+import           Data.Monoid                (Endo (..))
+import           GHC.Generics               (Generic)
+import           Numeric.Natural            (Natural)
+import           Prelude                    hiding (fail)
+
+-- template-haskell
+import           Language.Haskell.TH        (ExpQ, Q)
+import           Language.Haskell.TH.Quote  (QuasiQuoter (QuasiQuoter))
+import           Language.Haskell.TH.Syntax (Lift (lift))
+
+-- $setup
+-- >>> :set -XQuasiQuotes
+
+---------------------------------------------------
+
+-- | A 'Char' value between @'0'@ and @'9'@.
+--
+-- The "Data.D10.Char" module provides many functions for
+-- constructing 'D10' values, including:
+--
+-- * @'integerD10Maybe' :: 'Integer' -> 'Maybe' 'D10'@
+-- * @'integerMod10' :: 'Integer' -> 'D10'@
+--
+-- With the @QuasiQuotes@ GHC extension enabled, you can write
+-- 'D10' literals using the quasi-quoters 'd10' and 'd10list'.
+
+newtype D10 =
+    D10_Unsafe Char
+      -- ^ The constructor's name include the word "unsafe" as a reminder
+      --   that you should generally avoid using it directly, because it
+      --   allows constructing invalid 'D10' values.
+    deriving (Eq, Ord, Lift)
+
+-- $bounded
+-- ==== Bounded
+--
+-- >>> minBound :: D10
+-- [d10|0|]
+--
+-- >>> maxBound :: D10
+-- [d10|9|]
+
+instance Bounded D10
+  where
+    minBound = D10_Unsafe '0'
+    maxBound = D10_Unsafe '9'
+
+-- $enum
+-- ==== Enum
+--
+-- >>> [ [d10|5|] .. ]
+-- [d10list|56789|]
+--
+-- >>> [ [d10|4|] .. [d10|7|] ]
+-- [d10list|4567|]
+--
+-- >>> [ [d10|5|], [d10|4|] .. ]
+-- [d10list|543210|]
+--
+-- >>> [ [d10|1|], [d10|3|] .. ]
+-- [d10list|13579|]
+--
+-- >>> [ minBound .. maxBound ] :: [D10]
+-- [d10list|0123456789|]
+
+instance Enum D10
+  where
+    fromEnum :: D10 -> Int
+    fromEnum = d10Int
+
+    toEnum :: Int -> D10
+    toEnum = either error id . intD10Either
+
+    enumFrom :: D10 -> [D10]
+    enumFrom x = enumFromTo x maxBound
+
+    enumFromThen :: D10 -> D10 -> [D10]
+    enumFromThen x y = enumFromThenTo x y bound
+      where
+        bound | fromEnum y >= fromEnum x = maxBound
+              | otherwise                = minBound
+
+    succ :: D10 -> D10
+    succ (D10_Unsafe '9') = error "D10 overflow"
+    succ (D10_Unsafe x) = D10_Unsafe (succ x)
+
+    pred :: D10 -> D10
+    pred (D10_Unsafe '0') = error "D10 underflow"
+    pred (D10_Unsafe x) = D10_Unsafe (pred x)
+
+---------------------------------------------------
+
+-- | Shows base-10 digits using the quasiquoters defined in
+-- "Data.D10.Char". A single digit is displayed using 'd10'.
+-- A list of digits is displayed using 'd10list'.
+
+instance Show D10 where
+    showsPrec _ x = showString "[d10|"     . showsChar x . showString "|]"
+    showList xs   = showString "[d10list|" . showsStr xs . showString "|]"
+
+showsChar :: D10 -> ShowS
+showsChar (D10_Unsafe x) = showChar x
+
+showsStr :: [D10] -> ShowS
+showsStr = appEndo . foldMap (Endo . showsChar)
+
+---------------------------------------------------
+
+-- | Convert a 'D10' to its underlying 'Char' representation.
+--
+-- >>> d10Char [d10|7|]
+-- '7'
+
+d10Char :: D10 -> Char
+d10Char (D10_Unsafe x) = x
+
+-- | Convert a 'D10' to a 'String'.
+--
+-- @'d10Str' x = ['d10Char' x]@
+--
+-- >>> d10Str [d10|7|]
+-- "7"
+
+d10Str :: D10 -> String
+d10Str (D10_Unsafe x) = [x]
+
+-- | Convert a 'D10' to a 'Natural'.
+--
+-- 'd10Num' is a more general version of this function.
+--
+-- >>> d10Nat [d10|7|]
+-- 7
+
+d10Nat :: D10 -> Natural
+d10Nat (D10_Unsafe x) = fromIntegral (ord x - ord '0')
+
+-- | Convert a 'D10' to an 'Integer'.
+--
+-- 'd10Num' is a more general version of this function.
+--
+-- >>> d10Integer [d10|7|]
+-- 7
+
+d10Integer :: D10 -> Integer
+d10Integer (D10_Unsafe x) = toInteger (ord x - ord '0')
+
+-- | Convert a 'D10' to an 'Int'.
+--
+-- 'd10Num' is a more general version of this function.
+--
+-- >>> d10Int [d10|7|]
+-- 7
+
+d10Int :: D10 -> Int
+d10Int (D10_Unsafe x) = ord x - ord '0'
+
+-- | Convert a 'D10' to any kind of number with a 'Num' instance.
+--
+-- Specialized versions of this function include 'd10Nat',
+-- 'd10Integer', and 'd10Int'.
+--
+-- >>> d10Num [d10|7|] :: Integer
+-- 7
+
+d10Num :: Num a => D10 -> a
+d10Num (D10_Unsafe x) = fromIntegral (ord x - ord '0')
+
+---------------------------------------------------
+
+-- | The 'D10' which is uniquely congruent modulo 10 to the given 'Natural'.
+--
+-- 'integralMod10' is a more general version of this function.
+--
+-- >>> natMod10 56
+-- [d10|6|]
+
+natMod10 :: Natural -> D10
+natMod10 x = D10_Unsafe (chr (ord '0' + fromIntegral (x `mod` 10)))
+
+-- | The 'D10' which is uniquely congruent modulo 10 to the given 'Integer'.
+--
+-- 'integralMod10' is a more general version of this function.
+--
+-- >>> integerMod10 56
+-- [d10|6|]
+--
+-- >>> integerMod10 (-56)
+-- [d10|4|]
+
+integerMod10 :: Integer -> D10
+integerMod10 x = D10_Unsafe (chr (ord '0' + fromInteger (x `mod` 10)))
+
+-- | The 'D10' which is uniquely congruent modulo 10 to the given 'Int'.
+--
+-- 'integralMod10' is a more general version of this function.
+--
+-- >>> intMod10 56
+-- [d10|6|]
+--
+-- >>> intMod10 (-56)
+-- [d10|4|]
+
+intMod10 :: Int -> D10
+intMod10 x = D10_Unsafe (chr (ord '0' + (x `mod` 10)))
+
+-- | The 'D10' which is uniquely congruent modulo 10 to the given number
+-- (whose type must have an instance of the 'Integral' class).
+--
+-- Specialized versions of this function include 'natMod10',
+-- 'integerMod10', and 'intMod10'.
+--
+-- >>> integralMod10 (56 :: Integer)
+-- [d10|6|]
+--
+-- >>> integralMod10 ((-56) :: Integer)
+-- [d10|4|]
+
+integralMod10 :: Integral a => a -> D10
+integralMod10 x = D10_Unsafe (chr (ord '0' + fromIntegral (x `mod` 10)))
+
+---------------------------------------------------
+
+-- | Convert a 'Char' to a 'D10' if it is within the range
+-- @'0'@ to @'9'@, or produce 'Nothing' otherwise.
+--
+-- @'Data.D10.Predicate.isD10Char' x = 'Data.Maybe.isJust' ('charD10Maybe' x)@
+--
+-- 'charD10Fail' is a more general version of this function.
+--
+-- >>> charD10Maybe '5'
+-- Just [d10|5|]
+--
+-- >>> charD10Maybe 'a'
+-- Nothing
+
+charD10Maybe :: Char -> Maybe D10
+charD10Maybe x
+        | isD10Char x  =  Just (D10_Unsafe x)
+        | otherwise    =  Nothing
+
+-- | Convert a 'String' to a 'D10' if it consists of exactly one
+-- character and that character is within the range @'0'@ to @'9'@,
+-- or produce 'Nothing' otherwise.
+--
+-- @'Data.D10.Predicate.isD10Str' x = 'Data.Maybe.isJust' ('strD10Maybe' x)@
+--
+-- 'strD10Fail' is a more general version of this function.
+--
+-- >>> strD10Maybe "5"
+-- Just [d10|5|]
+--
+-- >>> strD10Maybe "a"
+-- Nothing
+--
+-- >>> strD10Maybe "58"
+-- Nothing
+
+strD10Maybe :: String -> Maybe D10
+strD10Maybe [x] = charD10Maybe x
+strD10Maybe _   = Nothing
+
+-- | Convert a 'String' to a list of 'D10' if all of the characters
+-- in the string are within the range @'0'@ to @'9'@, or produce
+-- 'Nothing' otherwise.
+--
+-- @'Data.D10.Predicate.isD10Str' x = 'Data.Maybe.isJust' ('strD10ListMaybe' x)@
+--
+-- 'strD10ListFail' is a more general version of this function.
+--
+-- >>> strD10ListMaybe "5"
+-- Just [d10list|5|]
+--
+-- >>> strD10ListMaybe "a"
+-- Nothing
+--
+-- >>> strD10ListMaybe "58"
+-- Just [d10list|58|]
+
+strD10ListMaybe :: String -> Maybe [D10]
+strD10ListMaybe = traverse charD10Maybe
+
+-- | Convert a 'Natural' to a 'D10' if it is less than 10,
+-- or produce 'Nothing' otherwise.
+--
+-- @'Data.D10.Predicate.isD10Nat' x = 'Data.Maybe.isJust' ('natD10Maybe' x)@
+--
+-- 'integralD10Maybe', 'natD10Fail', and 'integralD10Fail'
+-- are more general versions of this function.
+--
+-- >>> natD10Maybe 5
+-- Just [d10|5|]
+--
+-- >>> natD10Maybe 12
+-- Nothing
+
+natD10Maybe :: Natural -> Maybe D10
+natD10Maybe x
+        | isD10Nat x  =  Just (D10_Unsafe (chr (fromIntegral x + ord '0')))
+        | otherwise   =  Nothing
+
+-- | Convert an 'Integer' to a 'D10' if it is within the range 0 to 9,
+-- or produce 'Nothing' otherwise.
+--
+-- @'Data.D10.Predicate.isD10Integer' x = 'Data.Maybe.isJust' ('integerD10Maybe' x)@
+--
+-- 'integralD10Maybe', 'integerD10Fail', and 'integralD10Fail'
+-- are more general versions of this function.
+--
+-- >>> integerD10Maybe 5
+-- Just [d10|5|]
+--
+-- >>> integerD10Maybe 12
+-- Nothing
+--
+-- >>> integerD10Maybe (-5)
+-- Nothing
+
+integerD10Maybe :: Integer -> Maybe D10
+integerD10Maybe x
+        | isD10Integer x  =  Just (D10_Unsafe (chr (fromInteger x + ord '0')))
+        | otherwise       =  Nothing
+
+-- | Convert an 'Int' to a 'D10' if it is within the range 0 to 9,
+-- or produce 'Nothing' otherwise.
+--
+-- @'Data.D10.Predicate.isD10Int' x = 'Data.Maybe.isJust' ('intD10Maybe' x)@
+--
+-- 'integralD10Maybe', 'intD10Fail', and 'integralD10Fail'
+-- are more general versions of this function.
+--
+-- >>> intD10Maybe 5
+-- Just [d10|5|]
+--
+-- >>> intD10Maybe 12
+-- Nothing
+--
+-- >>> intD10Maybe (-5)
+-- Nothing
+
+intD10Maybe :: Int -> Maybe D10
+intD10Maybe x
+        | isD10Int x  =  Just (D10_Unsafe (chr (x + ord '0')))
+        | otherwise   =  Nothing
+
+-- | Construct a 'D10' from any kind of number with an 'Integral'
+-- instance, or produce 'Nothing' if the number falls outside the
+-- range 0 to 9.
+--
+-- @'Data.D10.Predicate.isD10Integral' x = 'Data.Maybe.isJust' ('integralD10Maybe' x)@
+--
+-- Specialized versions of this function include 'natD10Maybe',
+-- 'integerD10Maybe', and 'intD10Maybe'.
+--
+-- 'integralD10Fail' is a more general version of this function.
+--
+-- >>> integralD10Maybe (5 :: Integer)
+-- Just [d10|5|]
+--
+-- >>> integralD10Maybe (12 :: Integer)
+-- Nothing
+--
+-- >>> integralD10Maybe ((-5) :: Integer)
+-- Nothing
+
+integralD10Maybe :: Integral a => a -> Maybe D10
+integralD10Maybe x = integerD10Maybe (toInteger x)
+
+---------------------------------------------------
+
+-- | Convert a 'Char' to a 'D10' if it is within the range
+-- @'0'@ to @'9'@, or 'Left' with an error message otherwise.
+--
+-- >>> charD10Either '5'
+-- Right [d10|5|]
+--
+-- >>> charD10Either 'a'
+-- Left "d10 must be between 0 and 9"
+
+charD10Either :: Char -> Either String D10
+charD10Either x
+        | isD10Char x  =  Right (D10_Unsafe x)
+        | otherwise    =  Left "d10 must be between 0 and 9"
+
+-- | Convert a 'String' to a 'D10' if it consists of a single
+-- character and that character is within the range @'0'@ to
+-- @'9'@, or 'Left' with an error message otherwise.
+--
+-- >>> strD10Either "5"
+-- Right [d10|5|]
+--
+-- >>> strD10Either "a"
+-- Left "d10 must be between 0 and 9"
+--
+-- >>> strD10Either "58"
+-- Left "d10 must be a single character"
+
+strD10Either :: String -> Either String D10
+strD10Either [x]         =  charD10Either x
+strD10Either _           =  Left "d10 must be a single character"
+
+-- | Convert a 'String' to a 'D10' if all of the characters in
+-- the string fall within the range @'0'@ to @'9'@, or 'Left'
+-- with an error message otherwise.
+--
+-- >>> strD10ListEither "5"
+-- Right [d10list|5|]
+--
+-- >>> strD10ListEither "a"
+-- Left "d10 must be between 0 and 9"
+--
+-- >>> strD10ListEither "58"
+-- Right [d10list|58|]
+
+strD10ListEither :: String -> Either String [D10]
+strD10ListEither = traverse charD10Either
+
+-- | Convert a 'Natural' to a 'D10' if it is less than 10,
+-- or 'Left' with an error message otherwise.
+--
+-- >>> natD10Either 5
+-- Right [d10|5|]
+--
+-- >>> natD10Either 12
+-- Left "d10 must be less than 10"
+
+natD10Either :: Natural -> Either String D10
+natD10Either x =
+    case (natD10Maybe x) of
+        Just y  -> Right y
+        Nothing -> Left "d10 must be less than 10"
+
+-- | Convert an 'Integer' to a 'D10' if it is within the
+-- range 0 to 9, or 'Left' with an error message otherwise.
+--
+-- >>> integerD10Either 5
+-- Right [d10|5|]
+--
+-- >>> integerD10Either 12
+-- Left "d10 must be between 0 and 9"
+--
+-- >>> integerD10Either (-5)
+-- Left "d10 must be between 0 and 9"
+
+integerD10Either :: Integer -> Either String D10
+integerD10Either x =
+    case (integerD10Maybe x) of
+        Just y  -> Right y
+        Nothing -> Left "d10 must be between 0 and 9"
+
+-- | Convert an 'Int' to a 'D10' if it is within the range
+-- 0 to 9, or 'Left' with an error message otherwise.
+--
+-- >>> intD10Either 5
+-- Right [d10|5|]
+--
+-- >>> intD10Either 12
+-- Left "d10 must be between 0 and 9"
+--
+-- >>> intD10Either (-5)
+-- Left "d10 must be between 0 and 9"
+
+intD10Either :: Int -> Either String D10
+intD10Either x =
+    case (intD10Maybe x) of
+        Just y  ->  Right y
+        Nothing ->  Left "d10 must be between 0 and 9"
+
+-- | Convert a number of a type that has an 'Integral' instance
+-- to a 'D10' if it falls within the range 0 to 9, or 'Left'
+-- with an error message otherwise.
+--
+-- >>> integralD10Either (5 :: Integer)
+-- Right [d10|5|]
+--
+-- >>> integralD10Either (12 :: Integer)
+-- Left "d10 must be between 0 and 9"
+--
+-- >>> integralD10Either ((-5) :: Integer)
+-- Left "d10 must be between 0 and 9"
+
+integralD10Either :: Integral a => a -> Either String D10
+integralD10Either x = integerD10Either (toInteger x)
+
+---------------------------------------------------
+
+-- | Convert a 'Char' to a 'D10' if it is within the range
+-- @'0'@ to @'9'@, or 'fail' with an error message otherwise.
+--
+-- 'charD10Maybe' is a specialized version of this function.
+--
+-- >>> charD10Fail '5' :: IO D10
+-- [d10|5|]
+--
+-- >>> charD10Fail 'a' :: IO D10
+-- *** Exception: user error (d10 must be between 0 and 9)
+
+charD10Fail :: MonadFail m => Char -> m D10
+charD10Fail x
+        | isD10Char x  =  return (D10_Unsafe x)
+        | otherwise    =  fail "d10 must be between 0 and 9"
+
+-- | Convert a 'String' to a 'D10' if it consists of a single
+-- character and that character is within the range @'0'@ to
+-- @'9'@, or 'fail' with an error message otherwise.
+--
+-- 'strD10Maybe' is a specialized version of this function.
+--
+-- >>> strD10Fail "5" :: IO D10
+-- [d10|5|]
+--
+-- >>> strD10Fail "a" :: IO D10
+-- *** Exception: user error (d10 must be between 0 and 9)
+--
+-- >>> strD10Fail "58" :: IO D10
+-- *** Exception: user error (d10 must be a single character)
+
+strD10Fail :: MonadFail m => String -> m D10
+strD10Fail [x]         =  charD10Fail x
+strD10Fail _           =  fail "d10 must be a single character"
+
+-- | Convert a 'String' to a 'D10' if all of the characters in
+-- the string fall within the range @'0'@ to @'9'@, or 'fail'
+-- with an error message otherwise.
+--
+-- 'strD10ListMaybe' is a specialized version of this function.
+--
+-- >>> strD10ListFail "5" :: IO [D10]
+-- [d10list|5|]
+--
+-- >>> strD10ListFail "a" :: IO [D10]
+-- *** Exception: user error (d10 must be between 0 and 9)
+--
+-- >>> strD10ListFail "58" :: IO [D10]
+-- [d10list|58|]
+
+strD10ListFail :: MonadFail m => String -> m [D10]
+strD10ListFail = traverse charD10Fail
+
+-- | Convert a 'Natural' to a 'D10' if it is less than 10,
+-- or 'fail' with an error message otherwise.
+--
+-- 'natD10Maybe' is a specialized version of this function.
+--
+-- 'integralD10Fail' is a more general version of this function.
+--
+-- >>> natD10Fail 5 :: IO D10
+-- [d10|5|]
+--
+-- >>> natD10Fail 12 :: IO D10
+-- *** Exception: user error (d10 must be less than 10)
+
+natD10Fail :: MonadFail m => Natural -> m D10
+natD10Fail x =
+    case (natD10Maybe x) of
+        Just y  -> return y
+        Nothing -> fail "d10 must be less than 10"
+
+-- | Convert an 'Integer' to a 'D10' if it is within the
+-- range 0 to 9, or 'fail' with an error message otherwise.
+--
+-- 'integerD10Maybe' is a specialized version of this function.
+--
+-- 'integralD10Fail' is a more general version of this function.
+--
+-- >>> integerD10Fail 5 :: IO D10
+-- [d10|5|]
+--
+-- >>> integerD10Fail 12 :: IO D10
+-- *** Exception: user error (d10 must be between 0 and 9)
+--
+-- >>> integerD10Fail (-5) :: IO D10
+-- *** Exception: user error (d10 must be between 0 and 9)
+
+integerD10Fail :: MonadFail m => Integer -> m D10
+integerD10Fail x =
+    case (integerD10Maybe x) of
+        Just y  -> return y
+        Nothing -> fail "d10 must be between 0 and 9"
+
+-- | Convert an 'Int' to a 'D10' if it is within the range
+-- 0 to 9, or 'fail' with an error message otherwise.
+--
+-- 'intD10Maybe' is a specialized version of this function.
+--
+-- 'integralD10Fail' is a more general version of this function.
+--
+-- >>> intD10Fail 5 :: IO D10
+-- [d10|5|]
+--
+-- >>> intD10Fail 12 :: IO D10
+-- *** Exception: user error (d10 must be between 0 and 9)
+--
+-- >>> intD10Fail (-5) :: IO D10
+-- *** Exception: user error (d10 must be between 0 and 9)
+
+intD10Fail :: MonadFail m => Int -> m D10
+intD10Fail x =
+    case (intD10Maybe x) of
+        Just y  ->  return y
+        Nothing ->  fail "d10 must be between 0 and 9"
+
+-- | Convert a number of a type that has an 'Integral' instance
+-- to a 'D10' if it falls within the range 0 to 9, or 'fail'
+-- with an error message otherwise.
+--
+-- 'natD10Maybe', 'integerD10Maybe', 'intD10Maybe',
+-- 'integralD10Maybe', 'natD10Fail', 'integerD10Fail', and
+-- 'intD10Fail' are all specialized versions of this function.
+--
+-- >>> integralD10Fail (5 :: Integer) :: IO D10
+-- [d10|5|]
+--
+-- >>> integralD10Fail (12 :: Integer) :: IO D10
+-- *** Exception: user error (d10 must be between 0 and 9)
+--
+-- >>> integralD10Fail ((-5) :: Integer) :: IO D10
+-- *** Exception: user error (d10 must be between 0 and 9)
+
+integralD10Fail :: (Integral a, MonadFail m) => a -> m D10
+integralD10Fail x = integerD10Fail (toInteger x)
+
+---------------------------------------------------
+
+qq :: Lift a => (String -> Q a) -> QuasiQuoter
+qq f = QuasiQuoter (f >=> lift) undefined undefined undefined
+
+-- | A single base-10 digit.
+--
+-- This quasi-quoter, when used as an expression, produces a
+-- value of type 'D10'.
+--
+-- >>> d10Nat [d10|5|]
+-- 5
+--
+-- >>> d10Nat [d10|a|]
+-- ...
+-- ... d10 must be between 0 and 9
+-- ...
+--
+-- >>> d10Nat [d10|58|]
+-- ...
+-- ... d10 must be a single character
+-- ...
+
+d10 :: QuasiQuoter
+d10 = qq strD10Fail
+
+-- | A list of base-10 digits.
+--
+-- This quasi-quoter, when used as an expression, produces a
+-- value of type @['D10']@.
+--
+-- >>> d10Nat <$> [d10list||]
+-- []
+--
+-- >>> d10Nat <$> [d10list|5|]
+-- [5]
+--
+-- >>> d10Nat <$> [d10list|58|]
+-- [5,8]
+--
+-- >>> d10Nat <$> [d10list|a|]
+-- ...
+-- ... d10 must be between 0 and 9
+-- ...
+
+d10list :: QuasiQuoter
+d10list = qq strD10ListFail
diff --git a/src/Data/D10/Num.hs b/src/Data/D10/Num.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/D10/Num.hs
@@ -0,0 +1,743 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE DeriveLift          #-}
+{-# LANGUAGE InstanceSigs        #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TemplateHaskell     #-}
+{-# LANGUAGE TypeApplications    #-}
+
+-- | Defines a 'D10' type as a newtype for any type with an
+-- instance of the 'Num' class, where the values are restricted
+-- to numbers between @'fromInteger' 0@ and @'fromInteger' 9@.
+
+module Data.D10.Num
+    (
+    -- * Type
+      D10 (..)
+    -- $bounded
+    -- $enum
+
+    -- * Quasi-quoters
+    , d10
+    , d10list
+
+    -- * Converting between D10 and Char
+    , d10Char
+    , charD10Maybe
+    , charD10Either
+    , charD10Fail
+
+    -- * Converting between D10 and String
+    , d10Str
+    , strD10Maybe
+    , strD10Either
+    , strD10Fail
+    , isD10Str
+
+    -- * Converting between [D10] and String
+    , strD10ListMaybe
+    , strD10ListEither
+    , strD10ListFail
+
+    -- * Converting between D10 and Natural
+    , d10Nat
+    , natD10Maybe
+    , natD10Either
+    , natD10Fail
+    , natMod10
+
+    -- * Converting between D10 and Integer
+    , d10Integer
+    , integerD10Maybe
+    , integerD10Either
+    , integerD10Fail
+    , integerMod10
+
+    -- * Converting between D10 and Int
+    , d10Int
+    , intD10Maybe
+    , intD10Either
+    , intD10Fail
+    , intMod10
+
+    -- * Converting between D10 and general numeric types
+    , d10Num
+    , integralD10Maybe
+    , integralD10Either
+    , integralD10Fail
+    , integralMod10
+
+    ) where
+
+import Data.D10.Predicate
+
+-- base
+import           Control.Monad              ((>=>))
+import           Control.Monad.Fail         (MonadFail (fail))
+import           Data.Char                  (chr, ord)
+import           Data.Monoid                (Endo (..))
+import           GHC.Generics               (Generic)
+import           Numeric.Natural            (Natural)
+import           Prelude                    hiding (fail)
+
+-- template-haskell
+import           Language.Haskell.TH        (ExpQ, Q)
+import           Language.Haskell.TH.Quote  (QuasiQuoter (QuasiQuoter))
+import           Language.Haskell.TH.Syntax (Lift (lift))
+
+-- $setup
+-- >>> :set -XQuasiQuotes
+
+---------------------------------------------------
+
+-- | A value of some numeric type @a@ between
+-- @'fromInteger' 0@ and @'fromInteger' 9@.
+--
+-- The "Data.D10.Num" module provides many functions for
+-- constructing 'D10' values, including:
+--
+-- * @'integerD10Maybe' :: 'Integer' -> 'Maybe' 'D10'@
+-- * @'integerMod10' :: 'Integer' -> 'D10'@
+--
+-- With the @QuasiQuotes@ GHC extension enabled, you can write
+-- 'D10' literals using the quasi-quoters 'd10' and 'd10list'.
+
+newtype D10 a =
+    D10_Unsafe a
+      -- ^ The constructor's name include the word "unsafe" as a reminder
+      --   that you should generally avoid using it directly, because it
+      --   allows constructing invalid 'D10' values.
+    deriving (Eq, Ord, Lift)
+
+-- $bounded
+-- ==== Bounded
+--
+-- >>> minBound :: D10 Integer
+-- [d10|0|]
+--
+-- >>> maxBound :: D10 Integer
+-- [d10|9|]
+
+instance Num a => Bounded (D10 a)
+  where
+    minBound = D10_Unsafe 0
+    maxBound = D10_Unsafe 9
+
+-- $enum
+-- ==== Enum
+--
+-- >>> [ [d10|5|] .. ]
+-- [d10list|56789|]
+--
+-- >>> [ [d10|4|] .. [d10|7|] ]
+-- [d10list|4567|]
+--
+-- >>> [ [d10|5|], [d10|4|] .. ]
+-- [d10list|543210|]
+--
+-- >>> [ [d10|1|], [d10|3|] .. ]
+-- [d10list|13579|]
+--
+-- >>> [ minBound .. maxBound ] :: [D10 Integer]
+-- [d10list|0123456789|]
+
+instance Integral a => Enum (D10 a)
+  where
+    fromEnum :: D10 a -> Int
+    fromEnum = d10Int
+
+    toEnum :: Int -> D10 a
+    toEnum = either error id . intD10Either
+
+    enumFrom :: D10 a -> [D10 a]
+    enumFrom x = enumFromTo x maxBound
+
+    enumFromThen :: D10 a -> D10 a -> [D10 a]
+    enumFromThen x y = enumFromThenTo x y bound
+      where
+        bound | fromEnum y >= fromEnum x = maxBound
+              | otherwise                = minBound
+
+    succ (D10_Unsafe 9) = error "D10 overflow"
+    succ (D10_Unsafe x) = D10_Unsafe (succ x)
+
+    pred (D10_Unsafe 0) = error "D10 underflow"
+    pred (D10_Unsafe x) = D10_Unsafe (pred x)
+
+---------------------------------------------------
+
+-- | Shows base-10 digits using the quasiquoters defined in
+-- "Data.D10.Char". A single digit is displayed using 'd10'.
+-- A list of digits is displayed using 'd10list'.
+
+instance Integral a => Show (D10 a) where
+    showsPrec _ x = showString "[d10|"     . showsChar x . showString "|]"
+    showList xs   = showString "[d10list|" . showsStr xs . showString "|]"
+
+showsChar :: Integral a => D10 a -> ShowS
+showsChar = showChar . d10Char
+
+showsStr :: Integral a => [D10 a] -> ShowS
+showsStr = appEndo . foldMap (Endo . showsChar)
+
+---------------------------------------------------
+
+-- | Convert a 'D10' to its underlying 'Char' representation.
+--
+-- >>> d10Char [d10|7|]
+-- '7'
+
+d10Char :: Integral a => D10 a -> Char
+d10Char (D10_Unsafe x) = chr (ord '0' + fromIntegral x)
+
+-- | Convert a 'D10' to a 'String'.
+--
+-- @'d10Str' x = ['d10Char' x]@
+--
+-- >>> d10Str [d10|7|]
+-- "7"
+
+d10Str :: Integral a => D10 a -> String
+d10Str x = [d10Char x]
+
+-- | Convert a 'D10' to a 'Natural'.
+--
+-- 'd10Num' is a more general version of this function.
+--
+-- >>> d10Nat [d10|7|]
+-- 7
+
+d10Nat :: Integral a => D10 a -> Natural
+d10Nat = d10Num
+
+-- | Convert a 'D10' to an 'Integer'.
+--
+-- 'd10Num' is a more general version of this function.
+--
+-- >>> d10Integer [d10|7|]
+-- 7
+
+d10Integer :: Integral a => D10 a -> Integer
+d10Integer = d10Num
+
+-- | Convert a 'D10' to an 'Int'.
+--
+-- 'd10Num' is a more general version of this function.
+--
+-- >>> d10Int [d10|7|]
+-- 7
+
+d10Int :: Integral a => D10 a -> Int
+d10Int = d10Num
+
+-- | Convert a 'D10' to any kind of number with a 'Num' instance.
+--
+-- Specialized versions of this function include 'd10Nat',
+-- 'd10Integer', and 'd10Int'.
+--
+-- >>> d10Num [d10|7|] :: Integer
+-- 7
+
+d10Num :: (Integral b, Num a) => D10 b -> a
+d10Num (D10_Unsafe x) = fromIntegral x
+
+---------------------------------------------------
+
+-- | The 'D10' which is uniquely congruent modulo 10 to the given 'Natural'.
+--
+-- 'integralMod10' is a more general version of this function.
+--
+-- >>> natMod10 56 :: D10 Int
+-- [d10|6|]
+
+natMod10 :: Num a => Natural -> D10 a
+natMod10 = integralMod10
+
+-- | The 'D10' which is uniquely congruent modulo 10 to the given 'Integer'.
+--
+-- 'integralMod10' is a more general version of this function.
+--
+-- >>> integerMod10 56 :: D10 Int
+-- [d10|6|]
+--
+-- >>> integerMod10 (-56) :: D10 Int
+-- [d10|4|]
+
+integerMod10 :: Num a => Integer -> D10 a
+integerMod10 = integralMod10
+
+-- | The 'D10' which is uniquely congruent modulo 10 to the given 'Int'.
+--
+-- 'integralMod10' is a more general version of this function.
+--
+-- >>> intMod10 56 :: D10 Int
+-- [d10|6|]
+--
+-- >>> intMod10 (-56) :: D10 Int
+-- [d10|4|]
+
+intMod10 :: Num a => Int -> D10 a
+intMod10 = integralMod10
+
+-- | The 'D10' which is uniquely congruent modulo 10 to the given number
+-- (whose type must have an instance of the 'Integral' class).
+--
+-- Specialized versions of this function include 'natMod10',
+-- 'integerMod10', and 'intMod10'.
+--
+-- >>> integralMod10 (56 :: Integer) :: D10 Int
+-- [d10|6|]
+--
+-- >>> integralMod10 ((-56) :: Integer) :: D10 Int
+-- [d10|4|]
+
+integralMod10 :: (Num b, Integral a) => a -> D10 b
+integralMod10 x = D10_Unsafe (fromIntegral (x `mod` 10))
+
+---------------------------------------------------
+
+-- | Convert a 'Char' to a 'D10' if it is within the range
+-- @'0'@ to @'9'@, or produce 'Nothing' otherwise.
+--
+-- @'Data.D10.Predicate.isD10Char' x = 'Data.Maybe.isJust' ('charD10Maybe' x)@
+--
+-- 'charD10Fail' is a more general version of this function.
+--
+-- >>> charD10Maybe '5'
+-- Just [d10|5|]
+--
+-- >>> charD10Maybe 'a'
+-- Nothing
+
+charD10Maybe :: Num a => Char -> Maybe (D10 a)
+charD10Maybe x
+        | isD10Char x  =  Just (D10_Unsafe (fromIntegral (ord x - ord '0')))
+        | otherwise    =  Nothing
+
+-- | Convert a 'String' to a 'D10' if it consists of exactly one
+-- character and that character is within the range @'0'@ to @'9'@,
+-- or produce 'Nothing' otherwise.
+--
+-- @'Data.D10.Predicate.isD10Str' x = 'Data.Maybe.isJust' ('strD10Maybe' x)@
+--
+-- 'strD10Fail' is a more general version of this function.
+--
+-- >>> strD10Maybe "5"
+-- Just [d10|5|]
+--
+-- >>> strD10Maybe "a"
+-- Nothing
+--
+-- >>> strD10Maybe "58"
+-- Nothing
+
+strD10Maybe :: Num a => String -> Maybe (D10 a)
+strD10Maybe [x] = charD10Maybe x
+strD10Maybe _   = Nothing
+
+-- | Convert a 'String' to a list of 'D10' if all of the characters
+-- in the string are within the range @'0'@ to @'9'@, or produce
+-- 'Nothing' otherwise.
+--
+-- @'Data.D10.Predicate.isD10Str' x = 'Data.Maybe.isJust' ('strD10ListMaybe' x)@
+--
+-- 'strD10ListFail' is a more general version of this function.
+--
+-- >>> strD10ListMaybe "5"
+-- Just [d10list|5|]
+--
+-- >>> strD10ListMaybe "a"
+-- Nothing
+--
+-- >>> strD10ListMaybe "58"
+-- Just [d10list|58|]
+
+strD10ListMaybe :: Num a => String -> Maybe [D10 a]
+strD10ListMaybe = traverse charD10Maybe
+
+-- | Convert a 'Natural' to a 'D10' if it is less than 10,
+-- or produce 'Nothing' otherwise.
+--
+-- @'Data.D10.Predicate.isD10Nat' x = 'Data.Maybe.isJust' ('natD10Maybe' x)@
+--
+-- 'integralD10Maybe', 'natD10Fail', and 'integralD10Fail'
+-- are more general versions of this function.
+--
+-- >>> natD10Maybe 5
+-- Just [d10|5|]
+--
+-- >>> natD10Maybe 12
+-- Nothing
+
+natD10Maybe :: Num a => Natural -> Maybe (D10 a)
+natD10Maybe x
+        | isD10Nat x  =  Just (D10_Unsafe (fromIntegral x))
+        | otherwise   =  Nothing
+
+-- | Convert an 'Integer' to a 'D10' if it is within the range 0 to 9,
+-- or produce 'Nothing' otherwise.
+--
+-- @'Data.D10.Predicate.isD10Integer' x = 'Data.Maybe.isJust' ('integerD10Maybe' x)@
+--
+-- 'integralD10Maybe', 'integerD10Fail', and 'integralD10Fail'
+-- are more general versions of this function.
+--
+-- >>> integerD10Maybe 5
+-- Just [d10|5|]
+--
+-- >>> integerD10Maybe 12
+-- Nothing
+--
+-- >>> integerD10Maybe (-5)
+-- Nothing
+
+integerD10Maybe :: Num a => Integer -> Maybe (D10 a)
+integerD10Maybe x
+        | isD10Integer x  =  Just (D10_Unsafe (fromIntegral x))
+        | otherwise       =  Nothing
+
+-- | Convert an 'Int' to a 'D10' if it is within the range 0 to 9,
+-- or produce 'Nothing' otherwise.
+--
+-- @'Data.D10.Predicate.isD10Int' x = 'Data.Maybe.isJust' ('intD10Maybe' x)@
+--
+-- 'integralD10Maybe', 'intD10Fail', and 'integralD10Fail'
+-- are more general versions of this function.
+--
+-- >>> intD10Maybe 5
+-- Just [d10|5|]
+--
+-- >>> intD10Maybe 12
+-- Nothing
+--
+-- >>> intD10Maybe (-5)
+-- Nothing
+
+intD10Maybe :: Num a => Int -> Maybe (D10 a)
+intD10Maybe x
+        | isD10Int x  =  Just (D10_Unsafe (fromIntegral x))
+        | otherwise   =  Nothing
+
+-- | Construct a 'D10' from any kind of number with an 'Integral'
+-- instance, or produce 'Nothing' if the number falls outside the
+-- range 0 to 9.
+--
+-- @'Data.D10.Predicate.isD10Integral' x = 'Data.Maybe.isJust' ('integralD10Maybe' x)@
+--
+-- Specialized versions of this function include 'natD10Maybe',
+-- 'integerD10Maybe', and 'intD10Maybe'.
+--
+-- 'integralD10Fail' is a more general version of this function.
+--
+-- >>> integralD10Maybe (5 :: Integer)
+-- Just [d10|5|]
+--
+-- >>> integralD10Maybe (12 :: Integer)
+-- Nothing
+--
+-- >>> integralD10Maybe ((-5) :: Integer)
+-- Nothing
+
+integralD10Maybe :: (Num b, Integral a) => a -> Maybe (D10 b)
+integralD10Maybe x = integerD10Maybe (toInteger x)
+
+---------------------------------------------------
+
+-- | Convert a 'Char' to a 'D10' if it is within the range
+-- @'0'@ to @'9'@, or 'Left' with an error message otherwise.
+--
+-- >>> charD10Either '5'
+-- Right [d10|5|]
+--
+-- >>> charD10Either 'a'
+-- Left "d10 must be between 0 and 9"
+
+charD10Either :: Num a => Char -> Either String (D10 a)
+charD10Either x
+        | isD10Char x  =  Right (D10_Unsafe (fromIntegral (ord x - ord '0')))
+        | otherwise    =  Left "d10 must be between 0 and 9"
+
+-- | Convert a 'String' to a 'D10' if it consists of a single
+-- character and that character is within the range @'0'@ to
+-- @'9'@, or 'Left' with an error message otherwise.
+--
+-- >>> strD10Either "5"
+-- Right [d10|5|]
+--
+-- >>> strD10Either "a"
+-- Left "d10 must be between 0 and 9"
+--
+-- >>> strD10Either "58"
+-- Left "d10 must be a single character"
+
+strD10Either :: Num a => String -> Either String (D10 a)
+strD10Either [x]         =  charD10Either x
+strD10Either _           =  Left "d10 must be a single character"
+
+-- | Convert a 'String' to a 'D10' if all of the characters in
+-- the string fall within the range @'0'@ to @'9'@, or 'Left'
+-- with an error message otherwise.
+--
+-- >>> strD10ListEither "5"
+-- Right [d10list|5|]
+--
+-- >>> strD10ListEither "a"
+-- Left "d10 must be between 0 and 9"
+--
+-- >>> strD10ListEither "58"
+-- Right [d10list|58|]
+
+strD10ListEither :: Num a => String -> Either String [D10 a]
+strD10ListEither = traverse charD10Either
+
+-- | Convert a 'Natural' to a 'D10' if it is less than 10,
+-- or 'Left' with an error message otherwise.
+--
+-- >>> natD10Either 5
+-- Right [d10|5|]
+--
+-- >>> natD10Either 12
+-- Left "d10 must be less than 10"
+
+natD10Either :: Num a => Natural -> Either String (D10 a)
+natD10Either x =
+    case (natD10Maybe x) of
+        Just y  -> Right y
+        Nothing -> Left "d10 must be less than 10"
+
+-- | Convert an 'Integer' to a 'D10' if it is within the
+-- range 0 to 9, or 'Left' with an error message otherwise.
+--
+-- >>> integerD10Either 5
+-- Right [d10|5|]
+--
+-- >>> integerD10Either 12
+-- Left "d10 must be between 0 and 9"
+--
+-- >>> integerD10Either (-5)
+-- Left "d10 must be between 0 and 9"
+
+integerD10Either :: Num a => Integer -> Either String (D10 a)
+integerD10Either x =
+    case (integerD10Maybe x) of
+        Just y  -> Right y
+        Nothing -> Left "d10 must be between 0 and 9"
+
+-- | Convert an 'Int' to a 'D10' if it is within the range
+-- 0 to 9, or 'Left' with an error message otherwise.
+--
+-- >>> intD10Either 5
+-- Right [d10|5|]
+--
+-- >>> intD10Either 12
+-- Left "d10 must be between 0 and 9"
+--
+-- >>> intD10Either (-5)
+-- Left "d10 must be between 0 and 9"
+
+intD10Either :: Num a => Int -> Either String (D10 a)
+intD10Either x =
+    case (intD10Maybe x) of
+        Just y  ->  Right y
+        Nothing ->  Left "d10 must be between 0 and 9"
+
+-- | Convert a number of a type that has an 'Integral' instance
+-- to a 'D10' if it falls within the range 0 to 9, or 'Left'
+-- with an error message otherwise.
+--
+-- >>> integralD10Either (5 :: Integer)
+-- Right [d10|5|]
+--
+-- >>> integralD10Either (12 :: Integer)
+-- Left "d10 must be between 0 and 9"
+--
+-- >>> integralD10Either ((-5) :: Integer)
+-- Left "d10 must be between 0 and 9"
+
+integralD10Either :: (Num b, Integral a) => a -> Either String (D10 b)
+integralD10Either x = integerD10Either (toInteger x)
+
+---------------------------------------------------
+
+-- | Convert a 'Char' to a 'D10' if it is within the range
+-- @'0'@ to @'9'@, or 'fail' with an error message otherwise.
+--
+-- 'charD10Maybe' is a specialized version of this function.
+--
+-- >>> charD10Fail '5' :: IO (D10 Int)
+-- [d10|5|]
+--
+-- >>> charD10Fail 'a' :: IO (D10 Int)
+-- *** Exception: user error (d10 must be between 0 and 9)
+
+charD10Fail :: (Num a, MonadFail m) => Char -> m (D10 a)
+charD10Fail x
+        | isD10Char x  =  return (D10_Unsafe (fromIntegral (ord x - ord '0')))
+        | otherwise    =  fail "d10 must be between 0 and 9"
+
+-- | Convert a 'String' to a 'D10' if it consists of a single
+-- character and that character is within the range @'0'@ to
+-- @'9'@, or 'fail' with an error message otherwise.
+--
+-- 'strD10Maybe' is a specialized version of this function.
+--
+-- >>> strD10Fail "5" :: IO (D10 Int)
+-- [d10|5|]
+--
+-- >>> strD10Fail "a" :: IO (D10 Int)
+-- *** Exception: user error (d10 must be between 0 and 9)
+--
+-- >>> strD10Fail "58" :: IO (D10 Int)
+-- *** Exception: user error (d10 must be a single character)
+
+strD10Fail :: (Num a, MonadFail m) => String -> m (D10 a)
+strD10Fail [x]         =  charD10Fail x
+strD10Fail _           =  fail "d10 must be a single character"
+
+-- | Convert a 'String' to a 'D10' if all of the characters in
+-- the string fall within the range @'0'@ to @'9'@, or 'fail'
+-- with an error message otherwise.
+--
+-- 'strD10ListMaybe' is a specialized version of this function.
+--
+-- >>> strD10ListFail "5" :: IO [D10 Int]
+-- [d10list|5|]
+--
+-- >>> strD10ListFail "a" :: IO [D10 Int]
+-- *** Exception: user error (d10 must be between 0 and 9)
+--
+-- >>> strD10ListFail "58" :: IO [D10 Int]
+-- [d10list|58|]
+
+strD10ListFail :: (Num a, MonadFail m) => String -> m [D10 a]
+strD10ListFail = traverse charD10Fail
+
+-- | Convert a 'Natural' to a 'D10' if it is less than 10,
+-- or 'fail' with an error message otherwise.
+--
+-- 'natD10Maybe' is a specialized version of this function.
+--
+-- 'integralD10Fail' is a more general version of this function.
+--
+-- >>> natD10Fail 5 :: IO (D10 Int)
+-- [d10|5|]
+--
+-- >>> natD10Fail 12 :: IO (D10 Int)
+-- *** Exception: user error (d10 must be less than 10)
+
+natD10Fail :: (Num a, MonadFail m) => Natural -> m (D10 a)
+natD10Fail x =
+    case (natD10Maybe x) of
+        Just y  -> return y
+        Nothing -> fail "d10 must be less than 10"
+
+-- | Convert an 'Integer' to a 'D10' if it is within the
+-- range 0 to 9, or 'fail' with an error message otherwise.
+--
+-- 'integerD10Maybe' is a specialized version of this function.
+--
+-- 'integralD10Fail' is a more general version of this function.
+--
+-- >>> integerD10Fail 5 :: IO (D10 Int)
+-- [d10|5|]
+--
+-- >>> integerD10Fail 12 :: IO (D10 Int)
+-- *** Exception: user error (d10 must be between 0 and 9)
+--
+-- >>> integerD10Fail (-5) :: IO (D10 Int)
+-- *** Exception: user error (d10 must be between 0 and 9)
+
+integerD10Fail :: (Num a, MonadFail m) => Integer -> m (D10 a)
+integerD10Fail x =
+    case (integerD10Maybe x) of
+        Just y  -> return y
+        Nothing -> fail "d10 must be between 0 and 9"
+
+-- | Convert an 'Int' to a 'D10' if it is within the range
+-- 0 to 9, or 'fail' with an error message otherwise.
+--
+-- 'intD10Maybe' is a specialized version of this function.
+--
+-- 'integralD10Fail' is a more general version of this function.
+--
+-- >>> intD10Fail 5 :: IO (D10 Int)
+-- [d10|5|]
+--
+-- >>> intD10Fail 12 :: IO (D10 Int)
+-- *** Exception: user error (d10 must be between 0 and 9)
+--
+-- >>> intD10Fail (-5) :: IO (D10 Int)
+-- *** Exception: user error (d10 must be between 0 and 9)
+
+intD10Fail :: (Num a, MonadFail m) => Int -> m (D10 a)
+intD10Fail x =
+    case (intD10Maybe x) of
+        Just y  ->  return y
+        Nothing ->  fail "d10 must be between 0 and 9"
+
+-- | Convert a number of a type that has an 'Integral' instance
+-- to a 'D10' if it falls within the range 0 to 9, or 'fail'
+-- with an error message otherwise.
+--
+-- 'natD10Maybe', 'integerD10Maybe', 'intD10Maybe',
+-- 'integralD10Maybe', 'natD10Fail', 'integerD10Fail', and
+-- 'intD10Fail' are all specialized versions of this function.
+--
+-- >>> integralD10Fail (5 :: Integer) :: IO (D10 Int)
+-- [d10|5|]
+--
+-- >>> integralD10Fail (12 :: Integer) :: IO (D10 Int)
+-- *** Exception: user error (d10 must be between 0 and 9)
+--
+-- >>> integralD10Fail ((-5) :: Integer) :: IO (D10 Int)
+-- *** Exception: user error (d10 must be between 0 and 9)
+
+integralD10Fail :: (Num b, Integral a, MonadFail m) => a -> m (D10 b)
+integralD10Fail x = integerD10Fail (toInteger x)
+
+---------------------------------------------------
+
+qq :: Lift a => (String -> Q a) -> QuasiQuoter
+qq f = QuasiQuoter (f >=> lift) undefined undefined undefined
+
+-- | A single base-10 digit.
+--
+-- This quasi-quoter, when used as an expression, produces a
+-- value of type @'D10' a@.
+--
+-- >>> d10Nat [d10|5|]
+-- 5
+--
+-- >>> d10Nat [d10|a|]
+-- ...
+-- ... d10 must be between 0 and 9
+-- ...
+--
+-- >>> d10Nat [d10|58|]
+-- ...
+-- ... d10 must be a single character
+-- ...
+
+d10 :: forall a. (Lift a, Num a) => QuasiQuoter
+d10 = qq @(D10 a) strD10Fail
+
+-- | A list of base-10 digits.
+--
+-- This quasi-quoter, when used as an expression, produces a
+-- value of type @['D10' a]@.
+--
+-- >>> d10Nat <$> [d10list||]
+-- []
+--
+-- >>> d10Nat <$> [d10list|5|]
+-- [5]
+--
+-- >>> d10Nat <$> [d10list|58|]
+-- [5,8]
+--
+-- >>> d10Nat <$> [d10list|a|]
+-- ...
+-- ... d10 must be between 0 and 9
+-- ...
+
+d10list :: forall a. (Lift a, Num a) => QuasiQuoter
+d10list = qq @[D10 a] strD10ListFail
diff --git a/src/Data/D10/Predicate.hs b/src/Data/D10/Predicate.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/D10/Predicate.hs
@@ -0,0 +1,45 @@
+-- | Functions to test whether values of various
+-- types represent digits in the range /0/ to /9/.
+
+module Data.D10.Predicate where
+
+import Numeric.Natural
+
+-- | Determines whether a 'Char' is in the range @'0'@ to @'9'@.
+
+isD10Char :: Char -> Bool
+isD10Char x = x >= '0' && x <= '9'
+
+-- | Determines whether a 'String' consists of a single character
+-- and that character is within the range @'0'@ to @'9'@.
+
+isD10Str :: String -> Bool
+isD10Str [x] = isD10Char x
+isD10Str _   = False
+
+-- | Determines whether a 'String' consists entirely of characters
+-- that are within the range @'0'@ to @'9'@.
+
+isD10ListStr :: String -> Bool
+isD10ListStr = all isD10Char
+
+-- | Determines whether a 'Natural' is in the range 0 to 9.
+
+isD10Nat :: Natural -> Bool
+isD10Nat x = x <= 9
+
+-- | Determines whether an 'Integer' is in the range 0 to 9.
+
+isD10Integer :: Integer -> Bool
+isD10Integer x = x >= 0 && x <= 9
+
+-- | Determines whether an 'Int' is in the range 0 to 9.
+
+isD10Int :: Int -> Bool
+isD10Int x = x >= 0 && x <= 9
+
+-- | Determines whether a number whose type has an 'Integral'
+-- instance is in the range 0 to 9.
+
+isD10Integral :: Integral a => a -> Bool
+isD10Integral x = isD10Integer (toInteger x)
diff --git a/src/Data/D10/Safe.hs b/src/Data/D10/Safe.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/D10/Safe.hs
@@ -0,0 +1,777 @@
+{-# LANGUAGE DeriveLift         #-}
+{-# LANGUAGE TemplateHaskell    #-}
+
+-- | Defines a 'D10' type as
+-- @'D0' | 'D1' | 'D2' | 'D3' | 'D4' | 'D5' | 'D6' | 'D7' | 'D8' | 'D9'@.
+--
+-- This module is called "safe" because, in contrast with
+-- the alternative representations of a digit defined in
+-- "Data.D10.Char" and "Data.D10.Num", this 'D10' type does
+-- not include any possibility of representing an invalid
+-- non-digit value.
+
+module Data.D10.Safe
+    (
+    -- * Type
+      D10 (..)
+    -- $bounded
+    -- $enum
+
+    -- * Quasi-quoters
+    , d10
+    , d10list
+
+    -- * Converting between D10 and Char
+    , d10Char
+    , charD10Maybe
+    , charD10Either
+    , charD10Fail
+
+    -- * Converting between D10 and String
+    , d10Str
+    , strD10Maybe
+    , strD10Either
+    , strD10Fail
+
+    -- * Converting between [D10] and String
+    , strD10ListMaybe
+    , strD10ListEither
+    , strD10ListFail
+
+    -- * Converting between D10 and Natural
+    , d10Nat
+    , natD10Maybe
+    , natD10Either
+    , natD10Fail
+    , natMod10
+
+    -- * Converting between D10 and Integer
+    , d10Integer
+    , integerD10Maybe
+    , integerD10Either
+    , integerD10Fail
+    , integerMod10
+
+    -- * Converting between D10 and Int
+    , d10Int
+    , intD10Maybe
+    , intD10Either
+    , intD10Fail
+    , intMod10
+
+    -- * Converting between D10 and general numeric types
+    , d10Num
+    , integralD10Maybe
+    , integralD10Either
+    , integralD10Fail
+    , integralMod10
+
+    ) where
+
+import Data.D10.Predicate
+
+-- base
+import           Control.Monad              ((>=>))
+import           Control.Monad.Fail         (MonadFail (fail))
+import           Data.Char                  (chr, ord)
+import           Data.Monoid                (Endo (..))
+import           GHC.Generics               (Generic)
+import           Numeric.Natural            (Natural)
+import           Prelude                    hiding (fail)
+
+-- template-haskell
+import           Language.Haskell.TH        (ExpQ, Q)
+import           Language.Haskell.TH.Quote  (QuasiQuoter (QuasiQuoter))
+import           Language.Haskell.TH.Syntax (Lift (lift))
+
+-- $setup
+-- >>> :set -XQuasiQuotes
+
+---------------------------------------------------
+
+-- | A whole number between /0/ and /9/.
+
+data D10
+    = D0  -- ^ Zero
+    | D1  -- ^ One
+    | D2  -- ^ Two
+    | D3  -- ^ Three
+    | D4  -- ^ Four
+    | D5  -- ^ Five
+    | D6  -- ^ Six
+    | D7  -- ^ Seven
+    | D8  -- ^ Eight
+    | D9  -- ^ Nine
+    deriving (Bounded, Enum, Eq, Lift, Ord, Show)
+
+-- $bounded
+-- ==== Bounded
+--
+-- >>> minBound :: D10
+-- D0
+--
+-- >>> maxBound :: D10
+-- D9
+
+-- $enum
+-- ==== Enum
+--
+-- >>> [ D5 .. ]
+-- [D5,D6,D7,D8,D9]
+--
+-- >>> [ D4 .. D7 ]
+-- [D4,D5,D6,D7]
+--
+-- >>> [ D5, D4 .. ]
+-- [D5,D4,D3,D2,D1,D0]
+--
+-- >>> [ D1, D3 .. ]
+-- [D1,D3,D5,D7,D9]
+--
+-- >>> [ minBound .. maxBound ] :: [D10]
+-- [D0,D1,D2,D3,D4,D5,D6,D7,D8,D9]
+
+---------------------------------------------------
+
+-- | Convert a 'D10' to its underlying 'Char' representation.
+--
+-- >>> d10Char D7
+-- '7'
+
+d10Char :: D10 -> Char
+d10Char x =
+    case x of
+        D0 -> '0'
+        D1 -> '1'
+        D2 -> '2'
+        D3 -> '3'
+        D4 -> '4'
+        D5 -> '5'
+        D6 -> '6'
+        D7 -> '7'
+        D8 -> '8'
+        D9 -> '9'
+
+-- | Convert a 'D10' to a 'String'.
+--
+-- @'d10Str' x = ['d10Char' x]@
+--
+-- >>> d10Str D7
+-- "7"
+
+d10Str :: D10 -> String
+d10Str x = [d10Char x]
+
+-- | Convert a 'D10' to a 'Natural'.
+--
+-- 'd10Num' is a more general version of this function.
+--
+-- >>> d10Nat D7
+-- 7
+
+d10Nat :: D10 -> Natural
+d10Nat = d10Num
+
+-- | Convert a 'D10' to an 'Integer'.
+--
+-- 'd10Num' is a more general version of this function.
+--
+-- >>> d10Integer D7
+-- 7
+
+d10Integer :: D10 -> Integer
+d10Integer = d10Num
+
+-- | Convert a 'D10' to an 'Int'.
+--
+-- 'd10Num' is a more general version of this function.
+--
+-- >>> d10Int D7
+-- 7
+
+d10Int :: D10 -> Int
+d10Int = d10Num
+
+-- | Convert a 'D10' to any kind of number with a 'Num' instance.
+--
+-- Specialized versions of this function include 'd10Nat',
+-- 'd10Integer', and 'd10Int'.
+--
+-- >>> d10Num D7 :: Integer
+-- 7
+
+d10Num :: Num a => D10 -> a
+d10Num x =
+    case x of
+        D0 -> 0
+        D1 -> 1
+        D2 -> 2
+        D3 -> 3
+        D4 -> 4
+        D5 -> 5
+        D6 -> 6
+        D7 -> 7
+        D8 -> 8
+        D9 -> 9
+
+---------------------------------------------------
+
+-- | The 'D10' which is uniquely congruent modulo 10 to the given 'Natural'.
+--
+-- 'integralMod10' is a more general version of this function.
+--
+-- >>> natMod10 56
+-- D6
+
+natMod10 :: Natural -> D10
+natMod10 = integralMod10
+
+-- | The 'D10' which is uniquely congruent modulo 10 to the given 'Integer'.
+--
+-- 'integralMod10' is a more general version of this function.
+--
+-- >>> integerMod10 56
+-- D6
+--
+-- >>> integerMod10 (-56)
+-- D4
+
+integerMod10 :: Integer -> D10
+integerMod10 = integralMod10
+
+-- | The 'D10' which is uniquely congruent modulo 10 to the given 'Int'.
+--
+-- 'integralMod10' is a more general version of this function.
+--
+-- >>> intMod10 56
+-- D6
+--
+-- >>> intMod10 (-56)
+-- D4
+
+intMod10 :: Int -> D10
+intMod10 = integralMod10
+
+-- | The 'D10' which is uniquely congruent modulo 10 to the given number
+-- (whose type must have an instance of the 'Integral' class).
+--
+-- Specialized versions of this function include 'natMod10',
+-- 'integerMod10', and 'intMod10'.
+--
+-- >>> integralMod10 (56 :: Integer)
+-- D6
+--
+-- >>> integralMod10 ((-56) :: Integer)
+-- D4
+
+integralMod10 :: Integral a => a -> D10
+integralMod10 x =
+    case (x `mod` 10) of
+        0 -> D0
+        1 -> D1
+        2 -> D2
+        3 -> D3
+        4 -> D4
+        5 -> D5
+        6 -> D6
+        7 -> D7
+        8 -> D8
+        9 -> D9
+        _ -> error "x `mod` 10 is not between 0 and 9"
+
+---------------------------------------------------
+
+-- | Convert a 'Char' to a 'D10' if it is within the range
+-- @'0'@ to @'9'@, or produce 'Nothing' otherwise.
+--
+-- @'Data.D10.Predicate.isD10Char' x = 'Data.Maybe.isJust' ('charD10Maybe' x)@
+--
+-- 'charD10Fail' is a more general version of this function.
+--
+-- >>> charD10Maybe '5'
+-- Just D5
+--
+-- >>> charD10Maybe 'a'
+-- Nothing
+
+charD10Maybe :: Char -> Maybe D10
+charD10Maybe x =
+    case x of
+        '0' -> Just D0
+        '1' -> Just D1
+        '2' -> Just D2
+        '3' -> Just D3
+        '4' -> Just D4
+        '5' -> Just D5
+        '6' -> Just D6
+        '7' -> Just D7
+        '8' -> Just D8
+        '9' -> Just D9
+        _   -> Nothing
+
+-- | Convert a 'String' to a 'D10' if it consists of exactly one
+-- character and that character is within the range @'0'@ to @'9'@,
+-- or produce 'Nothing' otherwise.
+--
+-- @'Data.D10.Predicate.isD10Str' x = 'Data.Maybe.isJust' ('strD10Maybe' x)@
+--
+-- 'strD10Fail' is a more general version of this function.
+--
+-- >>> strD10Maybe "5"
+-- Just D5
+--
+-- >>> strD10Maybe "a"
+-- Nothing
+--
+-- >>> strD10Maybe "58"
+-- Nothing
+
+strD10Maybe :: String -> Maybe D10
+strD10Maybe [x] = charD10Maybe x
+strD10Maybe _   = Nothing
+
+-- | Convert a 'String' to a list of 'D10' if all of the characters
+-- in the string are within the range @'0'@ to @'9'@, or produce
+-- 'Nothing' otherwise.
+--
+-- @'Data.D10.Predicate.isD10Str' x = 'Data.Maybe.isJust' ('strD10ListMaybe' x)@
+--
+-- 'strD10ListFail' is a more general version of this function.
+--
+-- >>> strD10ListMaybe "5"
+-- Just [D5]
+--
+-- >>> strD10ListMaybe "a"
+-- Nothing
+--
+-- >>> strD10ListMaybe "58"
+-- Just [D5,D8]
+
+strD10ListMaybe :: String -> Maybe [D10]
+strD10ListMaybe = traverse charD10Maybe
+
+-- | Convert a 'Natural' to a 'D10' if it is less than 10,
+-- or produce 'Nothing' otherwise.
+--
+-- @'Data.D10.Predicate.isD10Nat' x = 'Data.Maybe.isJust' ('natD10Maybe' x)@
+--
+-- 'integralD10Maybe', 'natD10Fail', and 'integralD10Fail'
+-- are more general versions of this function.
+--
+-- >>> natD10Maybe 5
+-- Just D5
+--
+-- >>> natD10Maybe 12
+-- Nothing
+
+natD10Maybe :: Natural -> Maybe D10
+natD10Maybe = integralD10Maybe
+
+-- | Convert an 'Integer' to a 'D10' if it is within the range 0 to 9,
+-- or produce 'Nothing' otherwise.
+--
+-- @'Data.D10.Predicate.isD10Integer' x = 'Data.Maybe.isJust' ('integerD10Maybe' x)@
+--
+-- 'integralD10Maybe', 'integerD10Fail', and 'integralD10Fail'
+-- are more general versions of this function.
+--
+-- >>> integerD10Maybe 5
+-- Just D5
+--
+-- >>> integerD10Maybe 12
+-- Nothing
+--
+-- >>> integerD10Maybe (-5)
+-- Nothing
+
+integerD10Maybe :: Integer -> Maybe D10
+integerD10Maybe = integralD10Maybe
+
+-- | Convert an 'Int' to a 'D10' if it is within the range 0 to 9,
+-- or produce 'Nothing' otherwise.
+--
+-- @'Data.D10.Predicate.isD10Int' x = 'Data.Maybe.isJust' ('intD10Maybe' x)@
+--
+-- 'integralD10Maybe', 'intD10Fail', and 'integralD10Fail'
+-- are more general versions of this function.
+--
+-- >>> intD10Maybe 5
+-- Just D5
+--
+-- >>> intD10Maybe 12
+-- Nothing
+--
+-- >>> intD10Maybe (-5)
+-- Nothing
+
+intD10Maybe :: Int -> Maybe D10
+intD10Maybe = integralD10Maybe
+
+-- | Construct a 'D10' from any kind of number with an 'Integral'
+-- instance, or produce 'Nothing' if the number falls outside the
+-- range 0 to 9.
+--
+-- @'Data.D10.Predicate.isD10Integral' x = 'Data.Maybe.isJust' ('integralD10Maybe' x)@
+--
+-- Specialized versions of this function include 'natD10Maybe',
+-- 'integerD10Maybe', and 'intD10Maybe'.
+--
+-- 'integralD10Fail' is a more general version of this function.
+--
+-- >>> integralD10Maybe (5 :: Integer)
+-- Just D5
+--
+-- >>> integralD10Maybe (12 :: Integer)
+-- Nothing
+--
+-- >>> integralD10Maybe ((-5) :: Integer)
+-- Nothing
+
+integralD10Maybe :: Integral a => a -> Maybe D10
+integralD10Maybe x =
+    case x of
+        0 -> Just D0
+        1 -> Just D1
+        2 -> Just D2
+        3 -> Just D3
+        4 -> Just D4
+        5 -> Just D5
+        6 -> Just D6
+        7 -> Just D7
+        8 -> Just D8
+        9 -> Just D9
+        _ -> Nothing
+
+---------------------------------------------------
+
+-- | Convert a 'Char' to a 'D10' if it is within the range
+-- @'0'@ to @'9'@, or 'Left' with an error message otherwise.
+--
+-- >>> charD10Either '5'
+-- Right D5
+--
+-- >>> charD10Either 'a'
+-- Left "d10 must be between 0 and 9"
+
+charD10Either :: Char -> Either String D10
+charD10Either x =
+    case x of
+        '0' -> Right D0
+        '1' -> Right D1
+        '2' -> Right D2
+        '3' -> Right D3
+        '4' -> Right D4
+        '5' -> Right D5
+        '6' -> Right D6
+        '7' -> Right D7
+        '8' -> Right D8
+        '9' -> Right D9
+        _   -> Left "d10 must be between 0 and 9"
+
+-- | Convert a 'String' to a 'D10' if it consists of a single
+-- character and that character is within the range @'0'@ to
+-- @'9'@, or 'Left' with an error message otherwise.
+--
+-- >>> strD10Either "5"
+-- Right D5
+--
+-- >>> strD10Either "a"
+-- Left "d10 must be between 0 and 9"
+--
+-- >>> strD10Either "58"
+-- Left "d10 must be a single character"
+
+strD10Either :: String -> Either String D10
+strD10Either [x] = charD10Either x
+strD10Either _   = Left "d10 must be a single character"
+
+-- | Convert a 'String' to a 'D10' if all of the characters in
+-- the string fall within the range @'0'@ to @'9'@, or 'Left'
+-- with an error message otherwise.
+--
+-- >>> strD10ListEither "5"
+-- Right [D5]
+--
+-- >>> strD10ListEither "a"
+-- Left "d10 must be between 0 and 9"
+--
+-- >>> strD10ListEither "58"
+-- Right [D5,D8]
+
+strD10ListEither :: String -> Either String [D10]
+strD10ListEither = traverse charD10Either
+
+-- | Convert a 'Natural' to a 'D10' if it is less than 10,
+-- or 'Left' with an error message otherwise.
+--
+-- >>> natD10Either 5
+-- Right D5
+--
+-- >>> natD10Either 12
+-- Left "d10 must be less than 10"
+
+natD10Either :: Natural -> Either String D10
+natD10Either x =
+    case x of
+        0 -> Right D0
+        1 -> Right D1
+        2 -> Right D2
+        3 -> Right D3
+        4 -> Right D4
+        5 -> Right D5
+        6 -> Right D6
+        7 -> Right D7
+        8 -> Right D8
+        9 -> Right D9
+        _ -> Left "d10 must be less than 10"
+
+-- | Convert an 'Integer' to a 'D10' if it is within the
+-- range 0 to 9, or 'Left' with an error message otherwise.
+--
+-- >>> integerD10Either 5
+-- Right D5
+--
+-- >>> integerD10Either 12
+-- Left "d10 must be between 0 and 9"
+--
+-- >>> integerD10Either (-5)
+-- Left "d10 must be between 0 and 9"
+
+integerD10Either :: Integer -> Either String D10
+integerD10Either = integralD10Either
+
+-- | Convert an 'Int' to a 'D10' if it is within the range
+-- 0 to 9, or 'Left' with an error message otherwise.
+--
+-- >>> intD10Either 5
+-- Right D5
+--
+-- >>> intD10Either 12
+-- Left "d10 must be between 0 and 9"
+--
+-- >>> intD10Either (-5)
+-- Left "d10 must be between 0 and 9"
+
+intD10Either :: Int -> Either String D10
+intD10Either = integralD10Either
+
+-- | Convert a number of a type that has an 'Integral' instance
+-- to a 'D10' if it falls within the range 0 to 9, or 'Left'
+-- with an error message otherwise.
+--
+-- >>> integralD10Either (5 :: Integer)
+-- Right D5
+--
+-- >>> integralD10Either (12 :: Integer)
+-- Left "d10 must be between 0 and 9"
+--
+-- >>> integralD10Either ((-5) :: Integer)
+-- Left "d10 must be between 0 and 9"
+
+integralD10Either :: Integral a => a -> Either String D10
+integralD10Either x =
+    case (integralD10Maybe x) of
+        Just y  -> Right y
+        Nothing -> Left "d10 must be between 0 and 9"
+
+---------------------------------------------------
+
+-- | Convert a 'Char' to a 'D10' if it is within the range
+-- @'0'@ to @'9'@, or 'fail' with an error message otherwise.
+--
+-- 'charD10Maybe' is a specialized version of this function.
+--
+-- >>> charD10Fail '5' :: IO D10
+-- D5
+--
+-- >>> charD10Fail 'a' :: IO D10
+-- *** Exception: user error (d10 must be between 0 and 9)
+
+charD10Fail :: MonadFail m => Char -> m D10
+charD10Fail x =
+    case x of
+        '0' -> return D0
+        '1' -> return D1
+        '2' -> return D2
+        '3' -> return D3
+        '4' -> return D4
+        '5' -> return D5
+        '6' -> return D6
+        '7' -> return D7
+        '8' -> return D8
+        '9' -> return D9
+        _   -> fail "d10 must be between 0 and 9"
+
+-- | Convert a 'String' to a 'D10' if it consists of a single
+-- character and that character is within the range @'0'@ to
+-- @'9'@, or 'fail' with an error message otherwise.
+--
+-- 'strD10Maybe' is a specialized version of this function.
+--
+-- >>> strD10Fail "5" :: IO D10
+-- D5
+--
+-- >>> strD10Fail "a" :: IO D10
+-- *** Exception: user error (d10 must be between 0 and 9)
+--
+-- >>> strD10Fail "58" :: IO D10
+-- *** Exception: user error (d10 must be a single character)
+
+strD10Fail :: MonadFail m => String -> m D10
+strD10Fail [x] = charD10Fail x
+strD10Fail _   = fail "d10 must be a single character"
+
+-- | Convert a 'String' to a 'D10' if all of the characters in
+-- the string fall within the range @'0'@ to @'9'@, or 'fail'
+-- with an error message otherwise.
+--
+-- 'strD10ListMaybe' is a specialized version of this function.
+--
+-- >>> strD10ListFail "5" :: IO [D10]
+-- [D5]
+--
+-- >>> strD10ListFail "a" :: IO [D10]
+-- *** Exception: user error (d10 must be between 0 and 9)
+--
+-- >>> strD10ListFail "58" :: IO [D10]
+-- [D5,D8]
+
+strD10ListFail :: MonadFail m => String -> m [D10]
+strD10ListFail = traverse charD10Fail
+
+-- | Convert a 'Natural' to a 'D10' if it is less than 10,
+-- or 'fail' with an error message otherwise.
+--
+-- 'natD10Maybe' is a specialized version of this function.
+--
+-- 'integralD10Fail' is a more general version of this function.
+--
+-- >>> natD10Fail 5 :: IO D10
+-- D5
+--
+-- >>> natD10Fail 12 :: IO D10
+-- *** Exception: user error (d10 must be less than 10)
+
+natD10Fail :: MonadFail m => Natural -> m D10
+natD10Fail x =
+    case x of
+        0 -> return D0
+        1 -> return D1
+        2 -> return D2
+        3 -> return D3
+        4 -> return D4
+        5 -> return D5
+        6 -> return D6
+        7 -> return D7
+        8 -> return D8
+        9 -> return D9
+        _ -> fail "d10 must be less than 10"
+
+-- | Convert an 'Integer' to a 'D10' if it is within the
+-- range 0 to 9, or 'fail' with an error message otherwise.
+--
+-- 'integerD10Maybe' is a specialized version of this function.
+--
+-- 'integralD10Fail' is a more general version of this function.
+--
+-- >>> integerD10Fail 5 :: IO D10
+-- D5
+--
+-- >>> integerD10Fail 12 :: IO D10
+-- *** Exception: user error (d10 must be between 0 and 9)
+--
+-- >>> integerD10Fail (-5) :: IO D10
+-- *** Exception: user error (d10 must be between 0 and 9)
+
+integerD10Fail :: MonadFail m => Integer -> m D10
+integerD10Fail = integralD10Fail
+
+-- | Convert an 'Int' to a 'D10' if it is within the range
+-- 0 to 9, or 'fail' with an error message otherwise.
+--
+-- 'intD10Maybe' is a specialized version of this function.
+--
+-- 'integralD10Fail' is a more general version of this function.
+--
+-- >>> intD10Fail 5 :: IO D10
+-- D5
+--
+-- >>> intD10Fail 12 :: IO D10
+-- *** Exception: user error (d10 must be between 0 and 9)
+--
+-- >>> intD10Fail (-5) :: IO D10
+-- *** Exception: user error (d10 must be between 0 and 9)
+
+intD10Fail :: MonadFail m => Int -> m D10
+intD10Fail = integralD10Fail
+
+-- | Convert a number of a type that has an 'Integral' instance
+-- to a 'D10' if it falls within the range 0 to 9, or 'fail'
+-- with an error message otherwise.
+--
+-- 'natD10Maybe', 'integerD10Maybe', 'intD10Maybe',
+-- 'integralD10Maybe', 'natD10Fail', 'integerD10Fail', and
+-- 'intD10Fail' are all specialized versions of this function.
+--
+-- >>> integralD10Fail (5 :: Integer) :: IO D10
+-- D5
+--
+-- >>> integralD10Fail (12 :: Integer) :: IO D10
+-- *** Exception: user error (d10 must be between 0 and 9)
+--
+-- >>> integralD10Fail ((-5) :: Integer) :: IO D10
+-- *** Exception: user error (d10 must be between 0 and 9)
+
+integralD10Fail :: (Integral a, MonadFail m) => a -> m D10
+integralD10Fail x =
+    case (integralD10Maybe x) of
+        Just y  -> return y
+        Nothing -> fail "d10 must be between 0 and 9"
+
+---------------------------------------------------
+
+qq :: Lift a => (String -> Q a) -> QuasiQuoter
+qq f = QuasiQuoter (f >=> lift) undefined undefined undefined
+
+-- | A single base-10 digit.
+--
+-- This quasi-quoter, when used as an expression, produces a
+-- value of type 'D10'.
+--
+-- >>> [d10|5|]
+-- D5
+--
+-- >>> [d10|a|]
+-- ...
+-- ... d10 must be between 0 and 9
+-- ...
+--
+-- >>> [d10|58|]
+-- ...
+-- ... d10 must be a single character
+-- ...
+
+d10 :: QuasiQuoter
+d10 = qq strD10Fail
+
+-- | A list of base-10 digits.
+--
+-- This quasi-quoter, when used as an expression, produces a
+-- value of type @['D10']@.
+--
+-- >>> [d10list||]
+-- []
+--
+-- >>> [d10list|5|]
+-- [D5]
+--
+-- >>> [d10list|58|]
+-- [D5,D8]
+--
+-- >>> [d10list|a|]
+-- ...
+-- ... d10 must be between 0 and 9
+-- ...
+
+d10list :: QuasiQuoter
+d10list = qq strD10ListFail
diff --git a/test/doctest.hs b/test/doctest.hs
new file mode 100644
--- /dev/null
+++ b/test/doctest.hs
@@ -0,0 +1,11 @@
+import           Test.DocTest
+
+main :: IO ()
+main =
+  doctest
+    [ "-isrc"
+    , "src/Data/D10/Char.hs"
+    , "src/Data/D10/Num.hs"
+    , "src/Data/D10/Predicate.hs"
+    , "src/Data/D10/Safe.hs"
+    ]
