diff --git a/ip.cabal b/ip.cabal
--- a/ip.cabal
+++ b/ip.cabal
@@ -1,5 +1,5 @@
 name: ip
-version: 1.0.0
+version: 1.1.0
 synopsis: Library for IP and MAC addresses
 homepage: https://github.com/andrewthad/haskell-ip#readme
 license: BSD3
@@ -48,13 +48,13 @@
     Data.ByteString.Builder.Fixed
   build-depends:
       base >= 4.8  && < 5
-    , attoparsec
+    , attoparsec >= 0.13 && < 0.14
     , aeson >= 0.9  && < 1.3
     , hashable >= 1.2  && < 1.3
     , text >= 1.2  && < 1.3
     , bytestring >= 0.10 && < 0.11
     , vector >= 0.11 && < 0.13
-    , primitive
+    , primitive >= 0.6 && < 0.7
   -- if impl(ghcjs)
   --   build-depends: ghcjs-base >= 0.2 && < 0.3
   ghc-options: -Wall -O2
@@ -70,6 +70,7 @@
     , test-framework
     , test-framework-quickcheck2
     , QuickCheck
+    , quickcheck-classes
     , text
     , bytestring
     , HUnit
@@ -81,13 +82,13 @@
     IPv4Text2
     IPv4ByteString1
     -- IPv4TextVariableBuilder
-  ghc-options:         -Wall -O2
-  default-language:    Haskell2010
+  ghc-options: -Wall -O2
+  default-language: Haskell2010
 
 test-suite doctest
-  type:                exitcode-stdio-1.0
-  hs-source-dirs:      test
-  main-is:             Doctests.hs
+  type: exitcode-stdio-1.0
+  hs-source-dirs: test
+  main-is: Doctests.hs
   build-depends:
       base
     , ip
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
@@ -3,7 +3,7 @@
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE BangPatterns #-}
 
-{-# OPTIONS_GHC -O2 -Wall -funbox-strict-fields #-}
+{-# OPTIONS_GHC -Wall -funbox-strict-fields #-}
 
 {-| For concatenating fixed-width strings that are only a few
     characters each, this can be six times faster than the builder
@@ -22,22 +22,17 @@
   , word12HexFixedUpper
   ) where
 
-import Control.Monad.ST
 import Data.Monoid
 import Data.Word
 import Data.Word.Synthetic.Word12 (Word12)
 import Data.Bits
 import Data.Char (ord)
 import Text.Printf
-import Debug.Trace
 import Data.ByteString.Internal (ByteString(..))
 import Foreign
 import Data.ByteString.Short (ShortByteString)
-import System.IO.Unsafe
 import qualified Data.ByteString.Internal as BI
-import qualified Data.ByteString.Builder as BBuilder
 import qualified Data.ByteString as ByteString
-import qualified Data.ByteString.Lazy as LByteString
 import qualified Data.ByteString.Char8 as BC8
 import qualified Data.ByteString.Short.Internal as SBS
 
@@ -138,10 +133,6 @@
 word8 :: Builder Word8
 word8 = BuilderFunction (BC8.pack "-") $ \i marr w -> pokeByteOff marr i w
 {-# INLINE word8 #-}
-
-word8At :: Int -> Word64 -> Word8
-word8At i w = fromIntegral (unsafeShiftR w i)
-{-# INLINE word8At #-}
 
 -- | Taken from @Data.ByteString.Internal@. The same warnings
 --   apply here.
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
@@ -3,7 +3,7 @@
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE BangPatterns #-}
 
-{-# OPTIONS_GHC -O2 -Wall -funbox-strict-fields #-}
+{-# OPTIONS_GHC -Wall -funbox-strict-fields #-}
 
 {-| For concatenating fixed-width strings that are only a few
     characters each, this can be ten times faster than the builder
@@ -25,23 +25,12 @@
 import Data.Monoid
 import Data.Word
 import Data.Bits
-import Text.Printf
-import Debug.Trace
 import Data.Char (ord)
 import Data.Word.Synthetic.Word12 (Word12)
-import Data.Vector (Vector)
-import Data.Foldable (fold)
 import Data.Text (Text)
-import qualified Data.Vector as Vector
 import qualified Data.Text as Text
-import qualified Data.Text.Lazy as LText
-import qualified Data.Text.Lazy.IO as LText
-import qualified Data.Text.Lazy.Builder as TBuilder
-import qualified Data.Text.Lazy.Builder.Int as TBuilder
-import qualified Data.Text.IO as Text
 import qualified Data.Text.Array as A
 import qualified Data.Text.Internal as TI
-import qualified Data.Text.Internal.Unsafe.Char as TC
 import qualified Data.Text.Builder.Common.Internal as I
 
 data Builder a where
diff --git a/src/Data/Word/Synthetic/Word12.hs b/src/Data/Word/Synthetic/Word12.hs
--- a/src/Data/Word/Synthetic/Word12.hs
+++ b/src/Data/Word/Synthetic/Word12.hs
@@ -35,7 +35,6 @@
 import           GHC.Base
 import           GHC.Enum
 import           GHC.Num
-import           GHC.Ptr
 import           GHC.Read
 import           GHC.Real
 import           GHC.Show
diff --git a/src/Net/IP.hs b/src/Net/IP.hs
--- a/src/Net/IP.hs
+++ b/src/Net/IP.hs
@@ -1,9 +1,35 @@
+{-# OPTIONS_GHC -Wall #-}
+
+{-| An IP data type representing either an IPv4 address or
+    an IPv6 address. The user can think of this
+    as though it were a sum type. However, to minimize indirections,
+    it is actually implemented as an 'IPv6' address, with 'IPv4'
+    addresses being represented as an IPv4-mapped IPv6 addresses:
+
+    > +---------+---------+--------------+
+    > | 80 bits | 16 bits | 32 bits      |
+    > +---------+---------+--------------+
+    > | 00...00 | FFFF    | IPv4 address |
+    > +---------+---------+--------------+
+
+    All functions and instance methods that deal with textual conversion
+    will encode an 'IP' using either dot-decimal notation (for IPv4) or
+    RFC 5952 (for IPv6). They will decode an 'IP' from either format
+    as well. The 'Show' instance presents an address in as valid haskell code 
+    that resembles the formatted address:
+    
+    >>> decode "192.168.3.100"
+    Just (ipv4 192 168 3 100)
+    >>> decode "A3F5:12:F26::1466:8B91"
+    Just (ipv6 0xa3f5 0x0012 0x0f26 0x0000 0x0000 0x0000 0x1466 0x8b91)
+-}
+
 module Net.IP
   ( -- * Pattern Matching
     case_
+    -- * Construction
   , ipv4
   , ipv6
-    -- * Construction
   , fromIPv4
   , fromIPv6
     -- * Textual Conversion
@@ -17,10 +43,11 @@
 import Data.Bits
 import Net.IPv6 (IPv6(..))
 import Net.IPv4 (IPv4(..))
-import Text.Read (Read(..),Lexeme(Ident),lexP,parens)
-import Text.ParserCombinators.ReadPrec (prec,step,(+++))
+import Text.Read (Read(..))
+import Text.ParserCombinators.ReadPrec ((+++))
 import Data.Aeson (FromJSON(..),ToJSON(..))
 import Data.Text (Text)
+import Data.Word (Word8,Word16)
 import qualified Net.IPv4 as IPv4
 import qualified Net.IPv6 as IPv6
 import qualified Data.Aeson as Aeson
@@ -30,14 +57,17 @@
   then f (IPv4 (fromIntegral w2))
   else g addr
 
--- | If the address is an 'IPv4' address, return the address.
-ipv4 :: IP -> Maybe IPv4
-ipv4 = case_ Just (const Nothing)
+-- | Construct an 'IP' address from the four octets of
+--   an IPv4 address.
+ipv4 :: Word8 -> Word8 -> Word8 -> Word8 -> IP
+ipv4 a b c d = fromIPv4 (IPv4.fromOctets a b c d)
 
--- | If the address is an 'IPv6' address, and if it is not
---   an IPv4-mapped IPv6 address, return the address.
-ipv6 :: IP -> Maybe IPv6
-ipv6 = case_ (const Nothing) Just
+-- | Construct an 'IP' address from the eight 16-bit
+--   chunks of an IPv6 address.
+ipv6 :: Word16 -> Word16 -> Word16 -> Word16
+     -> Word16 -> Word16 -> Word16 -> Word16
+     -> IP
+ipv6 a b c d e f g h = fromIPv6 (IPv6.fromWord16s a b c d e f g h)
 
 fromIPv4 :: IPv4 -> IP
 fromIPv4 (IPv4 w) = IP (IPv6 0 (0x0000FFFF00000000 .|. fromIntegral w))
@@ -64,20 +94,10 @@
   deriving (Eq,Ord)
 
 instance Show IP where
-  showsPrec p = case_
-    (\x -> showParen (p > 10) $ showString "fromIPv4 " . showsPrec 11 x)
-    (\x -> showParen (p > 10) $ showString "fromIPv6 " . showsPrec 11 x)
+  showsPrec p = case_ (showsPrec p) (showsPrec p)
 
 instance Read IP where
-  readPrec = parens $
-    ( prec 10 $ do
-        Ident "fromIPv4 " <- lexP
-        fmap fromIPv4 (step readPrec)
-    ) +++
-    ( prec 10 $ do
-        Ident "fromIPv6 " <- lexP
-        fmap fromIPv6 (step readPrec)
-    )
+  readPrec = fmap fromIPv4 readPrec +++ fmap fromIPv6 readPrec
 
 instance ToJSON IP where
   toJSON = Aeson.String . encode
diff --git a/src/Net/IPv4.hs b/src/Net/IPv4.hs
--- a/src/Net/IPv4.hs
+++ b/src/Net/IPv4.hs
@@ -23,7 +23,8 @@
 
 module Net.IPv4
   ( -- * Conversion Functions
-    fromOctets
+    ipv4
+  , fromOctets
   , toOctets
     -- * Special IP Addresses
   , any
@@ -40,7 +41,6 @@
   , builder
   , reader
   , parser
-  , print
     -- ** UTF-8 ByteString
   , encodeUtf8
   , decodeUtf8
@@ -54,7 +54,7 @@
   , IPv4(..)
   ) where
 
-import Prelude hiding (any,print)
+import Prelude hiding (any)
 import Data.Bits ((.&.),(.|.),shiftR,shiftL,unsafeShiftR)
 import Data.Word
 import Data.Hashable
@@ -77,7 +77,6 @@
 import Text.ParserCombinators.ReadPrec (prec,step)
 import qualified Data.Text.Read as TextRead
 import qualified Data.ByteString.Builder as Builder
-import qualified Data.Text.IO as Text
 import qualified Data.ByteString.Unsafe as ByteString
 import qualified Data.Text.Read as Text (Reader)
 import qualified Data.Text.Lazy.Builder as TBuilder
@@ -107,6 +106,7 @@
 --
 -- >>> import Test.QuickCheck (Arbitrary(..))
 -- >>> import qualified Prelude as P
+-- >>> import qualified Data.Text.IO as T
 -- >>> instance Arbitrary IPv4 where { arbitrary = fmap IPv4 arbitrary }
 --
 
@@ -117,12 +117,16 @@
 --   Additionally, it is used for the 'Show' and 'Read' instances
 --   of 'IPv4' to help keep things readable in GHCi.
 --
---   >>> let ip = fromOctets 192 168 1 1
---   >>> ip
---   fromOctets 192 168 1 1
---   >>> getIPv4 ip
+--   >>> let addr = ipv4 192 168 1 1
+--   >>> addr
+--   ipv4 192 168 1 1
+--   >>> getIPv4 addr
 --   3232235777
 --
+ipv4 :: Word8 -> Word8 -> Word8 -> Word8 -> IPv4
+ipv4 = fromOctets
+
+-- | An alias for the 'ipv4' smart constructor.
 fromOctets :: Word8 -> Word8 -> Word8 -> Word8 -> IPv4
 fromOctets a b c d = fromOctets'
   (fromIntegral a) (fromIntegral b) (fromIntegral c) (fromIntegral d)
@@ -211,12 +215,18 @@
 public :: IPv4 -> Bool
 public = not . reserved
 
+-- | Encode an 'IPv4' address to 'Text' using dot-decimal notation:
+--
+--   >>> T.putStrLn (encode (ipv4 192 168 2 47))
+--   192.168.2.47
 encode :: IPv4 -> Text
 encode = toDotDecimalText
 
+-- | Decode an 'IPv4' address.
 decode :: Text -> Maybe IPv4
 decode = decodeIPv4TextMaybe
 
+-- | Encode an 'IPv4' address to a text 'TBuilder.Builder'.
 builder :: IPv4 -> TBuilder.Builder
 builder = toDotDecimalBuilder
 
@@ -226,10 +236,7 @@
 parser :: AT.Parser IPv4
 parser = dotDecimalParser
 
--- | This exists mostly for testing purposes.
-print :: IPv4 -> IO ()
-print = Text.putStrLn . encode
-
+-- | Encode an 'IPv4' address to a UTF-8 encoded 'ByteString'.
 encodeUtf8 :: IPv4 -> ByteString
 encodeUtf8 = toBSPreAllocated
 
@@ -292,7 +299,7 @@
   where
   limitSize i =
     if i > 255
-      then fail "All octets in an ip address must be between 0 and 255"
+      then fail "All octets in an ipv4 address must be between 0 and 255"
       else return i
 
 {- $string
@@ -318,7 +325,7 @@
 
 instance Show IPv4 where
   showsPrec p addr = showParen (p > 10)
-    $ showString "fromOctets "
+    $ showString "ipv4 "
     . showsPrec 11 a
     . showChar ' '
     . showsPrec 11 b
@@ -331,7 +338,7 @@
 
 instance Read IPv4 where
   readPrec = parens $ prec 10 $ do
-    Ident "fromOctets" <- lexP
+    Ident "ipv4" <- lexP
     a <- step readPrec
     b <- step readPrec
     c <- step readPrec
@@ -395,7 +402,7 @@
 instance ToJSONKey IPv4 where
   toJSONKey = ToJSONKeyText
     encode
-    (\ip -> Aeson.unsafeToEncoding $ BB.char7 '"' <> builderUtf8 ip <> BB.char7 '"')
+    (\addr -> Aeson.unsafeToEncoding $ BB.char7 '"' <> builderUtf8 addr <> BB.char7 '"')
 
 #if MIN_VERSION_aeson(1,0,0) 
 instance FromJSONKey IPv4 where
@@ -405,7 +412,7 @@
 aesonParser :: Text -> Aeson.Parser IPv4
 aesonParser t = case decode t of
   Nothing -> fail "Could not parse IPv4 address"
-  Just ip -> return ip
+  Just addr -> return addr
 
 
 ------------------------------------
diff --git a/src/Net/IPv4/Range.hs b/src/Net/IPv4/Range.hs
--- a/src/Net/IPv4/Range.hs
+++ b/src/Net/IPv4/Range.hs
@@ -59,7 +59,8 @@
 --
 -- >>> import qualified Prelude as P
 -- >>> import qualified Net.IPv4 as I
--- >>> import Net.IPv4 (fromOctets)
+-- >>> import qualified Data.Text.IO as T
+-- >>> import Net.IPv4 (fromOctets,ipv4)
 -- >>> import Test.QuickCheck (Arbitrary(..))
 -- >>> instance Arbitrary IPv4 where { arbitrary = fmap IPv4 arbitrary }
 -- >>> instance Arbitrary IPv4Range where { arbitrary = IPv4Range <$> arbitrary <*> arbitrary }
@@ -108,7 +109,7 @@
 -- | The inclusive lower bound of an 'IPv4Range'. This is conventionally
 --   understood to be the broadcast address of a subnet. For example:
 --
--- >>> I.print $ lowerInclusive $ IPv4Range (fromOctets 10 10 1 160) 25
+-- >>> T.putStrLn $ I.encode $ lowerInclusive $ IPv4Range (ipv4 10 10 1 160) 25
 -- 10.10.1.128
 --
 -- Note that the lower bound of a normalized 'IPv4Range' is simply the
@@ -149,7 +150,7 @@
 -- | Convert an 'IPv4Range' into a list of the 'IPv4' addresses that
 --   are in it.
 -- >>> let r = IPv4Range (fromOctets 192 168 1 8) 30
--- >>> mapM_ I.print (toList r)
+-- >>> mapM_ (T.putStrLn . I.encode) (toList r)
 -- 192.168.1.8
 -- 192.168.1.9
 -- 192.168.1.10
diff --git a/src/Net/IPv6.hs b/src/Net/IPv6.hs
--- a/src/Net/IPv6.hs
+++ b/src/Net/IPv6.hs
@@ -6,6 +6,7 @@
   ( -- * Types
     IPv6(..)
     -- * Convert
+  , ipv6
   , fromOctets
   , fromWord16s
   , toWord16s
@@ -50,7 +51,7 @@
 
 instance Show IPv6 where
   showsPrec p addr = showParen (p > 10)
-    $ showString "fromWord16s "
+    $ showString "ipv6 "
     . showHexWord16 a
     . showChar ' '
     . showHexWord16 b
@@ -66,7 +67,6 @@
     . showHexWord16 g
     . showChar ' '
     . showHexWord16 h
-    . showChar ' '
     where
     (a,b,c,d,e,f,g,h) = toWord16s addr
 
@@ -86,7 +86,7 @@
 
 instance Read IPv6 where
   readPrec = parens $ prec 10 $ do
-    Ident "fromWord16s" <- lexP
+    Ident "ipv6" <- lexP
     a <- step readPrec
     b <- step readPrec
     c <- step readPrec
@@ -128,11 +128,18 @@
 --   feature for suppress zeroes in an 'IPv6' address, but it should be
 --   readable enough for hacking in GHCi.
 --
---   >>> let ip = fromWord16s 0x3124 0x0 0x0 0xDEAD 0xCAFE 0xFF 0xFE00 0x1
---   >>> ip
---   fromWord16s 0x3124 0x0000 0x0000 0xdead 0xcafe 0x00ff 0xfe00 0x0001
---   >>> T.putStrLn (encode ip)
+--   >>> let addr = ipv6 0x3124 0x0 0x0 0xDEAD 0xCAFE 0xFF 0xFE00 0x1
+--   >>> addr
+--   ipv6 0x3124 0x0000 0x0000 0xdead 0xcafe 0x00ff 0xfe00 0x0001
+--   >>> T.putStrLn (encode addr)
 --   3124::dead:cafe:ff:fe00:1
+ipv6 :: 
+     Word16 -> Word16 -> Word16 -> Word16
+  -> Word16 -> Word16 -> Word16 -> Word16
+  -> IPv6
+ipv6 = fromWord16s
+
+-- | An alias for the 'ipv6' smart constructor.
 fromWord16s ::
      Word16 -> Word16 -> Word16 -> Word16
   -> Word16 -> Word16 -> Word16 -> Word16
diff --git a/src/Net/Mac.hs b/src/Net/Mac.hs
--- a/src/Net/Mac.hs
+++ b/src/Net/Mac.hs
@@ -1,4 +1,5 @@
 {-# LANGUAGE CPP #-}
+{-# LANGUAGE BangPatterns #-}
 {-# LANGUAGE TypeFamilies #-}
 {-# LANGUAGE MultiParamTypeClasses #-}
 {-# LANGUAGE InstanceSigs #-}
@@ -11,7 +12,8 @@
 {-# OPTIONS_GHC -Wall #-}
 module Net.Mac
   ( -- * Convert
-    fromOctets
+    mac
+  , fromOctets
   , toOctets
     -- * Textual Conversion
     -- ** Text
@@ -19,8 +21,6 @@
   , encodeWith
   , decode
   , decodeWith
-  , decodeEither
-  , decodeEitherWith
   , builder
   , parser
   , parserWith
@@ -32,7 +32,8 @@
   , builderUtf8
   , parserUtf8
   , parserWithUtf8
-  , parserLenientUtf8
+    -- ** ByteString
+  , decodeBytes
     -- * Types
   , Mac(..)
   , MacCodec(..)
@@ -40,7 +41,7 @@
   ) where
 
 import Data.Word
-import Data.Bits ((.|.),unsafeShiftL,unsafeShiftR)
+import Data.Bits ((.|.),unsafeShiftL,unsafeShiftR,(.&.))
 import Data.Text (Text)
 import Data.Word (Word8)
 import Data.Word.Synthetic.Word12 (Word12)
@@ -49,7 +50,11 @@
 import Data.Aeson (FromJSON(..),ToJSON(..))
 import Data.Hashable (Hashable)
 import GHC.Generics (Generic)
-import Data.Char (ord)
+import Data.Char (ord,chr)
+import Text.Read (Read(..),Lexeme(Ident),lexP,parens)
+import Text.ParserCombinators.ReadPrec (prec,step)
+import qualified Data.ByteString as B
+import qualified Data.ByteString.Unsafe as BU
 import qualified Data.ByteString.Builder as BB
 import qualified Data.Attoparsec.ByteString as ABW
 import qualified Data.Attoparsec.Text as AT
@@ -65,12 +70,31 @@
   ToJSONKeyFunction(..),FromJSONKeyFunction(..))
 #endif
 
+-- $setup
+--
+-- These are here to get doctest's property checking to work
+--
+-- >>> :set -XOverloadedStrings
+-- >>> import Test.QuickCheck (Arbitrary(..))
+-- >>> import qualified Data.Text.IO as T
+-- >>> import qualified Data.ByteString.Char8 as BC
+-- >>> instance Arbitrary Mac where { arbitrary = fmap (Mac . (0xFFFFFFFFFFFF .&.)) arbitrary }
+
+-- | Construct a 'Mac' address from a 'Word64'. Only the lower
+--   48 bits are used.
+mac :: Word64 -> Mac
+mac w = Mac (w .&. 0xFFFFFFFFFFFF)
+
+-- | Create a 'Mac' address from six octets.
 fromOctets :: Word8 -> Word8 -> Word8 -> Word8 -> Word8 -> Word8 -> Mac
 fromOctets a b c d e f = Mac $ unsafeWord48FromOctets
   (fromIntegral a) (fromIntegral b) (fromIntegral c)
   (fromIntegral d) (fromIntegral e) (fromIntegral f)
-{-# INLINE fromOctets #-}
 
+-- | Convert a 'Mac' address to the six octets that make it up.
+--   This function and 'fromOctets' are inverses:
+--
+--   prop> m == (let (a,b,c,d,e,f) = toOctets m in fromOctets a b c d e f)
 toOctets :: Mac -> (Word8,Word8,Word8,Word8,Word8,Word8)
 toOctets (Mac w) =
   ( fromIntegral $ unsafeShiftR w 40
@@ -81,14 +105,41 @@
   , fromIntegral w
   )
 
+-- | Decode a 'Mac' address from a 'ByteString'. Each byte is interpreted
+--   as an octet of the 'Mac' address. Consequently, 'ByteString's
+--   of length 6 successfully decode, and all other 'ByteString's fail
+--   to decode.
+--
+--   >>> decodeBytes (B.pack [0x6B,0x47,0x18,0x90,0x55,0xC3])
+--   Just (mac 0x6b47189055c3)
+--   >>> decodeBytes (B.replicate 6 0x3A)
+--   Just (mac 0x3a3a3a3a3a3a)
+--   >>> decodeBytes (B.replicate 7 0x3A)
+--   Nothing
+decodeBytes :: ByteString -> Maybe Mac
+decodeBytes bs = if B.length bs == 6
+  then Just $ fromOctets
+    (BU.unsafeIndex bs 0)
+    (BU.unsafeIndex bs 1)
+    (BU.unsafeIndex bs 2)
+    (BU.unsafeIndex bs 3)
+    (BU.unsafeIndex bs 4)
+    (BU.unsafeIndex bs 5)
+  else Nothing
+
 rightToMaybe :: Either a b -> Maybe b
 rightToMaybe = either (const Nothing) Just
 
 c2w :: Char -> Word8
 c2w = fromIntegral . ord
 
+-- | Encode a 'Mac' address lowercase hex, separating every two characters
+--   with a colon:
+--
+--   >>> T.putStrLn (encode (Mac 0xA47F247AB423))
+--   a4:7f:24:7a:b4:23
 encode :: Mac -> Text
-encode = encodeWith defCodec -- Internal.macToTextDefault w
+encode = encodeWith defCodec
 
 encodeWith :: MacCodec -> Mac -> Text
 encodeWith (MacCodec g u) m = case g of
@@ -106,12 +157,6 @@
     True -> TFB.run (fixedBuilderQuadruples TFB.word8HexFixedUpper) (Pair c m)
     False -> TFB.run (fixedBuilderQuadruples TFB.word8HexFixedLower) (Pair c m)
 
-decodeEitherWith :: MacCodec -> Text -> Either String Mac
-decodeEitherWith codec t = AT.parseOnly (parserWith codec <* AT.endOfInput) t
-
-decodeEither :: Text -> Either String Mac
-decodeEither = decodeEitherWith defCodec
-
 decode :: Text -> Maybe Mac
 decode = decodeWith defCodec
 
@@ -264,11 +309,23 @@
 word12At i (Mac w) = fromIntegral (unsafeShiftR w i)
 {-# INLINE word12At #-}
 
+-- | Encode a 'Mac' address, as lowercase hexadecimal digits
+--   separated by a colon:
+--
+--   >>> BC.putStrLn (encodeUtf8 (mac 0x64255A0F2C47))
+--   64:25:5a:0f:2c:47
 encodeUtf8 :: Mac -> ByteString
 encodeUtf8 = encodeWithUtf8 defCodec
 
 -- | Lenient decoding of MAC address that accepts lowercase, uppercase,
 --   and any kind separator.
+--
+--   >>> decodeUtf8 "A2:DE:AD:BE:EF:67"
+--   Just (mac 0xa2deadbeef67)
+--   >>> decodeUtf8 "13-a2-fe-a4-17-96"
+--   Just (mac 0x13a2fea41796)
+--   >>> decodeUtf8 "0A42.47BA.67C2"
+--   Just (mac 0x0a4247ba67c2)
 decodeUtf8 :: ByteString -> Maybe Mac
 decodeUtf8 = decodeLenientUtf8
 
@@ -283,10 +340,11 @@
 builderUtf8 :: Mac -> BB.Builder
 builderUtf8 = BB.byteString . encodeUtf8
 
--- | Parser for a 'Mac' address using with a colon as the
---   separator (i.e. @FA:43:B2:C0:0F:99@).
+-- | Lenient parser for a 'Mac' address using any character
+--   as the separator and accepting any digit grouping
+--   (i.e. @FA:43:B2:C0:0F:99@ or @A065.647B.87FA@).
 parserUtf8 :: AB.Parser Mac
-parserUtf8 = parserWithUtf8 defCodec
+parserUtf8 = parserLenientUtf8
 
 -- | Parser for a 'Mac' address using the provided settings.
 parserWithUtf8 :: MacCodec -> AB.Parser Mac
@@ -479,9 +537,38 @@
 -- | A 48-bit MAC address. Do not use the data constructor for this
 --   type. It is not considered part of the stable API, and it
 --   allows you to construct invalid MAC addresses.
-newtype Mac = Mac { getMac :: Word64 }
-  deriving (Eq,Ord,Show,Read,Generic)
+newtype Mac = Mac Word64
+  deriving (Eq,Ord,Generic)
 
+-- What this instance does is to display the
+-- inner contents in hexadecimal and pad them out to
+-- 6 bytes in case it begins with several zeroes.
+-- It also uses the smart constructor instead
+-- of the actual constructor
+instance Show Mac where
+  showsPrec p (Mac addr) = showParen (p > 10)
+    $ showString "mac "
+    . showHexWord48 addr
+
+instance Read Mac where
+  readPrec = parens $ prec 10 $ do
+    Ident "mac" <- lexP
+    w <- step readPrec
+    return (mac w)
+
+showHexWord48 :: Word64 -> ShowS
+showHexWord48 w = showString "0x" . go 11
+  where
+  go :: Int -> ShowS
+  go !ix = if ix >= 0
+    then showChar (nibbleToHex ((unsafeShiftR (fromIntegral w) (ix * 4)) .&. 0xF)) . go (ix - 1)
+    else id
+
+nibbleToHex :: Word -> Char
+nibbleToHex w
+  | w < 10 = chr (fromIntegral (w + 48))
+  | otherwise = chr (fromIntegral (w + 87))
+
 data MacCodec = MacCodec
   { macCodecGrouping :: !MacGrouping
   , macCodecUpperCase :: !Bool
@@ -512,7 +599,7 @@
 instance FromJSONKey Mac where
   fromJSONKey = FromJSONKeyTextParser $ \t -> case decode t of
     Nothing -> fail "invalid mac address"
-    Just mac -> return mac
+    Just addr -> return addr
 #endif
 
 instance FromJSON Mac where
diff --git a/test/Doctests.hs b/test/Doctests.hs
--- a/test/Doctests.hs
+++ b/test/Doctests.hs
@@ -4,5 +4,11 @@
 main = doctest
   [ "src/Net/IPv4.hs"
   , "src/Net/IPv6.hs"
+  , "src/Net/IP.hs"
   , "src/Net/IPv4/Range.hs"
+  , "src/Data/Word/Synthetic/Word12.hs"
+  , "src/Data/Text/Builder/Common/Internal.hs"
+  , "src/Data/Text/Builder/Fixed.hs"
+  , "src/Data/ByteString/Builder/Fixed.hs"
+  , "src/Net/Mac.hs"
   ]
diff --git a/test/Naive.hs b/test/Naive.hs
--- a/test/Naive.hs
+++ b/test/Naive.hs
@@ -13,12 +13,9 @@
 import Data.ByteString (ByteString)
 import Control.Monad.ST
 import Data.Text.Encoding (encodeUtf8)
-import qualified Data.ByteString.Char8  as BC8
-import qualified Data.ByteString        as ByteString
-import qualified Data.ByteString.Unsafe as ByteString
-import qualified Data.Text.Lazy         as LText
-import qualified Data.Text.Lazy.Builder as TBuilder
-import qualified Data.Text.Array        as TArray
+import qualified Data.ByteString.Char8 as BC8
+import qualified Data.ByteString as ByteString
+import qualified Data.Text.Lazy as LText
 
 encodeByteString :: IPv4 -> ByteString
 encodeByteString = encodeUtf8 . encodeText
diff --git a/test/Test.hs b/test/Test.hs
--- a/test/Test.hs
+++ b/test/Test.hs
@@ -6,22 +6,25 @@
 module Main (main) where
 
 import Naive
+import Data.Proxy (Proxy(..))
 import Test.Framework (defaultMain, testGroup, Test)
 import Test.Framework.Providers.QuickCheck2 (testProperty)
-import Test.QuickCheck (Arbitrary(..),oneof,Gen,elements)
+import Test.QuickCheck (Arbitrary(..),Property,oneof,Gen,elements)
 import Test.HUnit (Assertion,(@?=))
 import Numeric (showHex)
 import Test.QuickCheck.Property (failed,succeeded,Result(..))
 import Data.Bifunctor
+import Test.QuickCheck.Classes (jsonProps,showReadProps)
 import qualified Test.Framework.Providers.HUnit as PH
 
-import Net.Types (IPv4(..),IPv4Range(..),Mac(..),IPv6(..),MacGrouping(..),MacCodec(..))
+import Net.Types (IP,IPv4(..),IPv4Range(..),Mac(..),IPv6(..),MacGrouping(..),MacCodec(..))
 import qualified Data.Text as Text
 import qualified Data.ByteString.Char8 as BC8
 import qualified Net.IPv4 as IPv4
 import qualified Net.IPv6 as IPv6
 import qualified Net.IPv4.Range as IPv4Range
 import qualified Net.Mac as Mac
+import qualified Net.IP as IP
 
 import qualified Data.Attoparsec.Text as AT
 import qualified Data.Attoparsec.ByteString as AB
@@ -92,8 +95,29 @@
     , testProperty "Membership agrees with bounds" propMemberUpperLower
     , testProperty "Range contains self" propRangeSelf
     ]
+  , testGroup "Instances"
+    [ testGroup "IPv4"
+      [ namedPropertiesToTest "JSON" (jsonProps (Proxy :: Proxy IPv4))
+      , namedPropertiesToTest "Show Read" (showReadProps (Proxy :: Proxy IPv4))
+      ]
+    , testGroup "IPv6"
+      [ namedPropertiesToTest "JSON" (jsonProps (Proxy :: Proxy IPv6))
+      , namedPropertiesToTest "Show Read" (showReadProps (Proxy :: Proxy IPv6))
+      ]
+    , testGroup "IP"
+      [ namedPropertiesToTest "JSON" (jsonProps (Proxy :: Proxy IP))
+      , namedPropertiesToTest "Show Read" (showReadProps (Proxy :: Proxy IP))
+      ]
+    , testGroup "Mac"
+      [ namedPropertiesToTest "JSON" (jsonProps (Proxy :: Proxy Mac))
+      , namedPropertiesToTest "Show Read" (showReadProps (Proxy :: Proxy Mac))
+      ]
+    ]
   ]
 
+namedPropertiesToTest :: String -> [(String,Property)] -> Test
+namedPropertiesToTest name pairs = testGroup name (map (uncurry testProperty) pairs)
+
 propEncodeDecodeIso :: Eq a => (a -> b) -> (b -> Maybe a) -> a -> Bool
 propEncodeDecodeIso f g a = g (f a) == Just a
 
@@ -291,6 +315,18 @@
 
 
 deriving instance Arbitrary IPv4
+
+instance Arbitrary IPv6 where
+  arbitrary = IPv6 <$> arbitrary <*> arbitrary
+
+-- Half of the test cases generated are IPv6 mapped
+-- IPv4 addresses.
+instance Arbitrary IP where
+  arbitrary = oneof
+    [ IP.fromIPv4 <$> arbitrary
+    , IP.fromIPv6 <$> arbitrary
+    ]
+
 instance Arbitrary Mac where
   arbitrary = Mac.fromOctets
     <$> arbitrary
