diff --git a/ip.cabal b/ip.cabal
--- a/ip.cabal
+++ b/ip.cabal
@@ -1,5 +1,6 @@
+cabal-version: 2.0
 name: ip
-version: 1.4.1
+version: 1.4.2
 synopsis: Library for IP and MAC addresses
 homepage: https://github.com/andrewthad/haskell-ip#readme
 license: BSD3
@@ -9,7 +10,6 @@
 copyright: 2016 Andrew Martin
 category: web
 build-type: Simple
-cabal-version: >=1.10
 description:
   The `ip` package provides types and functions for dealing with
   IPv4 addresses, CIDR blocks, and MAC addresses. We provide instances
@@ -68,7 +68,7 @@
     , test-framework
     , test-framework-quickcheck2
     , QuickCheck
-    , quickcheck-classes >= 0.4.13 && < 0.5
+    , quickcheck-classes >= 0.4.13 && < 0.7.0.0
     , text
     , bytestring
     , HUnit
@@ -89,13 +89,13 @@
   build-depends:
       base
     , ip
-    , hspec
-    , hspec >= 2.4.4
+    , hspec >= 2.7
   other-modules:
     Net.IPv4Spec
     Net.IPv4.RangeSpec
   ghc-options: -Wall -O2
   default-language: Haskell2010
+  build-tool-depends: hspec-discover:hspec-discover >= 2.7
 
 test-suite doctest
   type: exitcode-stdio-1.0
diff --git a/src/Data/ByteString/Builder/Fixed.hs b/src/Data/ByteString/Builder/Fixed.hs
--- a/src/Data/ByteString/Builder/Fixed.hs
+++ b/src/Data/ByteString/Builder/Fixed.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE CPP #-}
 {-# LANGUAGE RankNTypes #-}
 {-# LANGUAGE GADTs #-}
 {-# LANGUAGE OverloadedStrings #-}
@@ -22,7 +23,9 @@
   , word12HexFixedUpper
   ) where
 
+#if !MIN_VERSION_base(4,11,0)
 import Data.Monoid
+#endif
 import Data.Word
 import Data.Word.Synthetic.Word12 (Word12)
 import Data.Bits
diff --git a/src/Data/Text/Builder/Fixed.hs b/src/Data/Text/Builder/Fixed.hs
--- a/src/Data/Text/Builder/Fixed.hs
+++ b/src/Data/Text/Builder/Fixed.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE CPP #-}
 {-# LANGUAGE RankNTypes #-}
 {-# LANGUAGE GADTs #-}
 {-# LANGUAGE OverloadedStrings #-}
@@ -22,7 +23,9 @@
   ) where
 
 import Control.Monad.ST
+#if !MIN_VERSION_base(4,11,0)
 import Data.Monoid
+#endif
 import Data.Word
 import Data.Bits
 import Data.Char (ord)
diff --git a/src/Data/Text/Builder/Variable.hs b/src/Data/Text/Builder/Variable.hs
--- a/src/Data/Text/Builder/Variable.hs
+++ b/src/Data/Text/Builder/Variable.hs
@@ -8,7 +8,7 @@
     than the builder provided by the @text@ library. However,
     data whose textual encoding has no known upper bound cannot
     be encoded by the builder provided here. For example, it
-    is possible to provide decimal builders for types like 'Int8' and
+    is possible to provide decimal builders for types like 'Data.Int.Int8' and
     'Word16', whose lengths are respectively bounded by
     4 and 5. However, this is not possible for 'Integer', since
     its decimal representation could be arbitrarily long.
diff --git a/src/Net/IP.hs b/src/Net/IP.hs
--- a/src/Net/IP.hs
+++ b/src/Net/IP.hs
@@ -60,6 +60,15 @@
 import qualified Data.Aeson as Aeson
 import qualified Data.Text.IO as TIO
 
+-- | Run a function over an 'IP' depending on its status
+--   as an 'IPv4' or 'IPv6'.
+--
+--   >>> case_ IPv4.encode IPv6.encode (ipv4 192 168 2 47)
+--   "192.168.2.47"
+--
+--   >>> addr = ipv6 0x2001 0x0db8 0x0000 0x0000 0x0000 0x0000 0x0000 0x0001
+--   >>> case_ IPv4.encode IPv6.encode addr
+--   "2001:db8::1"
 case_ :: (IPv4 -> a) -> (IPv6 -> a) -> IP -> a
 case_ f g (IP addr@(IPv6 w1 w2)) = if w1 == 0 && (0xFFFFFFFF00000000 .&. w2 == 0x0000FFFF00000000)
   then f (IPv4 (fromIntegral w2))
@@ -77,15 +86,19 @@
      -> IP
 ipv6 a b c d e f g h = fromIPv6 (IPv6.fromWord16s a b c d e f g h)
 
+-- | Turn an 'IPv4' into an 'IP'.
 fromIPv4 :: IPv4 -> IP
 fromIPv4 (IPv4 w) = IP (IPv6 0 (0x0000FFFF00000000 .|. fromIntegral w))
 
+-- | Turn an 'IPv6' into an 'IP'.
 fromIPv6 :: IPv6 -> IP
 fromIPv6 = IP
 
+-- | Encode an 'IP' as 'Text'.
 encode :: IP -> Text
 encode = case_ IPv4.encode IPv6.encode
 
+-- | Decode an 'IP' from 'Text'.
 decode :: Text -> Maybe IP
 decode t = case IPv4.decode t of
   Nothing -> case IPv6.decode t of
@@ -93,6 +106,7 @@
     Just v6 -> Just (fromIPv6 v6)
   Just v4 -> Just (fromIPv4 v4)
 
+-- | Print an 'IP' using the textual encoding.
 print :: IP -> IO ()
 print = TIO.putStrLn . encode
 
diff --git a/src/Net/IPv4.hs b/src/Net/IPv4.hs
--- a/src/Net/IPv4.hs
+++ b/src/Net/IPv4.hs
@@ -1,25 +1,14 @@
 {-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE CPP #-}
 {-# LANGUAGE DeriveGeneric #-}
 {-# LANGUAGE GeneralizedNewtypeDeriving #-}
-{-# LANGUAGE UnboxedTuples #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
 {-# LANGUAGE TypeFamilies #-}
 {-# LANGUAGE TypeInType #-}
-{-# LANGUAGE CPP #-}
-
-{-# OPTIONS_GHC -Wall #-}
-{-| An IPv4 data type
-
-    This module provides the IPv4 data type and functions for working
-    with it. There are also encoding and decoding functions provided
-    in this module, but they should be imported from
-    @Net.IPv4.Text@ and @Net.ByteString.Char8@ instead. They are
-    defined here so that the 'FromJSON' and 'ToJSON' instances can
-    use them.
+{-# LANGUAGE UnboxedTuples #-}
 
-    At some point, a highly efficient IPv4-to-ByteString function needs
-    to be added to this module to take advantage of @aeson@'s new
-    @toEncoding@ method.
+{-| This module provides the IPv4 data type and functions for working
+    with it.
 -}
 
 module Net.IPv4
@@ -31,6 +20,7 @@
     -- * Special IP Addresses
   , any
   , loopback
+  , localhost
   , broadcast
     -- * Range Predicates
   , private
@@ -174,15 +164,31 @@
   , fromIntegral w
   )
 
--- | The IP address representing any host: @0.0.0.0@
+-- | The IP address representing any host.
+--
+--   >>> any
+--   ipv4 0 0 0 0
 any :: IPv4
 any = IPv4 0
 
--- | The local loopback IP address: @127.0.0.1@
+-- | The local loopback IP address.
+--
+--   >>> loopback
+--   ipv4 127 0 0 1
 loopback :: IPv4
 loopback = fromOctets 127 0 0 1
 
--- | The broadcast IP address: @255.255.255.255@
+-- | A useful and common alias for 'loopback'.
+--
+--   >>> localhost
+--   ipv4 127 0 0 1
+localhost :: IPv4
+localhost = loopback
+
+-- | The broadcast IP address.
+--
+--   >>> broadcast
+--   ipv4 255 255 255 255
 broadcast :: IPv4
 broadcast = fromOctets 255 255 255 255
 
@@ -255,20 +261,46 @@
 encode = toDotDecimalText
 
 -- | Decode an 'IPv4' address.
+--
+--   >>> decode (Text.pack "192.168.2.47")
+--   Just (ipv4 192 168 2 47)
+--
+--   >>> decode (Text.pack "10.100.256.256")
+--   Nothing
 decode :: Text -> Maybe IPv4
 decode = decodeIPv4TextMaybe
 
 -- | Encode an 'IPv4' address to a text 'TBuilder.Builder'.
+--
+--   >>> builder (ipv4 192 168 2 47)
+--   "192.168.2.47"
 builder :: IPv4 -> TBuilder.Builder
 builder = toDotDecimalBuilder
 
+-- | Parse an 'IPv4' address using a 'TextRead.Reader'.
+--
+--   >>> reader (Text.pack "192.168.2.47")
+--   Right (ipv4 192 168 2 47,"")
+--
+--   >>> reader (Text.pack "192.168.2.470")
+--   Left "All octets in an IPv4 address must be between 0 and 255"
 reader :: TextRead.Reader IPv4
 reader = decodeIPv4TextReader
 
+-- | Parse an 'IPv4' address using a 'AT.Parser'.
+--
+--   >>> AT.parseOnly parser (Text.pack "192.168.2.47")
+--   Right (ipv4 192 168 2 47)
+--
+--   >>> AT.parseOnly parser (Text.pack "192.168.2.470")
+--   Left "Failed reading: All octets in an IPv4 address must be between 0 and 255"
 parser :: AT.Parser IPv4
 parser = dotDecimalParser
 
 -- | Encode an 'IPv4' address to a UTF-8 encoded 'ByteString'.
+--
+--   >>> encodeUtf8 (ipv4 192 168 2 47)
+--   "192.168.2.47"
 encodeUtf8 :: IPv4 -> ByteString
 encodeUtf8 = toBSPreAllocated
 
@@ -311,14 +343,26 @@
               poke ptr (word + 48)
               return 1
 
--- This should be rewritten to not go through text
--- as an intermediary.
+-- | Decode a UTF8-encoded 'ByteString' into an 'IPv4'.
+--
+--   >>> decodeUtf8 (BC8.pack "192.168.2.47")
+--   Just (ipv4 192 168 2 47)
 decodeUtf8 :: ByteString -> Maybe IPv4
 decodeUtf8 = decode <=< rightToMaybe . decodeUtf8'
+-- This (decodeUtf8) should be rewritten to not go through text
+-- as an intermediary.
 
+-- | Encode an 'IPv4' as a 'Builder.Builder'
 builderUtf8 :: IPv4 -> Builder.Builder
 builderUtf8 = Builder.byteString . encodeUtf8
 
+-- | Parse an 'IPv4' using a 'AB.Parser'.
+--
+--   >>> AB.parseOnly parserUtf8 (BC8.pack "192.168.2.47")
+--   Right (ipv4 192 168 2 47)
+--
+--   >>> AB.parseOnly parserUtf8 (BC8.pack "192.168.2.470")
+--   Left "Failed reading: All octets in an ipv4 address must be between 0 and 255"
 parserUtf8 :: AB.Parser IPv4
 parserUtf8 = fromOctets'
   <$> (AB.decimal >>= limitSize)
@@ -344,9 +388,11 @@
 
 -}
 
+-- | Encode an 'IPv4' as a 'String'.
 encodeString :: IPv4 -> String
 encodeString = Text.unpack . encode
 
+-- | Decode an 'IPv4' from a 'String'.
 decodeString :: String -> Maybe IPv4
 decodeString = decode . Text.pack
 
@@ -382,6 +428,7 @@
     d <- step readPrec
     return (fromOctets a b c d)
 
+-- | Print an 'IPv4' using the textual encoding.
 print :: IPv4 -> IO ()
 print = TIO.putStrLn . encode
 
@@ -745,6 +792,10 @@
 lowerInclusive (IPv4Range (IPv4 w) len) =
   IPv4 (w .&. mask len)
 
+-- | The inclusive upper bound of an 'IPv4Range'.
+--
+--   >>> T.putStrLn $ encode $ upperInclusive $ IPv4Range (ipv4 10 10 1 160) 25
+--   10.10.1.255
 upperInclusive :: IPv4Range -> IPv4
 upperInclusive (IPv4Range (IPv4 w) len) =
   let theInvertedMask = shiftR 0xffffffff (fromIntegral len)
@@ -786,6 +837,8 @@
   let totalAddrs = countAddrs len
    in wordSuccessors totalAddrs ip
 
+-- | A stream-polymorphic generator over an 'IPv4Range'.
+--   For more information, see <http://www.haskellforall.com/2014/11/how-to-build-library-agnostic-streaming.html How to build library-agnostic streaming sources>.
 toGenerator :: MonadPlus m => IPv4Range -> m IPv4
 toGenerator (IPv4Range ip len) =
   let totalAddrs = countAddrs len
@@ -827,15 +880,19 @@
       w' = w .&. mask len'
    in IPv4Range (IPv4 w') len'
 
+-- | Encode an 'IPv4Range' as 'Text'.
 encodeRange :: IPv4Range -> Text
 encodeRange = rangeToDotDecimalText
 
+-- | Decode an 'IPv4Range' from 'Text'.
 decodeRange :: Text -> Maybe IPv4Range
 decodeRange = rightToMaybe . AT.parseOnly (parserRange <* AT.endOfInput)
 
+-- | Encode an 'IPv4Range' to a 'TBuilder.Builder'.
 builderRange :: IPv4Range -> TBuilder.Builder
 builderRange = rangeToDotDecimalBuilder
 
+-- | Parse an 'IPv4Range' using a 'AT.Parser.'
 parserRange :: AT.Parser IPv4Range
 parserRange = do
   ip <- parser
diff --git a/src/Net/IPv6.hs b/src/Net/IPv6.hs
--- a/src/Net/IPv6.hs
+++ b/src/Net/IPv6.hs
@@ -8,8 +8,9 @@
 {-# LANGUAGE StandaloneDeriving  #-}
 {-# LANGUAGE UnboxedTuples       #-}
 
-{-# OPTIONS_GHC -Wall #-}
-
+{-| This module provides the IPv6 data type and functions for working
+    with it.
+-}
 module Net.IPv6
   ( -- * Convert
     ipv6
@@ -23,6 +24,7 @@
     -- * Special IP Addresses
   , any
   , loopback
+  , localhost
     -- * Textual Conversion
     -- ** Text
   , encode
@@ -228,6 +230,7 @@
 internal_ m s = case internal m s of
   (# s', _ #) -> s'
 
+-- | Print an 'IPv6' using the textual encoding.
 print :: IPv6 -> IO ()
 print = TIO.putStrLn . encode
 
@@ -269,6 +272,14 @@
 rightToMaybe :: Either a b -> Maybe b
 rightToMaybe = either (const Nothing) Just
 
+-- | This could be useful for the rare occasion
+--   in which one could construct an 'IPv6' from
+--   octets.
+--
+--   Note that while @Net.IPv4.'Net.IPv4.fromOctets' = Net.IPv4.'Net.IPv4.ipv4'@,
+--   @Net.IPv6.fromOctets /= Net.IPv6.ipv6@. While this should be obvious
+--   from their types, it is worth mentioning since the similarity in naming
+--   might be confusing.
 fromOctets ::
      Word8 -> Word8 -> Word8 -> Word8
   -> Word8 -> Word8 -> Word8 -> Word8
@@ -349,9 +360,24 @@
   , fromIntegral b
   )
 
+-- | The local loopback IP address.
+--
+--   >>> loopback
+--   ipv6 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0001
 loopback :: IPv6
 loopback = IPv6 0 1
 
+-- | A useful alias for 'loopback'.
+--
+--   >>> localhost
+--   ipv6 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0001
+localhost :: IPv6
+localhost = loopback
+
+-- | The IP address representing any host.
+--   
+--   >>> any 
+--   ipv6 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000
 any :: IPv6
 any = IPv6 0 0
 
@@ -397,6 +423,11 @@
 decode :: Text -> Maybe IPv6
 decode t = rightToMaybe (AT.parseOnly (parser <* AT.endOfInput) t)
 
+-- | Parse an 'IPv6' using 'Atto.Parser'.
+--
+--   >>> ip = ipv6 0xDEAD 0xBEEF 0x3240 0xA426 0xBA68 0x1CD0 0x4263 0x109B
+--   >>> Atto.parseOnly parser (Text.pack "dead:beef:3240:a426:ba68:1cd0:4263:109b")
+--   Right (ipv6 0xdead 0xbeef 0x3240 0xa426 0xba68 0x1cd0 0x4263 0x109b)
 parser :: Atto.Parser IPv6
 parser = makeIP <$> ip
   where
@@ -496,6 +527,8 @@
 fromWord32Word64 :: Word64 -> Word64 -> Word64
 fromWord32Word64 a b = fromIntegral (unsafeShiftL a 32 .|. b)
 
+-- | An 'IPv6Range'. It is made up of the first 'IPv6' in the range
+--   and its length.
 data IPv6Range = IPv6Range
   { ipv6RangeBase   :: {-# UNPACK #-} !IPv6
   , ipv6RangeLength :: {-# UNPACK #-} !Word8
@@ -508,6 +541,25 @@
   then 0xffffffffffffffff 
   else complement (shiftR 0xffffffffffffffff (fromIntegral w))
 
+-- | Normalize an 'IPv6Range'. The first result of this is that the
+--   'IPv6' inside the 'IPv6Range' is changed so that the insignificant
+--   bits are zeroed out. For example:
+--
+--   >>> addr1 = ipv6 0x0192 0x0168 0x0001 0x0019 0x0000 0x0000 0x0000 0x0000
+--   >>> addr2 = ipv6 0x0192 0x0168 0x0001 0x0163 0x0000 0x0000 0x0000 0x0000
+--   >>> printRange $ normalize $ IPv6Range addr1 24
+--   192:100::/24 
+--   >>> printRange $ normalize $ IPv6Range addr2 28
+--   192:160::/28
+--
+--   The second effect of this is that the mask length is lowered to be 128
+--   or smaller. Working with 'IPv6Range's that have not been normalized does
+--   not cause any issues for this library, although other applications may
+--   reject such ranges (especially those with a mask length above 128).
+--
+--   Note that 'normalize is idempotent, that is:
+--
+--   prop> normalize r == (normalize . normalize) r
 normalize :: IPv6Range -> IPv6Range
 normalize (IPv6Range (IPv6 w1 w2) len) =
   let len' = min len 128
@@ -516,12 +568,23 @@
         | otherwise =  (IPv6Range (IPv6 (w1 .&. mask 64) (w2 .&. mask (len' - 64))) len')
   in norm
 
+-- | Encode an 'IPv6Range' as 'Text'.
+--
+--   >>> addr = ipv6 0xDEAD 0xBEEF 0x3240 0xA426 0xBA68 0x1CD0 0x4263 0x109B
+--   >>> T.putStrLn $ encodeRange $ IPv6Range addr 28
+--   dead:beef:3240:a426:ba68:1cd0:4263:109b/28
 encodeRange :: IPv6Range -> Text
 encodeRange x = encode (ipv6RangeBase x) <> Text.pack "/" <> (Text.pack $ (show . fromEnum) $ ipv6RangeLength x)
 
+-- | Decode an 'IPv6Range' from 'Text'.
+--
+--   >>> addr = ipv6 0xDEAD 0xBEEF 0x3240 0xA426 0xBA68 0x1CD0 0x4263 0x109B
+--   >>> fmap encodeRange $ decodeRange (Text.pack "dead:beef:3240:a426:ba68:1cd0:4263:109b/28")
+--   Just "dead:bee0::/28"
 decodeRange :: Text -> Maybe IPv6Range
 decodeRange = rightToMaybe . AT.parseOnly (parserRange <* AT.endOfInput)
 
+-- | Parse an 'IPv6Range' using a 'AT.Parser'.
 parserRange :: AT.Parser IPv6Range
 parserRange = do
   ip <- parser
@@ -581,7 +644,7 @@
 -- >>> T.putStrLn $ encode $ lowerInclusive $ IPv6Range (ipv6 0x2001 0x0db8 0x0000 0x0000 0x0000 0x0000 0x0000 0x0001) 25
 -- 2001:d80::
 --
--- Note that the lower bound of a normalized 'IPv4Range' is simply the
+-- Note that the lower bound of a normalized 'IPv6Range' is simply the
 -- ip address of the range:
 --
 -- prop> lowerInclusive r == ipv6RangeBase (normalize r)
@@ -589,6 +652,12 @@
 lowerInclusive (IPv6Range (IPv6 w1 w2) len) =
   ipv6RangeBase (normalize (IPv6Range (IPv6 w1 w2) len))
 
+-- | The inclusive upper bound of an 'IPv6Range'.
+--
+--   >>> let addr = ipv6 0xDEAD 0xBEEF 0x3240 0xA426 0xBA68 0x1CD0 0x4263 0x109B
+--   >>> T.putStrLn $ encode $ upperInclusive $ IPv6Range addr 25
+--   dead:beff:ffff:ffff:ffff:ffff:ffff:ffff
+--
 upperInclusive :: IPv6Range -> IPv6
 upperInclusive (IPv6Range (IPv6 w1 w2) len) =
   let len' = min 128 len
@@ -600,13 +669,29 @@
         | otherwise =  IPv6 (w1) (w2 .|. theInvertedMask2)
   in upper
 
--- | This exists mostly for testing purposes.
+-- | Print an 'IPv6Range' using the textual encoding.
 printRange :: IPv6Range -> IO ()
 printRange = TIO.putStrLn . encodeRange
 
+-- | Smart constructor for 'IPv6Range'. Ensures the mask is appropriately
+--   sized and sets masked bits in the 'IPv6' to zero.
+--
+--   >>> let addr = ipv6 0xDEAD 0xBEEF 0x3240 0xA426 0xBA68 0x1CD0 0x4263 0x109B
+--   >>> printRange $ range addr 25
+--   dead:be80::/25
 range :: IPv6 -> Word8 -> IPv6Range
 range addr len = normalize (IPv6Range addr len)
 
+-- | Given an inclusive lower and upper ip address, create the smallest 'IPv6Range'
+--   that contains the two. This is helpful in situations where input is given as a
+--   range, like @ @.
+--   
+--   This makes the range broader if it cannot be represented in <https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing CIDR> notation.
+--
+--   >>> addrLower = ipv6 0xDEAD 0xBE80 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000
+--   >>> addrUpper = ipv6 0xDEAD 0xBEFF 0xFFFF 0xFFFF 0xFFFF 0xFFFF 0xFFFF 0xFFFF
+--   >>> printRange $ fromBounds addrLower addrUpper
+--   dead:be80::/25
 fromBounds :: IPv6 -> IPv6 -> IPv6Range
 fromBounds (IPv6 a1 a2) (IPv6 b1 b2) =
   normalize (IPv6Range (IPv6 a1 a2) (maskFromBounds a1 b1 a2 b2))
diff --git a/src/Net/Mac.hs b/src/Net/Mac.hs
--- a/src/Net/Mac.hs
+++ b/src/Net/Mac.hs
@@ -1,5 +1,7 @@
 {-# LANGUAGE BangPatterns #-}
 {-# LANGUAGE CPP #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
 {-# LANGUAGE InstanceSigs #-}
 {-# LANGUAGE MagicHash #-}
 {-# LANGUAGE MultiParamTypeClasses #-}
@@ -7,10 +9,9 @@
 {-# LANGUAGE TypeFamilies #-}
 {-# LANGUAGE UnboxedTuples #-}
 
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-
-{-# OPTIONS_GHC -Wall #-}
+{-| This module provides the Mac data type and functions for working
+    with it.
+-}
 module Net.Mac
   ( -- * Convert
     mac
@@ -80,6 +81,7 @@
 import qualified Data.Text.Builder.Fixed as TFB
 import qualified Data.Text.IO as TIO
 import qualified Data.Text.Lazy.Builder as TBuilder
+import qualified Data.Text as Text ()
 
 -- $setup
 --
@@ -144,14 +146,21 @@
 c2w :: Char -> Word8
 c2w = fromIntegral . ord
 
--- | Encode a 'Mac' address lowercase hex, separating every two characters
---   with a colon:
+-- | Encode a 'Mac' address using the default 'MacCodec' 'defCodec'.
 --
 --   >>> T.putStrLn (encode (Mac 0xA47F247AB423))
 --   a4:7f:24:7a:b4:23
 encode :: Mac -> Text
 encode = encodeWith defCodec
 
+-- | Encode a 'Mac' address using the given 'MacCodec'.
+--
+--   >>> m = Mac 0xA47F247AB423
+--   >>> T.putStrLn $ encodeWith defCodec m
+--   a4:7f:24:7a:b4:23
+--
+--   >>> T.putStrLn $ encodeWith (MacCodec (MacGroupingTriples '-') True) m
+--   A47-F24-7AB-423
 encodeWith :: MacCodec -> Mac -> Text
 encodeWith (MacCodec g u) m = case g of
   MacGroupingNoSeparator -> case u of
@@ -168,18 +177,49 @@
     True -> TFB.run (fixedBuilderQuadruples TFB.word8HexFixedUpper) (Pair c m)
     False -> TFB.run (fixedBuilderQuadruples TFB.word8HexFixedLower) (Pair c m)
 
+-- | Decode a 'Mac' address using the default 'MacCodec' 'defCodec'.
+--
+--   >>> decode (Text.pack "a4:7f:24:7a:b4:23")
+--   Just (mac 0xa47f247ab423)
+--
+--   >>> decode (Text.pack "a47-f24-7ab-423")
+--   Nothing
 decode :: Text -> Maybe Mac
 decode = decodeWith defCodec
 
+-- | Decode a 'Mac' address from 'Text' using the given 'MacCodec'.
+--
+-- >>> decodeWith defCodec (Text.pack "a4:7f:24:7a:b4:23")
+-- Just (mac 0xa47f247ab423)
+--
+-- >>> decodeWith (MacCodec MacGroupingNoSeparator False) (Text.pack "a47f247ab423")
+-- Just (mac 0xa47f247ab423)
 decodeWith :: MacCodec -> Text -> Maybe Mac
 decodeWith codec t = rightToMaybe (AT.parseOnly (parserWith codec <* AT.endOfInput) t)
 
+-- | Encode a 'Mac' address as a 'TBuilder.Builder'.
 builder :: Mac -> TBuilder.Builder
 builder = TBuilder.fromText . encode
 
+-- | Parse a 'Mac' address using a 'AT.Parser'.
+--
+--   >>> AT.parseOnly parser (Text.pack "a4:7f:24:7a:b4:23")
+--   Right (mac 0xa47f247ab423)
+--
+--   >>> AT.parseOnly parser (Text.pack "a47-f24-7ab-423")
+--   Left "':': Failed reading: satisfy"
 parser :: AT.Parser Mac
 parser = parserWith defCodec
 
+-- | Parser a 'Mac' address using the given 'MacCodec'.
+--
+--   >>> p1 = parserWith defCodec
+--   >>> AT.parseOnly p1 (Text.pack "a4:7f:24:7a:b4:23")
+--   Right (mac 0xa47f247ab423)
+--
+--   >>> p2 = parserWith (MacCodec MacGroupingNoSeparator False)
+--   >>> AT.parseOnly p2 (Text.pack "a47f247ab423")
+--   Right (mac 0xa47f247ab423)
 parserWith :: MacCodec -> AT.Parser Mac
 parserWith (MacCodec g _) = case g of
   MacGroupingQuadruples c -> parserQuadruples c
@@ -187,6 +227,10 @@
   MacGroupingPairs c -> parserPairs c
   MacGroupingNoSeparator -> parserNoSeparator
 
+-- | The default 'MacCodec': all characters are lowercase hex, separated by colons into pairs.
+--
+--   >>> T.putStrLn $ encodeWith defCodec (Mac 0xa47f247ab423)
+--   a4:7f:24:7a:b4:23
 defCodec :: MacCodec
 defCodec = MacCodec (MacGroupingPairs ':') False
 
@@ -320,8 +364,7 @@
 word12At i (Mac w) = fromIntegral (unsafeShiftR w i)
 {-# INLINE word12At #-}
 
--- | Encode a 'Mac' address, as lowercase hexadecimal digits
---   separated by a colon:
+-- | Encode a 'Mac' address using the default 'MacCodec' 'defCodec'.
 --
 --   >>> BC.putStrLn (encodeUtf8 (mac 0x64255A0F2C47))
 --   64:25:5a:0f:2c:47
@@ -329,7 +372,7 @@
 encodeUtf8 = encodeWithUtf8 defCodec
 
 -- | Lenient decoding of MAC address that accepts lowercase, uppercase,
---   and any kind separator.
+--   and any kind of separator.
 --
 --   >>> decodeUtf8 "A2:DE:AD:BE:EF:67"
 --   Just (mac 0xa2deadbeef67)
@@ -340,6 +383,13 @@
 decodeUtf8 :: ByteString -> Maybe Mac
 decodeUtf8 = decodeLenientUtf8
 
+-- | Decode a 'ByteString' as a 'Mac' address using the given 'MacCodec'.
+--
+--   >>> decodeWithUtf8 defCodec (BC.pack "64:25:5a:0f:2c:47")
+--   Just (mac 0x64255a0f2c47)
+--
+--   >>> decodeWithUtf8 (MacCodec MacGroupingNoSeparator False) (BC.pack "64255a0f2c47")
+--   Just (mac 0x64255a0f2c47)
 decodeWithUtf8 :: MacCodec -> ByteString -> Maybe Mac
 decodeWithUtf8 codec bs = rightToMaybe (AB.parseOnly (parserWithUtf8 codec <* AB.endOfInput) bs)
 
@@ -468,6 +518,14 @@
 parseWord8Hex :: Word8 -> AB.Parser Word8
 parseWord8Hex = tryParseWord8Hex (fail "invalid hexadecimal character")
 
+-- | Encode a 'Mac' address as a 'ByteString' using the given 'MacCodec'.
+--
+--   >>> m = Mac 0xA47F247AB423
+--   >>> BC.putStrLn $ encodeWithUtf8 defCodec m
+--   a4:7f:24:7a:b4:23
+--
+--   >>> BC.putStrLn $ encodeWithUtf8 (MacCodec (MacGroupingTriples '-') True) m
+--   A47-F24-7AB-423
 encodeWithUtf8 :: MacCodec -> Mac -> ByteString
 encodeWithUtf8 (MacCodec g u) m = case g of
   MacGroupingNoSeparator -> case u of
@@ -657,6 +715,7 @@
   toEnum i = Mac (toEnum i)
   fromEnum (Mac x) = fromEnum x
 
+-- | Print a 'Mac' address using the textual encoding.
 print :: Mac -> IO ()
 print = TIO.putStrLn . encode
 
@@ -673,6 +732,8 @@
   | w < 10 = chr (fromIntegral (w + 48))
   | otherwise = chr (fromIntegral (w + 87))
 
+-- | A 'MacCodec' allows users to control the encoding/decoding
+--   of their 'Mac' addresses.
 data MacCodec = MacCodec
   { macCodecGrouping :: !MacGrouping
   , macCodecUpperCase :: !Bool
diff --git a/src/Net/Types.hs b/src/Net/Types.hs
--- a/src/Net/Types.hs
+++ b/src/Net/Types.hs
@@ -1,5 +1,5 @@
-{-# OPTIONS_GHC -Wall -Werror #-}
-
+{-| This module re-exports all of the thematic types that this library defines.
+-}
 module Net.Types
   ( IPv4(..)
   , IPv6(..)
diff --git a/test/Doctests.hs b/test/Doctests.hs
--- a/test/Doctests.hs
+++ b/test/Doctests.hs
@@ -10,4 +10,5 @@
   , "src/Data/Text/Builder/Fixed.hs"
   , "src/Data/ByteString/Builder/Fixed.hs"
   , "src/Net/Mac.hs"
+  , "-XCPP"
   ]
diff --git a/test/Net/IPv4/RangeSpec.hs b/test/Net/IPv4/RangeSpec.hs
--- a/test/Net/IPv4/RangeSpec.hs
+++ b/test/Net/IPv4/RangeSpec.hs
@@ -3,7 +3,6 @@
 import Prelude hiding (any)
 import Data.Bits
 import Net.IPv4
-import Net.IPv4.Range
 import Test.Hspec
 
 spec :: Spec
