diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -40,3 +40,56 @@
 **v0.2.1.6**
 
   * Support `doctest-0.17`
+
+**v0.3**
+
+In module `Data.D10.Char`:
+
+  * Removed `isD10Str` (see `Data.D10.Predicate`)
+  * Type of `d10Exp` changed
+    from `Integral a => a -> Q Exp`
+    to `Integer -> Q Exp`
+  * Type of `d10Pat` changed
+    from `D10 -> Q Pat`
+    to `Integer -> Q Pat`
+  * Type of `d10ListPat` changed
+    from `[D10] -> Q Pat`
+    to `String -> Q Pat`
+
+In module `Data.D10.Num`:
+
+  * Removed `isD10Str` (see `Data.D10.Predicate`)
+  * Type of `d10Exp` changed
+    from `(Integral b, Lift a, Num a) => b -> Q Exp`
+    to `Integer -> Q Exp`
+  * Type of `d10ListExp` changed
+    from `(Lift a, Num a) => String -> Q Exp`
+    to `String -> Q Exp`
+  * Type of `d10Pat` changed
+    from `Integral a => D10 a -> Q Pat`
+    to `Integer -> Q Pat`
+  * Type of `d10ListPat` changed
+    from `Integral a => [D10 a] -> Q Pat`
+    to `String -> Q Pat`
+  * Type of `d10` changed
+    from `(Lift a, Integral a) => QuasiQuoter`
+    to `QuasiQuoter`
+  * Type of `d10list` changed
+    from `(Lift a, Integral a) => QuasiQuoter`
+    to `QuasiQuoter`
+  * Although type variables no longer appear in the
+    various Template Haskell functions, the expressions
+    and patterns they generate are polymorphic.
+
+In module `Data.D10.Safe`:
+
+  * The `D10` type now has instances of the `Data` and `Generic`.
+  * Type of `d10ListPat` changed
+    from `[D10] -> Q Pat`
+    to `String -> Q Pat`
+  * Removed `d10Exp`, `d10Pat`, and `d10`
+
+Other changes:
+
+  * `doctest` test dependency has been removed, and
+    `hedgehog` dependency has been added instead
diff --git a/d10.cabal b/d10.cabal
--- a/d10.cabal
+++ b/d10.cabal
@@ -1,5 +1,7 @@
+cabal-version: 2.2
+
 name: d10
-version: 0.2.1.6
+version: 0.3
 category: Data
 synopsis: Digits 0-9
 
@@ -48,12 +50,11 @@
 author:     Chris Martin
 maintainer: Chris Martin, Julie Moronuki
 
-copyright: 2018 Typeclass Consulting, LLC
+copyright: 2018-2021 Mission Valley Software LLC
 license: MIT
 license-file: license.txt
 
 build-type: Simple
-cabal-version: >= 1.10
 
 tested-with:
     GHC == 8.2.2
@@ -72,11 +73,15 @@
 
 library
   default-language: Haskell2010
+  ghc-options: -Wall
   hs-source-dirs: src
 
   build-depends:
-      base             >= 4.10 && < 4.15
-    , template-haskell >= 2.12 && < 2.17
+      base ^>= 4.10 || ^>= 4.11 || ^>= 4.12
+        || ^>= 4.13 || ^>= 4.14
+    , template-haskell
+           ^>= 2.12 || ^>= 2.13 || ^>= 2.14
+        || ^>= 2.15 || ^>= 2.16
 
   exposed-modules:
       Data.D10.Char
@@ -84,13 +89,110 @@
     , Data.D10.Predicate
     , Data.D10.Safe
 
-test-suite doctest
+  default-extensions:
+      DeriveDataTypeable
+      DeriveGeneric
+      DeriveLift
+      DerivingStrategies
+      GeneralizedNewtypeDeriving
+      InstanceSigs
+      QuasiQuotes
+      ScopedTypeVariables
+      TemplateHaskell
+      TypeApplications
+
+test-suite char
   default-language: Haskell2010
+  ghc-options: -Wall
   hs-source-dirs: test
   type: exitcode-stdio-1.0
-  main-is: doctest.hs
-  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+  main-is: char.hs
 
+  other-modules:
+      AssertQFails
+      Fallible
+
   build-depends:
-      base             >= 4.10 && < 4.15
-    , doctest          >= 0.13 && < 0.18
+      d10
+    , base ^>= 4.10 || ^>= 4.11 || ^>= 4.12
+        || ^>= 4.13 || ^>= 4.14
+    , hedgehog ^>= 1.0
+    , template-haskell
+           ^>= 2.12 || ^>= 2.13 || ^>= 2.14
+        || ^>= 2.15 || ^>= 2.16
+
+  default-extensions:
+      DeriveDataTypeable
+      DeriveGeneric
+      DeriveLift
+      DerivingStrategies
+      GeneralizedNewtypeDeriving
+      InstanceSigs
+      QuasiQuotes
+      ScopedTypeVariables
+      TemplateHaskell
+      TypeApplications
+
+test-suite num
+  default-language: Haskell2010
+  ghc-options: -Wall
+  hs-source-dirs: test
+  type: exitcode-stdio-1.0
+  main-is: num.hs
+
+  other-modules:
+      AssertQFails
+      Fallible
+
+  build-depends:
+      d10
+    , base ^>= 4.10 || ^>= 4.11 || ^>= 4.12
+        || ^>= 4.13 || ^>= 4.14
+    , hedgehog ^>= 1.0
+    , template-haskell
+           ^>= 2.12 || ^>= 2.13 || ^>= 2.14
+        || ^>= 2.15 || ^>= 2.16
+
+  default-extensions:
+      DeriveDataTypeable
+      DeriveGeneric
+      DeriveLift
+      DerivingStrategies
+      GeneralizedNewtypeDeriving
+      InstanceSigs
+      QuasiQuotes
+      ScopedTypeVariables
+      TemplateHaskell
+      TypeApplications
+
+test-suite safe
+  default-language: Haskell2010
+  ghc-options: -Wall
+  hs-source-dirs: test
+  type: exitcode-stdio-1.0
+  main-is: safe.hs
+
+  other-modules:
+      AssertQFails
+      Fallible
+
+  build-depends:
+      d10
+    , base ^>= 4.10 || ^>= 4.11 || ^>= 4.12
+        || ^>= 4.13 || ^>= 4.14
+    , hedgehog ^>= 1.0
+    , template-haskell
+           ^>= 2.12 || ^>= 2.13 || ^>= 2.14
+        || ^>= 2.15 || ^>= 2.16
+
+  default-extensions:
+      DeriveDataTypeable
+      DeriveGeneric
+      DeriveLift
+      DerivingStrategies
+      GeneralizedNewtypeDeriving
+      InstanceSigs
+      QuasiQuotes
+      ScopedTypeVariables
+      TemplateHaskell
+      TypeApplications
diff --git a/license.txt b/license.txt
--- a/license.txt
+++ b/license.txt
@@ -1,4 +1,4 @@
-Copyright 2018 Typeclass Consulting, LLC
+Copyright 2018-2021 Mission Valley Software 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:
 
diff --git a/src/Data/D10/Char.hs b/src/Data/D10/Char.hs
--- a/src/Data/D10/Char.hs
+++ b/src/Data/D10/Char.hs
@@ -1,8 +1,3 @@
-{-# LANGUAGE DeriveLift       #-}
-{-# LANGUAGE InstanceSigs     #-}
-{-# LANGUAGE TemplateHaskell  #-}
-{-# LANGUAGE TypeApplications #-}
-
 -- | Defines a 'D10' type as a newtype for 'Char', where the
 -- values are restricted to characters between @'0'@ and @'9'@.
 --
@@ -42,7 +37,6 @@
     , strD10Maybe
     , strD10Either
     , strD10Fail
-    , isD10Str
 
     -- * Converting between [D10] and String
     , strD10ListMaybe
@@ -89,20 +83,15 @@
 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, (+), (-), (*))
 
 import qualified Prelude as P
 
 -- template-haskell
+import Language.Haskell.TH.Lib    (litP, charL)
 import Language.Haskell.TH.Quote  (QuasiQuoter (..))
-import Language.Haskell.TH.Syntax (Exp (..), Lift (lift), Lit (..), Pat (..), Q)
-
--- $setup
--- >>> :set -XQuasiQuotes
--- >>> :set -XTemplateHaskell
--- >>> :set -fno-warn-overlapping-patterns
+import Language.Haskell.TH.Syntax (Exp (..), Lift, Pat (..), Q)
 
 ---------------------------------------------------
 
@@ -356,7 +345,7 @@
 -- in the string are within the range @'0'@ to @'9'@, or produce
 -- 'Nothing' otherwise.
 --
--- @'Data.D10.Predicate.isD10Str' x = 'Data.Maybe.isJust' ('strD10ListMaybe' x)@
+-- @'Data.D10.Predicate.isD10ListStr' x = 'Data.Maybe.isJust' ('strD10ListMaybe' x)@
 --
 -- 'strD10ListFail' is a more general version of this function.
 --
@@ -728,9 +717,12 @@
 -- You may also be interested in 'd10', a quasi-quoter which
 -- does something similar.
 
-d10Exp :: Integral a => a -> Q Exp
-d10Exp = integralD10Fail >=> (lift :: D10 -> Q Exp)
+d10Exp :: Integer -> Q Exp
+d10Exp = integerD10Fail >=> d10Exp'
 
+d10Exp' :: D10 -> Q Exp
+d10Exp' x = [| x |]
+
 -- | Produces an expression of type @['D10']@ that can be used
 -- in a Template Haskell splice.
 --
@@ -752,8 +744,11 @@
 -- does something similar.
 
 d10ListExp :: String -> Q Exp
-d10ListExp = strD10ListFail >=> (lift :: [D10] -> Q Exp)
+d10ListExp = strD10ListFail >=> d10ListExp'
 
+d10ListExp' :: [D10] -> Q Exp
+d10ListExp' xs = [| xs |]
+
 ---------------------------------------------------
 
 -- | Produces a pattern that can be used in a splice
@@ -761,36 +756,35 @@
 --
 -- >>> :{
 --       case (charD10Maybe '5') of
---         Just $(d10Pat [d10|4|]) -> "A"
---         Just $(d10Pat [d10|5|]) -> "B"
---         _                       -> "C"
+--         Just $(d10Pat 4) -> "A"
+--         Just $(d10Pat 5) -> "B"
+--         _                -> "C"
 -- >>> :}
 -- "B"
 --
 -- You may wish to use the 'd10' quasi-quoter instead.
 
-d10Pat :: D10 -> Q Pat
-d10Pat x =
-    return (ConP 'D10_Unsafe [LitP (CharL (d10Char x))])
+d10Pat :: Integer -> Q Pat
+d10Pat = integerD10Fail >=> d10Pat'
 
+d10Pat' :: D10 -> Q Pat
+d10Pat' (D10_Unsafe x) = [p| D10_Unsafe $(litP $ charL x) |]
+
 -- | Produces a pattern that can be used in a splice
 -- to match a particular list of 'D10' values.
 --
 -- >>> :{
 --       case (strD10ListMaybe "56") of
---         Just $(d10ListPat [d10list|42|]) -> "A"
---         Just $(d10ListPat [d10list|56|]) -> "B"
---         _                                -> "C"
+--         Just $(d10ListPat "42") -> "A"
+--         Just $(d10ListPat "56") -> "B"
+--         _                       -> "C"
 -- >>> :}
 -- "B"
 --
 -- You may wish to use the 'd10list' quasi-quoter instead.
 
-d10ListPat :: [D10] -> Q Pat
-d10ListPat xs =
-  do
-    pats <- traverse d10Pat xs
-    return (ListP pats)
+d10ListPat :: String -> Q Pat
+d10ListPat = strD10ListFail >=> foldr (\x p -> [p| $(d10Pat' x) : $(p) |]) [p| [] |]
 
 --------------------------------------------------
 
@@ -834,8 +828,8 @@
 
 d10 :: QuasiQuoter
 d10 = QuasiQuoter
-    { quoteExp  = strD10Fail >=> lift
-    , quotePat  = strD10Fail >=> d10Pat
+    { quoteExp  = strD10Fail >=> d10Exp'
+    , quotePat  = strD10Fail >=> d10Pat'
     , quoteType = \_ -> fail "d10 cannot be used in a type context"
     , quoteDec  = \_ -> fail "d10 cannot be used in a declaration context"
     }
@@ -881,8 +875,8 @@
 
 d10list :: QuasiQuoter
 d10list = QuasiQuoter
-    { quoteExp  = strD10ListFail >=> lift
-    , quotePat  = strD10ListFail >=> d10ListPat
+    { quoteExp  = strD10ListFail >=> d10ListExp'
+    , quotePat  = d10ListPat
     , quoteType = \_ -> fail "d10list cannot be used in a type context"
     , quoteDec  = \_ -> fail "d10list cannot be used in a declaration context"
     }
diff --git a/src/Data/D10/Num.hs b/src/Data/D10/Num.hs
--- a/src/Data/D10/Num.hs
+++ b/src/Data/D10/Num.hs
@@ -1,10 +1,3 @@
-{-# 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@.
@@ -45,7 +38,6 @@
     , strD10Maybe
     , strD10Either
     , strD10Fail
-    , isD10Str
 
     -- * Converting between [D10] and String
     , strD10ListMaybe
@@ -92,20 +84,15 @@
 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, (+), (-), (*))
 
 import qualified Prelude as P
 
 -- template-haskell
+import Language.Haskell.TH.Lib    (litP, integerL)
 import Language.Haskell.TH.Quote  (QuasiQuoter (..))
-import Language.Haskell.TH.Syntax (Exp (..), Lift (lift), Lit (..), Pat (..), Q)
-
--- $setup
--- >>> :set -XQuasiQuotes
--- >>> :set -XTemplateHaskell
--- >>> :set -fno-warn-overlapping-patterns
+import Language.Haskell.TH.Syntax (Exp (..), Lift, Pat (..), Q)
 
 ---------------------------------------------------
 
@@ -363,7 +350,7 @@
 -- in the string are within the range @'0'@ to @'9'@, or produce
 -- 'Nothing' otherwise.
 --
--- @'Data.D10.Predicate.isD10Str' x = 'Data.Maybe.isJust' ('strD10ListMaybe' x)@
+-- @'Data.D10.Predicate.isD10ListStr' x = 'Data.Maybe.isJust' ('strD10ListMaybe' x)@
 --
 -- 'strD10ListFail' is a more general version of this function.
 --
@@ -735,9 +722,12 @@
 -- You may also be interested in 'd10', a quasi-quoter which
 -- does something similar.
 
-d10Exp :: forall a b. (Integral b, Lift a, Num a) => b -> Q Exp
-d10Exp = integralD10Fail >=> (lift :: D10 a -> Q Exp)
+d10Exp :: Integer -> Q Exp
+d10Exp = integerD10Fail >=> d10Exp'
 
+d10Exp' :: D10 Integer -> Q Exp
+d10Exp' (D10_Unsafe x) = [| D10_Unsafe (fromInteger x) |]
+
 -- | Produces an expression of type @['D10' a]@ that can be used
 -- in a Template Haskell splice.
 --
@@ -758,9 +748,12 @@
 -- You may also be interested in 'd10list', a quasi-quoter which
 -- does something similar.
 
-d10ListExp :: forall a. (Lift a, Num a) => String -> Q Exp
-d10ListExp = strD10ListFail >=> (lift :: [D10 a] -> Q Exp)
+d10ListExp :: String -> Q Exp
+d10ListExp = strD10ListFail >=> d10ListExp'
 
+d10ListExp' :: [D10 Integer] -> Q Exp
+d10ListExp' xs = [| xs |]
+
 ---------------------------------------------------
 
 -- | Produces a pattern that can be used in a splice
@@ -768,37 +761,42 @@
 --
 -- >>> :{
 --       case (charD10Maybe '5') of
---         Just $(d10Pat [d10|4|]) -> "A"
---         Just $(d10Pat [d10|5|]) -> "B"
---         _                       -> "C"
+--         Just $(d10Pat 4) -> "A"
+--         Just $(d10Pat 5) -> "B"
+--         _                -> "C"
 -- >>> :}
 -- "B"
 --
 -- You may wish to use the 'd10' quasi-quoter instead.
 
-d10Pat :: Integral a => D10 a -> Q Pat
-d10Pat x =
-    return (ConP 'D10_Unsafe [LitP (IntegerL (d10Integer x))])
+d10Pat :: Integer -> Q Pat
+d10Pat = integerD10Fail >=> d10Pat'
 
+d10Pat' :: D10 Integer -> Q Pat
+d10Pat' (D10_Unsafe x) = [p| D10_Unsafe $(litP $ integerL x) |]
+
+d10Pat'' :: Integral a => D10 a -> Q Pat
+d10Pat'' (D10_Unsafe x) = [p| D10_Unsafe $(litP $ integerL $ toInteger x) |]
+
 -- | Produces a pattern that can be used in a splice
 -- to match a particular list of @'D10' a@ values.
 --
 -- >>> :{
 --       case (strD10ListMaybe "56") of
---         Just $(d10ListPat [d10list|42|]) -> "A"
---         Just $(d10ListPat [d10list|56|]) -> "B"
---         _                                -> "C"
+--         Just $(d10ListPat "42") -> "A"
+--         Just $(d10ListPat "56") -> "B"
+--         _                       -> "C"
 -- >>> :}
 -- "B"
 --
 -- You may wish to use the 'd10list' quasi-quoter instead.
 
-d10ListPat :: Integral a => [D10 a] -> Q Pat
-d10ListPat xs =
-  do
-    pats <- traverse d10Pat xs
-    return (ListP pats)
+d10ListPat :: String -> Q Pat
+d10ListPat = strD10ListFail >=> d10ListPat'
 
+d10ListPat' :: [D10 Integer] -> Q Pat
+d10ListPat' = foldr (\x p -> [p| $(d10Pat'' x) : $(p) |]) [p| [] |]
+
 ---------------------------------------------------
 
 -- | A single base-10 digit.
@@ -839,10 +837,10 @@
 -- ... d10 must be between 0 and 9
 -- ...
 
-d10 :: forall a. (Lift a, Integral a) => QuasiQuoter
+d10 :: QuasiQuoter
 d10 = QuasiQuoter
-    { quoteExp  = strD10Fail >=> (lift :: D10 a -> Q Exp)
-    , quotePat  = strD10Fail >=> d10Pat @a
+    { quoteExp  = strD10Fail >=> d10Exp'
+    , quotePat  = strD10Fail >=> d10Pat'
     , quoteType = \_ -> fail "d10 cannot be used in a type context"
     , quoteDec  = \_ -> fail "d10 cannot be used in a declaration context"
     }
@@ -886,10 +884,10 @@
 -- ... d10 must be between 0 and 9
 -- ...
 
-d10list :: forall a. (Lift a, Integral a) => QuasiQuoter
+d10list :: QuasiQuoter
 d10list = QuasiQuoter
-    { quoteExp  = strD10ListFail >=> (lift :: [D10 a] -> Q Exp)
-    , quotePat  = strD10ListFail >=> d10ListPat @a
+    { quoteExp  = strD10ListFail >=> d10ListExp'
+    , quotePat  = strD10ListFail >=> d10ListPat'
     , quoteType = \_ -> fail "d10list cannot be used in a type context"
     , quoteDec  = \_ -> fail "d10list cannot be used in a declaration context"
     }
diff --git a/src/Data/D10/Predicate.hs b/src/Data/D10/Predicate.hs
--- a/src/Data/D10/Predicate.hs
+++ b/src/Data/D10/Predicate.hs
@@ -1,45 +1,37 @@
 -- | Functions to test whether values of various
 -- types represent digits in the range /0/ to /9/.
-
 module Data.D10.Predicate where
 
-import Numeric.Natural
+import Numeric.Natural (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
--- a/src/Data/D10/Safe.hs
+++ b/src/Data/D10/Safe.hs
@@ -1,7 +1,3 @@
-{-# LANGUAGE DeriveLift       #-}
-{-# LANGUAGE TemplateHaskell  #-}
-{-# LANGUAGE TypeApplications #-}
-
 -- | Defines a 'D10' type as
 -- @'D0' | 'D1' | 'D2' | 'D3' | 'D4' | 'D5' | 'D6' | 'D7' | 'D8' | 'D9'@.
 --
@@ -24,15 +20,12 @@
     -- $enum
 
     -- * Quasi-quoters
-    , d10
     , d10list
 
     -- * Splice expressions
-    , d10Exp
     , d10ListExp
 
     -- * Splice patterns
-    , d10Pat
     , d10ListPat
 
     -- * Converting between D10 and Char
@@ -85,13 +78,10 @@
 
     ) 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 Data.Data          (Data)
 import GHC.Generics       (Generic)
 import Numeric.Natural    (Natural)
 import Prelude            hiding (fail, (+), (-), (*))
@@ -100,12 +90,7 @@
 
 -- template-haskell
 import Language.Haskell.TH.Quote  (QuasiQuoter (..))
-import Language.Haskell.TH.Syntax (Exp (..), Lift (lift), Lit (..), Pat (..), Q)
-
--- $setup
--- >>> :set -XQuasiQuotes
--- >>> :set -XTemplateHaskell
--- >>> :set -fno-warn-overlapping-patterns
+import Language.Haskell.TH.Syntax (Exp (..), Lift, Pat (..), Q, dataToPatQ)
 
 ---------------------------------------------------
 
@@ -122,7 +107,7 @@
     | D7  -- ^ Seven
     | D8  -- ^ Eight
     | D9  -- ^ Nine
-    deriving (Bounded, Enum, Eq, Lift, Ord, Show)
+    deriving (Bounded, Enum, Eq, Lift, Ord, Show, Data, Generic)
 
 -- $bounded
 -- ==== Bounded
@@ -354,7 +339,7 @@
 -- in the string are within the range @'0'@ to @'9'@, or produce
 -- 'Nothing' otherwise.
 --
--- @'Data.D10.Predicate.isD10Str' x = 'Data.Maybe.isJust' ('strD10ListMaybe' x)@
+-- @'Data.D10.Predicate.isD10ListStr' x = 'Data.Maybe.isJust' ('strD10ListMaybe' x)@
 --
 -- 'strD10ListFail' is a more general version of this function.
 --
@@ -750,23 +735,6 @@
 
 ---------------------------------------------------
 
--- | Produces an expression of type 'D10' that can be used
--- in a Template Haskell splice.
---
--- >>> $(d10Exp 5)
--- D5
---
--- >>> $(d10Exp 12)
--- ...
--- ... d10 must be between 0 and 9
--- ...
---
--- You may also be interested in 'd10', a quasi-quoter which
--- does something similar.
-
-d10Exp :: Integral a => a -> Q Exp
-d10Exp = integralD10Fail >=> (lift :: D10 -> Q Exp)
-
 -- | Produces an expression of type @['D10']@ that can be used
 -- in a Template Haskell splice.
 --
@@ -788,114 +756,43 @@
 -- does something similar.
 
 d10ListExp :: String -> Q Exp
-d10ListExp = strD10ListFail >=> (lift :: [D10] -> Q Exp)
+d10ListExp = strD10ListFail >=> d10ListExp'
 
----------------------------------------------------
+d10ListExp' :: [D10] -> Q Exp
+d10ListExp' x = [| x |]
 
--- | Produces a pattern that can be used in a splice
--- to match a particular 'D10' value.
---
--- >>> :{
---       case (charD10Maybe '5') of
---         Just $(d10Pat D4) -> "A"
---         Just $(d10Pat D5) -> "B"
---         _                 -> "C"
--- >>> :}
--- "B"
---
--- You are unlikely to write any code that resembles this example,
--- since you can just write @D4@ instead of @$(d10Pat D4)@.
--- Rather, this function exists to help implement other things
--- related to Template Haskell, such as 'd10ListPat' and 'd10'.
+---------------------------------------------------
 
 d10Pat :: D10 -> Q Pat
-d10Pat x =
-    case x of
-        D0 -> [p| D0 |]
-        D1 -> [p| D1 |]
-        D2 -> [p| D2 |]
-        D3 -> [p| D3 |]
-        D4 -> [p| D4 |]
-        D5 -> [p| D5 |]
-        D6 -> [p| D6 |]
-        D7 -> [p| D7 |]
-        D8 -> [p| D8 |]
-        D9 -> [p| D9 |]
+d10Pat = dataToPatQ (const Nothing)
 
 -- | Produces a pattern that can be used in a splice
 -- to match a particular list of 'D10' values.
 --
 -- >>> :{
---       case (strD10ListMaybe "56") of
---         Just $(d10ListPat [D4, D2]) -> "A"
---         Just $(d10ListPat [D5, D6]) -> "B"
---         _                                -> "C"
+--       case [D5, D6] of
+--         $(d10ListPat "42") -> "A"
+--         $(d10ListPat "56") -> "B"
+--         _                  -> "C"
 -- >>> :}
 -- "B"
 --
--- You are unlikely to write any code that resembles this example,
--- since you can just write @[D4, D2]@ instead of @$(d10Pat [D4, D2])@.
--- Rather, this function exists to help implement other things
--- related to Template Haskell, such as 'd10list'.
+-- You may also be interested in 'd10list', a quasi-quoter which
+-- does something similar.
 
-d10ListPat :: [D10] -> Q Pat
-d10ListPat xs =
+d10ListPat :: String -> Q Pat
+d10ListPat = strD10ListFail >=> \xs ->
   do
     pats <- traverse d10Pat xs
     return (ListP pats)
 
----------------------------------------------------
-
--- | A single base-10 digit.
---
--- This isn't very useful, since you can just write 'D5'
--- instead of '[d10|5|]'. It is included only for completeness,
--- because each of the modules "Data.D10.Char" and "Data.D10.Num"
--- defines a similar quasi-quoter.
---
--- 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
--- ...
---
--- This quasi-quoter can also be used as a pattern.
---
--- >>> :{
---       case (charD10Maybe '5') of
---         Just [d10|4|] -> "A"
---         Just [d10|5|] -> "B"
---         _             -> "C"
--- >>> :}
--- "B"
---
--- >>> :{
---       case (charD10Maybe '5') of
---         Just [d10|x|] -> "A"
---         Just [d10|5|] -> "B"
---         _             -> "C"
--- >>> :}
--- ...
--- ... d10 must be between 0 and 9
--- ...
+d10ListPat' :: [D10] -> Q Pat
+d10ListPat' xs =
+  do
+    pats <- traverse d10Pat xs
+    return (ListP pats)
 
-d10 :: QuasiQuoter
-d10 = QuasiQuoter
-    { quoteExp  = strD10Fail >=> lift
-    , quotePat  = strD10Fail >=> d10Pat
-    , quoteType = \_ -> fail "d10 cannot be used in a type context"
-    , quoteDec  = \_ -> fail "d10 cannot be used in a declaration context"
-    }
+---------------------------------------------------
 
 -- | A list of base-10 digits.
 --
@@ -919,7 +816,7 @@
 -- This quasi-quoter can also be used as a pattern.
 --
 -- >>> :{
---       case [d10list|56|] of
+--       case [D5, D6] of
 --         [d10list|41|] -> "A"
 --         [d10list|56|] -> "B"
 --         _             -> "C"
@@ -927,7 +824,7 @@
 -- "B"
 --
 -- >>> :{
---       case [d10list|56|] of
+--       case [D5, D6] of
 --         [d10list|4x|] -> "A"
 --         [d10list|56|] -> "B"
 --         _             -> "C"
@@ -938,8 +835,8 @@
 
 d10list :: QuasiQuoter
 d10list = QuasiQuoter
-    { quoteExp  = strD10ListFail >=> lift
-    , quotePat  = strD10ListFail >=> d10ListPat
+    { quoteExp  = strD10ListFail >=> d10ListExp'
+    , quotePat  = strD10ListFail >=> d10ListPat'
     , quoteType = \_ -> fail "d10list cannot be used in a type context"
     , quoteDec  = \_ -> fail "d10list cannot be used in a declaration context"
     }
diff --git a/test/AssertQFails.hs b/test/AssertQFails.hs
new file mode 100644
--- /dev/null
+++ b/test/AssertQFails.hs
@@ -0,0 +1,15 @@
+module AssertQFails where
+
+import Hedgehog
+
+import Control.Exception (try, SomeException)
+import Control.Monad.IO.Class (liftIO)
+import Language.Haskell.TH (runQ, Q)
+
+qFails :: Show a => Q a -> PropertyT IO ()
+qFails (q :: Q a) =
+  do
+    result <- liftIO (try (runQ q) :: IO (Either SomeException a))
+    case result of
+        Left _ -> success
+        Right _ -> failure
diff --git a/test/Fallible.hs b/test/Fallible.hs
new file mode 100644
--- /dev/null
+++ b/test/Fallible.hs
@@ -0,0 +1,10 @@
+module Fallible where
+
+import Control.Monad.Fail (MonadFail (fail))
+import Prelude hiding (fail)
+
+newtype Fallible a = Fallible (Either String a)
+    deriving newtype (Functor, Applicative, Monad, Eq, Show)
+
+instance MonadFail Fallible where
+    fail = Fallible . Left
diff --git a/test/char.hs b/test/char.hs
new file mode 100644
--- /dev/null
+++ b/test/char.hs
@@ -0,0 +1,247 @@
+import Prelude hiding (fail, (+), (-), (*))
+
+import Data.D10.Char
+import qualified Data.D10.Predicate as Predicate
+
+import Hedgehog
+import qualified Hedgehog.Gen as Gen
+import qualified Hedgehog.Range as Range
+
+import Control.Applicative ((<|>))
+import Control.Monad (when)
+import Control.Monad.Fail (MonadFail (fail))
+import Data.Maybe (isJust)
+import System.Exit (exitFailure)
+
+import AssertQFails (qFails)
+import Fallible (Fallible)
+
+main :: IO ()
+main =
+  do
+    ok <- checkParallel $$(discover)
+    when (not ok) exitFailure
+
+genD10 :: Gen D10
+genD10 = Gen.enumBounded
+
+prop_bounds_examples :: Property
+prop_bounds_examples = withTests 1 $ property $ do
+    minBound === [d10|0|]
+    maxBound === [d10|9|]
+
+prop_enumeration_examples :: Property
+prop_enumeration_examples = withTests 1 $ property $ do
+    [ [d10|5|] .. ]           === [d10list|56789|]
+    [ [d10|4|] .. [d10|7|] ]  === [d10list|4567|]
+    [ [d10|5|], [d10|4|] .. ] === [d10list|543210|]
+    [ [d10|1|], [d10|3|] .. ] === [d10list|13579|]
+    [ minBound .. maxBound ]  === [d10list|0123456789|]
+
+prop_lift_examples :: Property
+prop_lift_examples = withTests 1 $ property $ do
+    d10Char    [d10|7|] === '7'
+    d10Str     [d10|7|] === "7"
+    d10Nat     [d10|7|] ===  7
+    d10Integer [d10|7|] ===  7
+    d10Int     [d10|7|] ===  7
+    d10Num     [d10|7|] ===  (7 :: Integer)
+
+prop_d10Str :: Property
+prop_d10Str = property $ do
+    x <- forAll genD10
+    d10Str x === [d10Char x]
+
+prop_mod_examples :: Property
+prop_mod_examples = withTests 1 $ property $ do
+    natMod10       56  === [d10|6|]
+    integerMod10   56  === [d10|6|]
+    integerMod10 (-56) === [d10|4|]
+    intMod10       56  === [d10|6|]
+    intMod10     (-56) === [d10|4|]
+
+    integralMod10   (56  :: Integer) === [d10|6|]
+    integralMod10 ((-56) :: Integer) === [d10|4|]
+
+prop_d10Maybe_examples :: Property
+prop_d10Maybe_examples = withTests 1 $ property $ do
+    charD10Maybe '5' === Just [d10|5|]
+    charD10Maybe 'a' === Nothing
+    strD10Maybe "5"  === Just [d10|5|]
+    strD10Maybe "a"  === Nothing
+    strD10Maybe "58" === Nothing
+
+prop_isJust_string :: Property
+prop_isJust_string = property $ do
+    x <- forAll $ Gen.string (Range.linear 0 5) (Gen.digit <|> Gen.alpha)
+    Predicate.isD10Str     x === isJust (strD10Maybe x)
+    Predicate.isD10ListStr x === isJust (strD10ListMaybe x)
+
+prop_isJust_nat :: Property
+prop_isJust_nat = property $ do
+    x <- forAll $ Gen.integral (Range.linear 0 20)
+    Predicate.isD10Nat x === isJust (natD10Maybe x)
+
+prop_d10ListMaybe_examples :: Property
+prop_d10ListMaybe_examples = withTests 1 $ property $ do
+    strD10ListMaybe "5"  === Just [d10list|5|]
+    strD10ListMaybe "a"  === Nothing
+    strD10ListMaybe "58" === Just [d10list|58|]
+
+prop_natD10Maybe_examples :: Property
+prop_natD10Maybe_examples = withTests 1 $ property $ do
+    natD10Maybe  5 === Just [d10|5|]
+    natD10Maybe 12 === Nothing
+
+prop_isJust_integer :: Property
+prop_isJust_integer = property $ do
+    x <- forAll $ Gen.integral (Range.linearFrom 0 (-5) 15)
+    Predicate.isD10Integer x === isJust (integerD10Maybe x)
+
+prop_integerD10Maybe_examples :: Property
+prop_integerD10Maybe_examples = withTests 1 $ property $ do
+    integerD10Maybe   5  === Just [d10|5|]
+    integerD10Maybe  12  === Nothing
+    integerD10Maybe (-5) === Nothing
+
+prop_isJust_int :: Property
+prop_isJust_int = withTests 1 $ property $ do
+    x <- forAll $ Gen.integral (Range.linearFrom 0 (-5) 15)
+    Predicate.isD10Int x === isJust (intD10Maybe x)
+
+prop_intD10Maybe_examples :: Property
+prop_intD10Maybe_examples = withTests 1 $ property $ do
+    intD10Maybe   5  === Just [d10|5|]
+    intD10Maybe  12  === Nothing
+    intD10Maybe (-5) === Nothing
+
+prop_isJust_integral :: Property
+prop_isJust_integral = property $ do
+    x <- forAll $ Gen.integral (Range.linearFrom 0 (-5) 15)
+    Predicate.isD10Integral (x :: Integer) === isJust (integerD10Maybe x)
+
+prop_integralD10Maybe_examples :: Property
+prop_integralD10Maybe_examples = withTests 1 $ property $ do
+    integralD10Maybe (   5  :: Integer ) === Just [d10|5|]
+    integralD10Maybe (  12  :: Integer ) === Nothing
+    integralD10Maybe ( (-5) :: Integer ) === Nothing
+
+prop_charD10Either_examples :: Property
+prop_charD10Either_examples = withTests 1 $ property $ do
+    charD10Either '5' === Right [d10|5|]
+    charD10Either 'a' === Left "d10 must be between 0 and 9"
+
+prop_strD10Either_examples :: Property
+prop_strD10Either_examples = withTests 1 $ property $ do
+    strD10Either  "5" === Right [d10|5|]
+    strD10Either  "a" === Left "d10 must be between 0 and 9"
+    strD10Either "58" === Left "d10 must be a single character"
+
+prop_strD10ListEither_examples :: Property
+prop_strD10ListEither_examples = withTests 1 $ property $ do
+    strD10ListEither  "5" === Right [d10list|5|]
+    strD10ListEither  "a" === Left "d10 must be between 0 and 9"
+    strD10ListEither "58" === Right [d10list|58|]
+
+prop_natD10ListEither_examples :: Property
+prop_natD10ListEither_examples = withTests 1 $ property $ do
+    natD10Either  5 === Right [d10|5|]
+    natD10Either 12 === Left "d10 must be less than 10"
+
+prop_integerD10ListEither_examples :: Property
+prop_integerD10ListEither_examples = withTests 1 $ property $ do
+    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"
+
+prop_intD10ListEither_examples :: Property
+prop_intD10ListEither_examples = withTests 1 $ property $ do
+    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"
+
+prop_integralD10ListEither_examples :: Property
+prop_integralD10ListEither_examples = withTests 1 $ property $ do
+    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"
+
+prop_charD10Fail_examples :: Property
+prop_charD10Fail_examples = withTests 1 $ property $ do
+    (charD10Fail '5' :: Fallible D10) === return [d10|5|]
+    (charD10Fail 'a' :: Fallible D10) === fail "d10 must be between 0 and 9"
+
+prop_strD10Fail_examples :: Property
+prop_strD10Fail_examples = withTests 1 $ property $ do
+    (strD10Fail  "5" :: Fallible D10) === return [d10|5|]
+    (strD10Fail  "a" :: Fallible D10) === fail "d10 must be between 0 and 9"
+    (strD10Fail "58" :: Fallible D10) === fail "d10 must be a single character"
+
+prop_strD10ListFail_examples :: Property
+prop_strD10ListFail_examples = withTests 1 $ property $ do
+    (strD10ListFail  "5" :: Fallible [D10]) === return [d10list|5|]
+    (strD10ListFail  "a" :: Fallible [D10]) === fail "d10 must be between 0 and 9"
+    (strD10ListFail "58" :: Fallible [D10]) === return [d10list|58|]
+
+prop_natD10Fail_examples :: Property
+prop_natD10Fail_examples = withTests 1 $ property $ do
+    (natD10Fail  5 :: Fallible D10) === return [d10|5|]
+    (natD10Fail 12 :: Fallible D10) === fail "d10 must be less than 10"
+
+prop_integerD10Fail_examples :: Property
+prop_integerD10Fail_examples = withTests 1 $ property $ do
+    (integerD10Fail   5  :: Fallible D10) === return [d10|5|]
+    (integerD10Fail  12  :: Fallible D10) === fail "d10 must be between 0 and 9"
+    (integerD10Fail (-5) :: Fallible D10) === fail "d10 must be between 0 and 9"
+
+prop_intD10Fail_examples :: Property
+prop_intD10Fail_examples = withTests 1 $ property $ do
+    (intD10Fail   5  :: Fallible D10) === return [d10|5|]
+    (intD10Fail  12  :: Fallible D10) === fail "d10 must be between 0 and 9"
+    (intD10Fail (-5) :: Fallible D10) === fail "d10 must be between 0 and 9"
+
+prop_integralD10Fail_examples :: Property
+prop_integralD10Fail_examples = withTests 1 $ property $ do
+    (integralD10Fail   (5  :: Integer) :: Fallible D10) === return [d10|5|]
+    (integralD10Fail  (12  :: Integer) :: Fallible D10) === fail "d10 must be between 0 and 9"
+    (integralD10Fail ((-5) :: Integer) :: Fallible D10) === fail "d10 must be between 0 and 9"
+
+prop_spliceExp_examples :: Property
+prop_spliceExp_examples = withTests 1 $ property $ do
+    d10Nat $(d10Exp 5) === 5
+    qFails (d10Exp 12)
+    (d10Nat <$> $(d10ListExp "")) === []
+    (d10Nat <$> $(d10ListExp "5")) === [5]
+    (d10Nat <$> $(d10ListExp "58")) === [5,8]
+
+prop_splicePat_examples :: Property
+prop_splicePat_examples = withTests 1 $ property $ do
+    "B" === case (charD10Maybe '5') of
+        Just $(d10Pat 4) -> "A"
+        Just $(d10Pat 5) -> "B"
+        _                -> "C"
+    qFails (d10Pat 12)
+    "B" === case (strD10ListMaybe "56") of
+        Just $(d10ListPat "42") -> "A"
+        Just $(d10ListPat "56") -> "B"
+        _                       -> "C"
+
+prop_qqPat_examples :: Property
+prop_qqPat_examples = withTests 1 $ property $ do
+    "B" === case (charD10Maybe '5') of
+        Just [d10|4|] -> "A"
+        Just [d10|5|] -> "B"
+        _             -> "C"
+    "B" === case [d10list|56|] of
+        [d10list|41|] -> "A"
+        [d10list|56|] -> "B"
+        _             -> "C"
+
+prop_arithmetic_examples :: Property
+prop_arithmetic_examples = withTests 1 $ property $ do
+    [d10|2|] + [d10|3|] === [d10|5|]
+    [d10|6|] + [d10|7|] === [d10|3|]
+    [d10|7|] - [d10|5|] === [d10|2|]
+    [d10|3|] - [d10|7|] === [d10|6|]
+    [d10|2|] * [d10|4|] === [d10|8|]
+    [d10|7|] * [d10|8|] === [d10|6|]
diff --git a/test/doctest.hs b/test/doctest.hs
deleted file mode 100644
--- a/test/doctest.hs
+++ /dev/null
@@ -1,11 +0,0 @@
-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"
-    ]
diff --git a/test/num.hs b/test/num.hs
new file mode 100644
--- /dev/null
+++ b/test/num.hs
@@ -0,0 +1,250 @@
+import Prelude hiding (fail, (+), (-), (*))
+
+import Data.D10.Num
+import qualified Data.D10.Predicate as Predicate
+
+import Hedgehog
+import qualified Hedgehog.Gen as Gen
+import qualified Hedgehog.Range as Range
+
+import Control.Applicative ((<|>))
+import Control.Monad (when)
+import Control.Monad.Fail (MonadFail (fail))
+import Data.Int (Int32)
+import Data.Maybe (isJust)
+import Data.Word (Word64)
+import System.Exit (exitFailure)
+
+import AssertQFails (qFails)
+import Fallible (Fallible)
+
+main :: IO ()
+main =
+  do
+    ok <- checkParallel $$(discover)
+    when (not ok) exitFailure
+
+genD10 :: Integral a => Gen (D10 a)
+genD10 = Gen.enumBounded
+
+prop_bounds_examples :: Property
+prop_bounds_examples = withTests 1 $ property $ do
+    minBound === ([d10|0|] :: D10 Int)
+    maxBound === ([d10|9|] :: D10 Int)
+
+prop_enumeration_examples :: Property
+prop_enumeration_examples = withTests 1 $ property $ do
+    [ [d10|5|] .. ]           === ([d10list|56789|]      :: [D10 Integer])
+    [ [d10|4|] .. [d10|7|] ]  === ([d10list|4567|]       :: [D10 Integer])
+    [ [d10|5|], [d10|4|] .. ] === ([d10list|543210|]     :: [D10 Integer])
+    [ [d10|1|], [d10|3|] .. ] === ([d10list|13579|]      :: [D10 Integer])
+    [ minBound .. maxBound ]  === ([d10list|0123456789|] :: [D10 Integer])
+
+prop_lift_examples :: Property
+prop_lift_examples = withTests 1 $ property $ do
+    d10Char    ([d10|7|] :: D10 Integer) === '7'
+    d10Str     ([d10|7|] :: D10 Integer) === "7"
+    d10Nat     ([d10|7|] :: D10 Integer) ===  7
+    d10Integer ([d10|7|] :: D10 Integer) ===  7
+    d10Int     ([d10|7|] :: D10 Integer) ===  7
+    d10Num     ([d10|7|] :: D10 Integer) ===  (7 :: Integer)
+
+prop_d10Str :: Property
+prop_d10Str = property $ do
+    x :: D10 Integer <- forAll genD10
+    d10Str x === [d10Char x]
+
+prop_mod_examples :: Property
+prop_mod_examples = withTests 1 $ property $ do
+    natMod10       56  === ([d10|6|] :: D10 Integer)
+    integerMod10   56  === ([d10|6|] :: D10 Integer)
+    integerMod10 (-56) === ([d10|4|] :: D10 Integer)
+    intMod10       56  === ([d10|6|] :: D10 Integer)
+    intMod10     (-56) === ([d10|4|] :: D10 Integer)
+
+    integralMod10   (56  :: Integer) === ([d10|6|] :: D10 Integer)
+    integralMod10 ((-56) :: Integer) === ([d10|4|] :: D10 Integer)
+
+prop_d10Maybe_examples :: Property
+prop_d10Maybe_examples = withTests 1 $ property $ do
+    charD10Maybe '5' === (Just [d10|5|] :: Maybe (D10 Integer))
+    charD10Maybe 'a' === (Nothing       :: Maybe (D10 Integer))
+    strD10Maybe "5"  === (Just [d10|5|] :: Maybe (D10 Integer))
+    strD10Maybe "a"  === (Nothing       :: Maybe (D10 Integer))
+    strD10Maybe "58" === (Nothing       :: Maybe (D10 Integer))
+
+prop_isJust_string :: Property
+prop_isJust_string = property $ do
+    x <- forAll $ Gen.string (Range.linear 0 5) (Gen.digit <|> Gen.alpha)
+    Predicate.isD10Str     x === isJust (strD10Maybe x     :: Maybe (D10 Integer))
+    Predicate.isD10ListStr x === isJust (strD10ListMaybe x :: Maybe [D10 Integer])
+
+prop_isJust_nat :: Property
+prop_isJust_nat = property $ do
+    x <- forAll $ Gen.integral (Range.linear 0 20)
+    Predicate.isD10Nat x === isJust (natD10Maybe x :: Maybe (D10 Integer))
+
+prop_d10ListMaybe_examples :: Property
+prop_d10ListMaybe_examples = withTests 1 $ property $ do
+    strD10ListMaybe "5"  === (Just [d10list|5|]  :: Maybe [D10 Integer])
+    strD10ListMaybe "a"  === (Nothing            :: Maybe [D10 Integer])
+    strD10ListMaybe "58" === (Just [d10list|58|] :: Maybe [D10 Integer])
+
+prop_natD10Maybe_examples :: Property
+prop_natD10Maybe_examples = withTests 1 $ property $ do
+    natD10Maybe  5 === (Just [d10|5|] :: Maybe (D10 Integer))
+    natD10Maybe 12 === (Nothing       :: Maybe (D10 Integer))
+
+prop_isJust_integer :: Property
+prop_isJust_integer = property $ do
+    x <- forAll $ Gen.integral (Range.linearFrom 0 (-5) 15)
+    Predicate.isD10Integer x === isJust (integerD10Maybe x :: Maybe (D10 Integer))
+
+prop_integerD10Maybe_examples :: Property
+prop_integerD10Maybe_examples = withTests 1 $ property $ do
+    integerD10Maybe   5  === (Just [d10|5|] :: Maybe (D10 Integer))
+    integerD10Maybe  12  === (Nothing       :: Maybe (D10 Integer))
+    integerD10Maybe (-5) === (Nothing       :: Maybe (D10 Integer))
+
+prop_isJust_int :: Property
+prop_isJust_int = withTests 1 $ property $ do
+    x <- forAll $ Gen.integral (Range.linearFrom 0 (-5) 15)
+    Predicate.isD10Int x === isJust (intD10Maybe x :: Maybe (D10 Integer))
+
+prop_intD10Maybe_examples :: Property
+prop_intD10Maybe_examples = withTests 1 $ property $ do
+    intD10Maybe   5  === (Just [d10|5|] :: Maybe (D10 Integer))
+    intD10Maybe  12  === (Nothing       :: Maybe (D10 Integer))
+    intD10Maybe (-5) === (Nothing       :: Maybe (D10 Integer))
+
+prop_isJust_integral :: Property
+prop_isJust_integral = property $ do
+    x <- forAll $ Gen.integral (Range.linearFrom 0 (-5) 15)
+    Predicate.isD10Integral (x :: Integer) === isJust (integerD10Maybe x :: Maybe (D10 Integer))
+
+prop_integralD10Maybe_examples :: Property
+prop_integralD10Maybe_examples = withTests 1 $ property $ do
+    integralD10Maybe (   5  :: Integer ) === (Just [d10|5|] :: Maybe (D10 Integer))
+    integralD10Maybe (  12  :: Integer ) === (Nothing       :: Maybe (D10 Integer))
+    integralD10Maybe ( (-5) :: Integer ) === (Nothing       :: Maybe (D10 Integer))
+
+prop_charD10Either_examples :: Property
+prop_charD10Either_examples = withTests 1 $ property $ do
+    charD10Either '5' === (Right [d10|5|]                     :: Either String (D10 Integer) )
+    charD10Either 'a' === (Left "d10 must be between 0 and 9" :: Either String (D10 Integer) )
+
+prop_strD10Either_examples :: Property
+prop_strD10Either_examples = withTests 1 $ property $ do
+    strD10Either  "5" === (Right [d10|5|]                        :: Either String (D10 Integer))
+    strD10Either  "a" === (Left "d10 must be between 0 and 9"    :: Either String (D10 Integer))
+    strD10Either "58" === (Left "d10 must be a single character" :: Either String (D10 Integer))
+
+prop_strD10ListEither_examples :: Property
+prop_strD10ListEither_examples = withTests 1 $ property $ do
+    strD10ListEither  "5" === (Right [d10list|5|]                 :: Either String [D10 Integer])
+    strD10ListEither  "a" === (Left "d10 must be between 0 and 9" :: Either String [D10 Integer])
+    strD10ListEither "58" === (Right [d10list|58|]                :: Either String [D10 Integer])
+
+prop_natD10ListEither_examples :: Property
+prop_natD10ListEither_examples = withTests 1 $ property $ do
+    natD10Either  5 === (Right [d10|5|]                  :: Either String (D10 Integer))
+    natD10Either 12 === (Left "d10 must be less than 10" :: Either String (D10 Integer))
+
+prop_integerD10ListEither_examples :: Property
+prop_integerD10ListEither_examples = withTests 1 $ property $ do
+    integerD10Either   5  === (Right [d10|5|]                     :: Either String (D10 Integer))
+    integerD10Either  12  === (Left "d10 must be between 0 and 9" :: Either String (D10 Integer))
+    integerD10Either (-5) === (Left "d10 must be between 0 and 9" :: Either String (D10 Integer))
+
+prop_intD10ListEither_examples :: Property
+prop_intD10ListEither_examples = withTests 1 $ property $ do
+    intD10Either   5  === (Right [d10|5|]                     :: Either String (D10 Integer))
+    intD10Either  12  === (Left "d10 must be between 0 and 9" :: Either String (D10 Integer))
+    intD10Either (-5) === (Left "d10 must be between 0 and 9" :: Either String (D10 Integer))
+
+prop_integralD10ListEither_examples :: Property
+prop_integralD10ListEither_examples = withTests 1 $ property $ do
+    integralD10Either   (5  :: Integer) === (Right [d10|5|]                     :: Either String (D10 Integer))
+    integralD10Either  (12  :: Integer) === (Left "d10 must be between 0 and 9" :: Either String (D10 Integer))
+    integralD10Either ((-5) :: Integer) === (Left "d10 must be between 0 and 9" :: Either String (D10 Integer))
+
+prop_charD10Fail_examples :: Property
+prop_charD10Fail_examples = withTests 1 $ property $ do
+    (charD10Fail '5' :: Fallible (D10 Int)) === return [d10|5|]
+    (charD10Fail 'a' :: Fallible (D10 Int)) === fail "d10 must be between 0 and 9"
+
+prop_strD10Fail_examples :: Property
+prop_strD10Fail_examples = withTests 1 $ property $ do
+    (strD10Fail  "5" :: Fallible (D10 Int)) === return [d10|5|]
+    (strD10Fail  "a" :: Fallible (D10 Int)) === fail "d10 must be between 0 and 9"
+    (strD10Fail "58" :: Fallible (D10 Int)) === fail "d10 must be a single character"
+
+prop_strD10ListFail_examples :: Property
+prop_strD10ListFail_examples = withTests 1 $ property $ do
+    (strD10ListFail  "5" :: Fallible [D10 Int]) === return [d10list|5|]
+    (strD10ListFail  "a" :: Fallible [D10 Int]) === fail "d10 must be between 0 and 9"
+    (strD10ListFail "58" :: Fallible [D10 Int]) === return [d10list|58|]
+
+prop_natD10Fail_examples :: Property
+prop_natD10Fail_examples = withTests 1 $ property $ do
+    (natD10Fail  5 :: Fallible (D10 Int)) === return [d10|5|]
+    (natD10Fail 12 :: Fallible (D10 Int)) === fail "d10 must be less than 10"
+
+prop_integerD10Fail_examples :: Property
+prop_integerD10Fail_examples = withTests 1 $ property $ do
+    (integerD10Fail   5  :: Fallible (D10 Int)) === return [d10|5|]
+    (integerD10Fail  12  :: Fallible (D10 Int)) === fail "d10 must be between 0 and 9"
+    (integerD10Fail (-5) :: Fallible (D10 Int)) === fail "d10 must be between 0 and 9"
+
+prop_intD10Fail_examples :: Property
+prop_intD10Fail_examples = withTests 1 $ property $ do
+    (intD10Fail   5  :: Fallible (D10 Int)) === return [d10|5|]
+    (intD10Fail  12  :: Fallible (D10 Int)) === fail "d10 must be between 0 and 9"
+    (intD10Fail (-5) :: Fallible (D10 Int)) === fail "d10 must be between 0 and 9"
+
+prop_integralD10Fail_examples :: Property
+prop_integralD10Fail_examples = withTests 1 $ property $ do
+    (integralD10Fail   (5  :: Integer) :: Fallible (D10 Int)) === return [d10|5|]
+    (integralD10Fail  (12  :: Integer) :: Fallible (D10 Int)) === fail "d10 must be between 0 and 9"
+    (integralD10Fail ((-5) :: Integer) :: Fallible (D10 Int)) === fail "d10 must be between 0 and 9"
+
+prop_spliceExp_examples :: Property
+prop_spliceExp_examples = withTests 1 $ property $ do
+    d10Nat ($(d10Exp 5) :: D10 Word64) === 5
+    qFails (d10Exp 12)
+    map d10Nat ($(d10ListExp "")   :: [D10 Word]) === []
+    map d10Nat ($(d10ListExp "5")  :: [D10 Word]) === [5]
+    map d10Nat ($(d10ListExp "58") :: [D10 Word]) === [5,8]
+
+prop_splicePat_examples :: Property
+prop_splicePat_examples = withTests 1 $ property $ do
+    "B" === case (charD10Maybe '5') :: Maybe (D10 Int) of
+        Just $(d10Pat 4) -> "A"
+        Just $(d10Pat 5) -> "B"
+        _                -> "C"
+    qFails (d10Pat 12)
+    qFails (d10ListPat "ab")
+
+prop_qqPat_examples :: Property
+prop_qqPat_examples = withTests 1 $ property $ do
+    "B" === case charD10Maybe '5' :: Maybe (D10 Int) of
+        Just [d10|4|] -> "A"
+        Just [d10|5|] -> "B"
+        _             -> "C"
+    "B" === case charD10Maybe '5' :: Maybe (D10 Integer) of
+        Just [d10|4|] -> "A"
+        Just [d10|5|] -> "B"
+        _             -> "C"
+    "B" === case [d10list|56|] :: [D10 Int32] of
+        [d10list|41|] -> "A"
+        [d10list|56|] -> "B"
+        _             -> "C"
+
+prop_arithmetic_examples :: Property
+prop_arithmetic_examples = withTests 1 $ property $ do
+    [d10|2|] + [d10|3|] === ([d10|5|] :: D10 Int32)
+    [d10|6|] + [d10|7|] === ([d10|3|] :: D10 Int32)
+    [d10|7|] - [d10|5|] === ([d10|2|] :: D10 Int32)
+    [d10|3|] - [d10|7|] === ([d10|6|] :: D10 Int32)
+    [d10|2|] * [d10|4|] === ([d10|8|] :: D10 Int32)
+    [d10|7|] * [d10|8|] === ([d10|6|] :: D10 Int32)
diff --git a/test/safe.hs b/test/safe.hs
new file mode 100644
--- /dev/null
+++ b/test/safe.hs
@@ -0,0 +1,279 @@
+import Prelude hiding (fail, (+), (-), (*))
+
+import Data.D10.Safe
+import qualified Data.D10.Predicate as Predicate
+
+import Hedgehog
+import qualified Hedgehog.Gen as Gen
+import qualified Hedgehog.Range as Range
+
+import Control.Applicative ((<|>))
+import Control.Monad (when)
+import Control.Monad.Fail (MonadFail (fail))
+import Data.Maybe (isJust)
+import System.Exit (exitFailure)
+
+import AssertQFails (qFails)
+import Fallible (Fallible)
+
+main :: IO ()
+main =
+  do
+    ok <- checkParallel $$(discover)
+    when (not ok) exitFailure
+
+genD10 :: Gen D10
+genD10 = Gen.enumBounded
+
+prop_bounds_examples :: Property
+prop_bounds_examples = withTests 1 $ property $ do
+    minBound === D0
+    maxBound === D9
+
+prop_enumeration_examples :: Property
+prop_enumeration_examples = withTests 1 $ property $ do
+    [ 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 ] === [D0,D1,D2,D3,D4,D5,D6,D7,D8,D9]
+
+prop_d10Char_examples :: Property
+prop_d10Char_examples = withTests 1 $ property $ do
+    d10Char D7 === '7'
+
+prop_d10Str :: Property
+prop_d10Str = property $ do
+    x <- forAll genD10
+    d10Str x === [d10Char x]
+
+prop_d10Str_examples :: Property
+prop_d10Str_examples = withTests 1 $ property $ do
+    d10Str D7 === "7"
+
+prop_d10Nat_examples :: Property
+prop_d10Nat_examples = withTests 1 $ property $ do
+    d10Nat D7 === 7
+
+prop_d10Integer_examples :: Property
+prop_d10Integer_examples = withTests 1 $ property $ do
+    d10Integer D7 === 7
+
+prop_d10Int_examples :: Property
+prop_d10Int_examples = withTests 1 $ property $ do
+    d10Int D7 === 7
+
+prop_d10Num_examples :: Property
+prop_d10Num_examples = withTests 1 $ property $ do
+    d10Num D7 === (7 :: Integer)
+
+prop_natMod10_examples :: Property
+prop_natMod10_examples = withTests 1 $ property $ do
+    natMod10 56 === D6
+
+prop_integerMod10_examples :: Property
+prop_integerMod10_examples = withTests 1 $ property $ do
+    integerMod10 56 === D6
+    integerMod10 (-56) === D4
+
+prop_intMod10_examples :: Property
+prop_intMod10_examples = withTests 1 $ property $ do
+    intMod10 56 === D6
+    intMod10 (-56) === D4
+
+prop_integralMod10_examples :: Property
+prop_integralMod10_examples = withTests 1 $ property $ do
+    integralMod10 (56 :: Integer) === D6
+    integralMod10 ((-56) :: Integer) === D4
+
+prop_charD10Maybe :: Property
+prop_charD10Maybe = property $ do
+    x <- forAll (Gen.digit <|> Gen.alpha)
+    Predicate.isD10Char x === isJust (charD10Maybe x)
+
+prop_charD10Maybe_examples :: Property
+prop_charD10Maybe_examples = withTests 1 $ property $ do
+    charD10Maybe '5' === Just D5
+    charD10Maybe 'a' === Nothing
+
+prop_strD10Maybe :: Property
+prop_strD10Maybe = property $ do
+    x <- forAll $ Gen.string (Range.linear 0 5) (Gen.digit <|> Gen.alpha)
+    Predicate.isD10Str x === isJust (strD10Maybe x)
+
+prop_strD10Maybe_examples :: Property
+prop_strD10Maybe_examples = withTests 1 $ property $ do
+    strD10Maybe "5" === Just D5
+    strD10Maybe "a" === Nothing
+    strD10Maybe "58" === Nothing
+
+prop_strD10ListMaybe :: Property
+prop_strD10ListMaybe = property $ do
+    x <- forAll $ Gen.string (Range.linear 0 5) (Gen.digit <|> Gen.alpha)
+    Predicate.isD10ListStr x === isJust (strD10ListMaybe x)
+
+prop_strD10ListMaybe_examples :: Property
+prop_strD10ListMaybe_examples = withTests 1 $ property $ do
+    strD10ListMaybe "5" === Just [D5]
+    strD10ListMaybe "a" === Nothing
+    strD10ListMaybe "58" === Just [D5,D8]
+
+prop_natD10Maybe :: Property
+prop_natD10Maybe = property $ do
+    x <- forAll $ Gen.integral (Range.linear 0 20)
+    Predicate.isD10Nat x === isJust (natD10Maybe x)
+
+prop_natD10Maybe_examples :: Property
+prop_natD10Maybe_examples = withTests 1 $ property $ do
+    natD10Maybe 5 === Just D5
+    natD10Maybe 12 === Nothing
+
+prop_integerD10Maybe :: Property
+prop_integerD10Maybe = property $ do
+    x <- forAll $ Gen.integral (Range.linearFrom 0 (-5) 15)
+    Predicate.isD10Integer x === isJust (integerD10Maybe x)
+
+prop_integerD10Maybe_examples :: Property
+prop_integerD10Maybe_examples = withTests 1 $ property $ do
+    integerD10Maybe 5 === Just D5
+    integerD10Maybe 12 === Nothing
+    integerD10Maybe (-5) === Nothing
+
+prop_intD10Maybe :: Property
+prop_intD10Maybe = property $ do
+    x :: Int <- forAll $ Gen.integral (Range.linearFrom 0 (-5) 15)
+    Predicate.isD10Int x === isJust (intD10Maybe x)
+
+prop_intD10Maybe_examples :: Property
+prop_intD10Maybe_examples = withTests 1 $ property $ do
+    intD10Maybe 5 === Just D5
+    intD10Maybe 12 === Nothing
+    intD10Maybe (-5) === Nothing
+
+prop_integralD10Maybe :: Property
+prop_integralD10Maybe = property $ do
+    x :: Integer <- forAll $ Gen.integral (Range.linearFrom 0 (-5) 15)
+    Predicate.isD10Integer x === isJust (integralD10Maybe x)
+
+prop_integralD10Maybe_examples :: Property
+prop_integralD10Maybe_examples = withTests 1 $ property $ do
+    integralD10Maybe (5 :: Integer) === Just D5
+    integralD10Maybe (12 :: Integer) === Nothing
+    integralD10Maybe ((-5) :: Integer) === Nothing
+
+prop_charD10Either_examples :: Property
+prop_charD10Either_examples = withTests 1 $ property $ do
+    charD10Either '5' === Right D5
+    charD10Either 'a' === Left "d10 must be between 0 and 9"
+
+prop_strD10Either_examples :: Property
+prop_strD10Either_examples = withTests 1 $ property $ do
+    strD10Either "5" === Right D5
+    strD10Either "a" === Left "d10 must be between 0 and 9"
+    strD10Either "58" === Left "d10 must be a single character"
+
+prop_strD10ListEither_examples :: Property
+prop_strD10ListEither_examples = withTests 1 $ property $ do
+    strD10ListEither "5" === Right [D5]
+    strD10ListEither "a" === Left "d10 must be between 0 and 9"
+    strD10ListEither "58" === Right [D5,D8]
+
+prop_natD10Either_examples :: Property
+prop_natD10Either_examples = withTests 1 $ property $ do
+    natD10Either 5 === Right D5
+    natD10Either 12 === Left "d10 must be less than 10"
+
+prop_integerD10Either_examples :: Property
+prop_integerD10Either_examples = withTests 1 $ property $ do
+    integerD10Either 5 === Right D5
+    integerD10Either 12 === Left "d10 must be between 0 and 9"
+    integerD10Either (-5) === Left "d10 must be between 0 and 9"
+
+prop_intD10Either_examples :: Property
+prop_intD10Either_examples = withTests 1 $ property $ do
+    intD10Either 5 === Right D5
+    intD10Either 12 === Left "d10 must be between 0 and 9"
+    intD10Either (-5) === Left "d10 must be between 0 and 9"
+
+prop_integralD10Either_examples :: Property
+prop_integralD10Either_examples = withTests 1 $ property $ do
+    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"
+
+prop_charD10Fail_examples :: Property
+prop_charD10Fail_examples = withTests 1 $ property $ do
+    (charD10Fail '5' :: Fallible D10) === return D5
+    (charD10Fail 'a' :: Fallible D10) === fail "d10 must be between 0 and 9"
+
+prop_strD10Fail_examples :: Property
+prop_strD10Fail_examples = withTests 1 $ property $ do
+    (strD10Fail "5" :: Fallible D10) === return D5
+    (strD10Fail "a" :: Fallible D10) === fail "d10 must be between 0 and 9"
+    (strD10Fail "58" :: Fallible D10) === fail "d10 must be a single character"
+
+prop_strD10ListFail_examples :: Property
+prop_strD10ListFail_examples = withTests 1 $ property $ do
+    (strD10ListFail "5" :: Fallible [D10]) === return [D5]
+    (strD10ListFail "a" :: Fallible [D10]) === fail "d10 must be between 0 and 9"
+    (strD10ListFail "58" :: Fallible [D10]) === return [D5,D8]
+
+prop_natD10Fail_examples :: Property
+prop_natD10Fail_examples = withTests 1 $ property $ do
+    (natD10Fail 5 :: Fallible D10) === return D5
+    (natD10Fail 12 :: Fallible D10) === fail "d10 must be less than 10"
+
+prop_integerD10Fail_examples :: Property
+prop_integerD10Fail_examples = withTests 1 $ property $ do
+    (integerD10Fail 5 :: Fallible D10) === return D5
+    (integerD10Fail 12 :: Fallible D10) === fail "d10 must be between 0 and 9"
+    (integerD10Fail (-5) :: Fallible D10) === fail "d10 must be between 0 and 9"
+
+prop_intD10Fail_examples :: Property
+prop_intD10Fail_examples = withTests 1 $ property $ do
+    (intD10Fail 5 :: Fallible D10) === return D5
+    (intD10Fail 12 :: Fallible D10) === fail "d10 must be between 0 and 9"
+    (intD10Fail (-5) :: Fallible D10) === fail "d10 must be between 0 and 9"
+
+prop_integralD10Fail_examples :: Property
+prop_integralD10Fail_examples = withTests 1 $ property $ do
+    (integralD10Fail (5 :: Integer) :: Fallible D10) === return D5
+    (integralD10Fail (12 :: Integer) :: Fallible D10) === fail "d10 must be between 0 and 9"
+    (integralD10Fail (-5 :: Integer) :: Fallible D10) === fail "d10 must be between 0 and 9"
+
+prop_d10ListExp_examples :: Property
+prop_d10ListExp_examples = withTests 1 $ property $ do
+    $(d10ListExp "") === ([] :: [D10])
+    $(d10ListExp "5") === [D5]
+    $(d10ListExp "58") === [D5,D8]
+    qFails (d10ListExp "a")
+
+prop_d10ListPat_examples :: Property
+prop_d10ListPat_examples = withTests 1 $ property $ do
+    "B" === case id [D5, D6] of
+        $(d10ListPat "42") -> "A"
+        $(d10ListPat "56") -> "B"
+        _                  -> "C"
+    qFails (d10ListPat "a")
+
+prop_qqExp_examples :: Property
+prop_qqExp_examples = withTests 1 $ property $ do
+    [d10list||] === ([] :: [D10])
+    [d10list|5|] === [D5]
+    [d10list|58|] === [D5,D8]
+
+prop_qqPat_examples :: Property
+prop_qqPat_examples = withTests 1 $ property $ do
+    "B" === case id [D5, D6] of
+        [d10list|41|] -> "A"
+        [d10list|56|] -> "B"
+        _             -> "C"
+
+prop_arithmetic_examples :: Property
+prop_arithmetic_examples = withTests 1 $ property $ do
+    D2 + D3 === D5
+    D6 + D7 === D3
+    D7 - D5 === D2
+    D3 - D7 === D6
+    D2 * D4 === D8
+    D7 * D8 === D6
