diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
 # Changelog
 
+## [0.4] - 2018-02-20
+
+### Added
+
+- Doctests
+
+### Changed
+
+- `TVec3` is now just a type synonym for `(Double, Double, Double)`
+
 ## [0.3.1] - 2018-02-18
 
 ### Added
@@ -48,6 +58,7 @@
 
 ## [0.1.0.0] - 2012-12-05
 
+[0.4]:   https://github.com/dzhus/simple-vec3/compare/0.3.1...0.4
 [0.3.1]:   https://github.com/dzhus/simple-vec3/compare/0.3...0.3.1
 [0.3]:     https://github.com/dzhus/simple-vec3/compare/0.2...0.3
 [0.2]:     https://github.com/dzhus/simple-vec3/compare/0.1.0.1...0.2
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,47 @@
+# simple-vec3
+
+[![Travis CI build status](https://travis-ci.org/dzhus/simple-vec3.svg)](https://travis-ci.org/dzhus/simple-vec3)
+[![Hackage](https://img.shields.io/hackage/v/simple-vec3.svg?colorB=5e5184&style=flat)](https://hackage.haskell.org/package/simple-vec3)
+[![Hackage deps](https://img.shields.io/hackage-deps/v/simple-vec3.svg)](http://packdeps.haskellers.com/feed?needle=simple-vec3)
+
+*Simple* three-dimensional vectors of doubles with basic vector and
+matrix operations, supporting `Data.Vector.Unboxed` and
+`Data.Vector.Storable`.
+
+```haskell
+>>> let v1 = (-1, 0.0,  0.2) :: TVec3
+>>> let v2 = ( 1, 2.3,  5.0) :: TVec3
+>>> let v3 = ( 1,   1, -0.2) :: TVec3
+
+-- Add two vectors:
+>>> v1 <+> v2
+(0.0, 2.3, 5.2)
+
+-- Dot product:
+>>> v1 .* v2
+0.0
+
+-- Multiply by a scalar:
+>>> v1 .^ 5
+(-5.0, 0.0, 1.0)
+
+-- Cross product:
+>>> v1 >< v3
+(-0.2, 0.0, -1.0)
+
+-- Matrix-vector product:
+>>> diag 2 `mxv` v2
+(2.0, 4.6, 10.0)
+```
+
+Please consult the [Hackage page for simple-vec3][hackage-doc] for full
+documentation.
+
+The package provides two different implementations for `Vec3` type
+class, which differ in storage scheme. Benchmarks are included for
+both. You most likely want to use `CVec3` which is based on contiguous
+storage scheme and offers the best performance.
+
+![simple-vec3 benchmarks](benchmark.png)
+
+[hackage-doc]: https://hackage.haskell.org/package/simple-vec3/docs/Data-Vec3.html
diff --git a/simple-vec3.cabal b/simple-vec3.cabal
--- a/simple-vec3.cabal
+++ b/simple-vec3.cabal
@@ -1,5 +1,5 @@
 name: simple-vec3
-version: 0.3.1
+version: 0.4
 cabal-version: >=1.10
 build-type: Simple
 license: BSD3
@@ -14,6 +14,7 @@
 author: Dmitry Dzhus
 extra-source-files:
     CHANGELOG.md
+    README.md
 
 source-repository head
     type: git
@@ -26,26 +27,34 @@
     build-depends:
         QuickCheck <2.11,
         base <5,
-        vector <0.13,
-        vector-th-unbox <0.3
+        vector <0.13
     default-language: Haskell2010
     hs-source-dirs: src
     other-modules:
-        Data.Vec3.Tupled
+        Paths_simple_vec3
     ghc-options: -Wall -Wcompat -O2
 
+test-suite  simple-vec3-doctests
+    type: exitcode-stdio-1.0
+    main-is: doctest-driver.hs
+    build-depends:
+        base <5,
+        doctest <0.14,
+        doctest-discover >=0.1.0.8 && <0.2
+    default-language: Haskell2010
+    hs-source-dirs: tests
+    other-modules:
+        Main
+        Paths_simple_vec3
+    ghc-options: -Wall -Wcompat -O2 -threaded
 test-suite  simple-vec3-test
     type: exitcode-stdio-1.0
-    main-is: Tests.hs
+    main-is: Main.hs
     build-depends:
-        QuickCheck <2.11,
         base <5,
         simple-vec3 -any,
         tasty <0.13,
-        tasty-quickcheck <0.10,
-        tasty-th <0.2,
-        vector <0.13,
-        vector-th-unbox <0.3
+        tasty-quickcheck <0.10
     default-language: Haskell2010
     hs-source-dirs: tests
     other-modules:
@@ -56,12 +65,10 @@
     type: exitcode-stdio-1.0
     main-is: Benchmark.hs
     build-depends:
-        QuickCheck <2.11,
         base <5,
         criterion <1.3,
         simple-vec3 -any,
-        vector <0.13,
-        vector-th-unbox <0.3
+        vector <0.13
     default-language: Haskell2010
     hs-source-dirs: benchmark
     other-modules:
diff --git a/src/Data/Vec3.hs b/src/Data/Vec3.hs
--- a/src/Data/Vec3.hs
+++ b/src/Data/Vec3.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE FlexibleInstances #-}
 {-# LANGUAGE MultiParamTypeClasses #-}
 {-# LANGUAGE TypeFamilies #-}
 
@@ -13,9 +14,13 @@
 -}
 
 module Data.Vec3
-    ( Vec3(..)
+    ( -- * Examples
+      -- $examples
+      -- * Type class
+      Vec3(..)
+      -- * Implementations
     , CVec3(..)
-    , TVec3(..)
+    , TVec3
     )
 
 where
@@ -28,13 +33,47 @@
 import Data.Vector.Unboxed as VU
 import Data.Vector.Generic as VG
 import Data.Vector.Generic.Mutable as VGM
-import Test.QuickCheck
+import Test.QuickCheck (Arbitrary(..))
 
 import Data.Vec3.Class
 
-import Data.Vec3.Tupled
 
+-- $setup
+-- >>> :set -XFlexibleContexts
 
+
+-- $examples
+--
+-- >>> let v1 = (-1, 0.0,  0.2) :: TVec3
+-- >>> let v2 = ( 1, 2.3,  5.0) :: TVec3
+-- >>> let v3 = ( 1,   1, -0.2) :: TVec3
+--
+-- Add two vectors:
+--
+-- >>> v1 <+> v2
+-- (0.0,2.3,5.2)
+--
+-- Dot product:
+--
+-- >>> v1 .* v2
+-- 0.0
+--
+-- Multiply by a scalar:
+--
+-- >>> v1 .^ 5
+-- (-5.0,0.0,1.0)
+--
+-- Cross product:
+--
+-- >>> v1 >< v3
+-- (-0.2,0.0,-1.0)
+--
+-- Matrix-vector product:
+--
+-- >>> diag 2 `mxv` v2
+-- (2.0,4.6,10.0)
+
+
 -- | 'Vec3' implementation with 'Data.Vector.Unboxed.Unbox' and
 -- 'Data.Vector.Unboxed.Storable' instances based on a single
 -- contiguous array storage scheme, suitable for use with
@@ -52,7 +91,7 @@
 
 
 instance Vec3 CVec3 where
-    newtype Matrix CVec3 = TMatrix (CVec3, CVec3, CVec3)
+    newtype Matrix CVec3 = CMatrix (CVec3, CVec3, CVec3)
                            deriving (Eq, Show)
 
     fromXYZ (x, y, z) = CVec3 x y z
@@ -61,10 +100,10 @@
     toXYZ (CVec3 x y z) = (x, y, z)
     {-# INLINE toXYZ #-}
 
-    fromRows (r1, r2, r3) = TMatrix (r1, r2, r3)
+    fromRows (r1, r2, r3) = CMatrix (r1, r2, r3)
     {-# INLINE fromRows #-}
 
-    toRows (TMatrix (r1, r2, r3)) = (r1, r2, r3)
+    toRows (CMatrix (r1, r2, r3)) = (r1, r2, r3)
     {-# INLINE toRows #-}
 
 
@@ -174,3 +213,13 @@
 
   shrink (CVec3 x y z) =
     Prelude.map fromXYZ $ shrink (x, y, z)
+
+instance Arbitrary (Matrix CVec3) where
+  arbitrary = do
+    r1 <- arbitrary
+    r2 <- arbitrary
+    r3 <- arbitrary
+    return $ fromRows (r1, r2, r3)
+
+  shrink (CMatrix (r1, r2, r3)) =
+    Prelude.map fromRows $ shrink (r1, r2, r3)
diff --git a/src/Data/Vec3/Class.hs b/src/Data/Vec3/Class.hs
--- a/src/Data/Vec3/Class.hs
+++ b/src/Data/Vec3/Class.hs
@@ -1,13 +1,19 @@
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
 {-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE TypeSynonymInstances #-}
 
 module Data.Vec3.Class
     ( Vec3(..)
+    , TVec3
     )
 
 where
 
 import Prelude hiding (zipWith)
 
+import Test.QuickCheck (Arbitrary(..))
+
 -- | Three-dimensional vector, with an associated matrix type.
 class Vec3 v where
     -- | Associated type for 3×3 matrix.
@@ -164,3 +170,30 @@
                         (r11, r12, r13) = toRows m1
                         (r21, r22, r23) = toRows m2
     {-# INLINE addM #-}
+
+
+-- | 'Vec3' implementation with 'Data.Vector.Unboxed.Unbox' instance
+-- based on default Unbox instance for tuples of arrays, which wraps a
+-- vector of tuples as a tuple of vectors.
+--
+-- @
+-- interface:  [v1 (x, y, z); v2 (x, y, z) ...], length = N
+--                  |  |  |       |  |  |
+-- storage(x): [v1x-+  |  | ; v2x-+  |  |  ...], length = N
+-- storage(y): [v1y----+  | ; v2y----+  |  ...], length = N
+-- storage(z): [v1z-------+ ; v2z-------+  ...], length = N
+-- @
+--
+-- You almost definitely want to use 'Data.Vec3.CVec3' instead as it has better
+-- performance.
+type TVec3 = (Double, Double, Double)
+
+instance Vec3 TVec3 where
+  newtype Matrix TVec3 = TMatrix { unTMatrix :: (TVec3, TVec3, TVec3) }
+                       deriving (Arbitrary, Eq, Show)
+
+  fromXYZ = id
+  toXYZ   = id
+
+  fromRows = TMatrix
+  toRows   = unTMatrix
diff --git a/src/Data/Vec3/Tupled.hs b/src/Data/Vec3/Tupled.hs
deleted file mode 100644
--- a/src/Data/Vec3/Tupled.hs
+++ /dev/null
@@ -1,64 +0,0 @@
-{-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE MultiParamTypeClasses #-}
-{-# LANGUAGE TemplateHaskell #-}
-{-# LANGUAGE TypeFamilies #-}
-
-module Data.Vec3.Tupled
-    ( TVec3(..)
-    )
-
-where
-
-import Prelude
-
-import Data.Vector.Unboxed.Deriving
-
-import Data.Vec3.Class
-
-
--- | 'Vec3' implementation with 'Data.Vector.Unboxed.Unbox' instance
--- based on default Unbox instance for tuples of arrays, which wraps a
--- vector of tuples as a tuple of vectors.
---
--- @
--- interface:  [v1 (x, y, z); v2 (x, y, z) ...], length = N
---                  |  |  |       |  |  |
--- storage(x): [v1x-+  |  | ; v2x-+  |  |  ...], length = N
--- storage(y): [v1y----+  | ; v2y----+  |  ...], length = N
--- storage(z): [v1z-------+ ; v2z-------+  ...], length = N
--- @
---
--- You almost definitely want to use 'CVec3' instead as it has better
--- performance.
-newtype TVec3 = TVec3 (Double, Double, Double)
-               deriving (Eq, Show)
-
-
-derivingUnbox "TVec3"
-  [t|TVec3 -> (Double, Double, Double)|]
-  [|\(TVec3 v) -> v|]
-  [|TVec3|]
-
-
-instance Vec3 TVec3 where
-    newtype Matrix TVec3 = TMatrix (TVec3, TVec3, TVec3)
-                           deriving (Eq, Show)
-
-
-    fromXYZ = TVec3
-    {-# INLINE fromXYZ #-}
-
-    toXYZ (TVec3 v) = v
-    {-# INLINE toXYZ #-}
-
-    fromRows (r1, r2, r3) = TMatrix (r1, r2, r3)
-    {-# INLINE fromRows #-}
-
-    toRows (TMatrix (r1, r2, r3)) = (r1, r2, r3)
-    {-# INLINE toRows #-}
-
-
-derivingUnbox "TMatrix"
-  [t|Matrix TVec3 -> (TVec3, TVec3, TVec3)|]
-  [|\(TMatrix v) -> v|]
-  [|TMatrix|]
diff --git a/tests/Main.hs b/tests/Main.hs
new file mode 100644
--- /dev/null
+++ b/tests/Main.hs
@@ -0,0 +1,84 @@
+{-# LANGUAGE FlexibleContexts    #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+import Data.Proxy
+
+import Test.Tasty
+import Test.Tasty.QuickCheck
+
+import Data.Vec3
+
+
+infix 4 <~=>
+(<~=>) :: forall v1 v2. (Vec3 v1, Vec3 v2) => v1 -> v2 -> Bool
+(<~=>) a b = ax ~= bx &&
+             ay ~= by &&
+             az ~= bz
+  where
+    (ax, ay, az) = toXYZ (a :: v1)
+    (bx, by, bz) = toXYZ (b :: v2)
+
+
+infix 4 ~=
+(~=) :: Double -> Double -> Bool
+(~=) a b | a == b = True
+         | a == 0 || b == 0 || isDenormalized absDiff = absDiff < maxError
+         | otherwise = absDiff / (abs a + abs b) < maxError
+  where
+    absDiff = abs $ a - b
+    maxError = 1e-13
+
+
+tests
+  :: forall ty.
+     ( Arbitrary ty, Arbitrary (Matrix ty)
+     , Show ty, Show (Matrix ty)
+     , Vec3 ty, Eq ty
+     )
+  => Proxy ty
+  -> [TestTree]
+tests _ =
+  [ testProperty
+    "Commutativity of addition: a + b = b + a"
+    (\(a :: ty) b -> a <+> b <~=> b <+> a)
+  , testProperty
+    "Associativity of addition: (a + b) + c = a + (b + c)"
+    (\(a :: ty) b c -> (a <+> b) <+> c <~=> a <+> (b <+> c))
+  , testProperty
+    "Identity element of addition (zero): v + 0 = v"
+    (\(v :: ty) -> (v <+> origin <~=> v))
+  , testProperty
+    "Inverse elements of addition: v + (-v) = 0"
+    (\(v :: ty) -> (v <+> invert v <~=> (origin :: ty)))
+  , testProperty
+    "Compatibility of scalar and field multiplication"
+    (\(v :: ty) p q -> (v .^ p .^ q <~=> v .^ (p * q)))
+  , testProperty
+    "Identity of scalar multiplication"
+    (\(v :: ty) -> (v .^ 1 <~=> v))
+  , testProperty
+    "Distributivity wrt vector addition"
+    (\(a :: ty) b p -> ((a <+> b) .^ p) <~=> (a .^ p) <+> (b .^ p))
+  , testProperty
+    "Distributivity wrt scalar addition"
+    (\(a :: ty) p q -> (a .^ (p + q) <~=> (a .^ p) <+> (a .^ q)))
+  , testProperty
+    "Subtraction definition"
+    (\(a :: ty) b -> (a <+> invert b <~=> a <-> b))
+  , testProperty
+    "Normalization"
+    (\(v :: ty) -> (v <~=> (origin :: ty) || norm (normalize v) ~= 1))
+  , testProperty
+    "Triangle inequality"
+    (\(a :: ty) b c -> (distance a b + distance b c >= distance a c))
+  , testProperty
+    "Diagonal matrix multiplication"
+    (\(v :: ty) (s :: Double) -> (diag s `mxv` v == v .^ s))
+  ]
+
+
+main :: IO ()
+main = defaultMain $ testGroup "Tests"
+  [ testGroup "CVec3" (tests (Proxy :: Proxy CVec3))
+  , testGroup "TVec3" (tests (Proxy :: Proxy TVec3))
+  ]
diff --git a/tests/Tests.hs b/tests/Tests.hs
deleted file mode 100644
--- a/tests/Tests.hs
+++ /dev/null
@@ -1,68 +0,0 @@
-{-# LANGUAGE ScopedTypeVariables #-}
-
-import Test.Tasty
-import Test.Tasty.QuickCheck
-
-import Data.Vec3
-
-
-infix 4 <~=>
-(<~=>) :: CVec3 -> CVec3 -> Bool
-(<~=>) a b = ax ~= bx &&
-             ay ~= by &&
-             az ~= bz
-  where
-    (ax, ay, az) = toXYZ (a :: CVec3)
-    (bx, by, bz) = toXYZ (b :: CVec3)
-
-
-infix 4 ~=
-(~=) :: Double -> Double -> Bool
-(~=) a b | a == b = True
-         | a == 0 || b == 0 || isDenormalized absDiff = absDiff < maxError
-         | otherwise = absDiff / (abs a + abs b) < maxError
-  where
-    absDiff = abs $ a - b
-    maxError = 1e-13
-
-
-tests :: [TestTree]
-tests =
-  [ testProperty
-    "Commutativity of addition: a + b = b + a"
-    (\(a :: CVec3) b -> a <+> b <~=> b <+> a)
-  , testProperty
-    "Associativity of addition: (a + b) + c = a + (b + c)"
-    (\(a :: CVec3) b c -> (a <+> b) <+> c <~=> a <+> (b <+> c))
-  , testProperty
-    "Identity element of addition (zero): v + 0 = v"
-    (\(v :: CVec3) -> (v <+> origin <~=> v))
-  , testProperty
-    "Inverse elements of addition: v + (-v) = 0"
-    (\(v :: CVec3) -> (v <+> invert v <~=> origin))
-  , testProperty
-    "Compatibility of scalar and field multiplication"
-    (\(v :: CVec3) p q -> (v .^ p .^ q <~=> v .^ (p * q)))
-  , testProperty
-    "Identity of scalar multiplication"
-    (\(v :: CVec3) -> (v .^ 1 <~=> v))
-  , testProperty
-    "Distributivity wrt vector addition"
-    (\(a :: CVec3) b p -> ((a <+> b) .^ p) <~=> (a .^ p) <+> (b .^ p))
-  , testProperty
-    "Distributivity wrt scalar addition"
-    (\(a :: CVec3) p q -> (a .^ (p + q) <~=> (a .^ p) <+> (a .^ q)))
-  , testProperty
-    "Subtraction definition"
-    (\(a :: CVec3) b -> (a <+> invert b <~=> a <-> b))
-  , testProperty
-    "Normalization"
-    (\(v :: CVec3) -> (v <~=> origin || norm (normalize v) ~= 1))
-  , testProperty
-    "Triangle inequality"
-    (\(a :: CVec3) b c -> (distance a b + distance b c >= distance a c))
-  ]
-
-
-main :: IO ()
-main = defaultMain $ testGroup "Tests" tests
diff --git a/tests/doctest-driver.hs b/tests/doctest-driver.hs
new file mode 100644
--- /dev/null
+++ b/tests/doctest-driver.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF doctest-discover #-}
