diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
 # Revision history for byteslice
 
+## 0.2.6.0 -- 2021-09-15
+
+* Add `BytesN` and `ByteArrayN`.
+* Add `isInfixOf`.
+* Add `hForLines_` and `hFoldLines`.
+* Add `lift` and `unlift` for converting between `Bytes` and `Bytes#`.
+* Move text-oriented functions from Data.Bytes to `Data.Bytes.Text.*`.
+  Provide aliases with older names that come with deprecation warning.
+
 ## 0.2.5.2 -- 2021-02-23
 
 * Correct compatibility shims.
diff --git a/bench/Main.hs b/bench/Main.hs
--- a/bench/Main.hs
+++ b/bench/Main.hs
@@ -2,7 +2,7 @@
 
 import Data.Bytes.Types
 import Data.List (permutations)
-import qualified Data.Bytes as Bytes
+import qualified Data.Bytes.Text.Ascii as Ascii
 
 naiveMconcat :: [Bytes] -> Bytes
 naiveMconcat = foldr mappend mempty
@@ -14,4 +14,4 @@
   ]
 
 mconcatBytes :: [Bytes]
-mconcatBytes = fmap Bytes.fromAsciiString $ permutations ['a'..'g']
+mconcatBytes = fmap Ascii.fromString $ permutations ['a'..'g']
diff --git a/byteslice.cabal b/byteslice.cabal
--- a/byteslice.cabal
+++ b/byteslice.cabal
@@ -1,6 +1,6 @@
 cabal-version: 2.4
 name: byteslice
-version: 0.2.5.2
+version: 0.2.6.0
 synopsis: Slicing managed and unmanaged memory
 description:
   This library provides types that allow the user to talk about a slice of
@@ -25,7 +25,13 @@
   exposed-modules:
     Data.Bytes
     Data.Bytes.Chunks
+    Data.Bytes.Internal
     Data.Bytes.Mutable
+    Data.Bytes.Text.Ascii
+    Data.Bytes.Text.AsciiExt
+    Data.Bytes.Text.Latin1
+    Data.Bytes.Text.Utf8
+    Data.Bytes.Text.Windows1252
     Data.Bytes.Types
   other-modules:
     Data.Bytes.Byte
diff --git a/src-no-unlifted-newtypes/UnliftedBytes.hs b/src-no-unlifted-newtypes/UnliftedBytes.hs
--- a/src-no-unlifted-newtypes/UnliftedBytes.hs
+++ b/src-no-unlifted-newtypes/UnliftedBytes.hs
@@ -5,10 +5,19 @@
 
 module UnliftedBytes
   ( Bytes#
+  , lift
+  , unlift
   ) where
 
+import Data.Bytes.Internal (Bytes)
 import GHC.TypeLits
 import GHC.Exts (RuntimeRep(..),TYPE)
 
 type family Bytes# :: TYPE ('TupleRep '[ 'UnliftedRep,'IntRep,'IntRep]) where
   Bytes# = TypeError ('Text "Bytes# not available before GHC 8.10")
+
+lift :: Bytes# -> Bytes
+lift _ = errorWithoutStackTrace "UnliftedBytes: lift not implemented"
+
+unlift :: Bytes -> Bytes#
+unlift _ = errorWithoutStackTrace "UnliftedBytes: unlift not implemented"
diff --git a/src-unlifted-newtypes/UnliftedBytes.hs b/src-unlifted-newtypes/UnliftedBytes.hs
--- a/src-unlifted-newtypes/UnliftedBytes.hs
+++ b/src-unlifted-newtypes/UnliftedBytes.hs
@@ -7,10 +7,20 @@
 
 module UnliftedBytes
   ( Bytes#(..)
+  , lift
+  , unlift
   ) where
 
-import GHC.Exts (ByteArray#,Int#,RuntimeRep(..),TYPE)
+import Data.Bytes.Internal (Bytes(Bytes))
+import Data.Primitive (ByteArray(ByteArray))
+import GHC.Exts (Int(I#),ByteArray#,Int#,RuntimeRep(..),TYPE)
 
 newtype Bytes# :: TYPE ('TupleRep '[ 'UnliftedRep,'IntRep,'IntRep]) where
   Bytes# :: (# ByteArray#, Int#, Int# #) -> Bytes#
 
+lift :: Bytes# -> Bytes
+lift (Bytes# (# arr, off, len #)) = Bytes (ByteArray arr) (I# off) (I# len)
+
+unlift :: Bytes -> Bytes#
+unlift (Bytes (ByteArray arr) (I# off) (I# len)) =
+  Bytes# (# arr, off, len #)
diff --git a/src/Data/Bytes.hs b/src/Data/Bytes.hs
--- a/src/Data/Bytes.hs
+++ b/src/Data/Bytes.hs
@@ -4,9 +4,17 @@
 {-# language MagicHash #-}
 {-# language NamedFieldPuns #-}
 {-# language RankNTypes #-}
+{-# language TupleSections #-}
 {-# language TypeApplications #-}
 {-# language UnboxedTuples #-}
 
+-- | If you are interested in sub-arrays of 'ByteArray's (e.g. writing a binary
+-- search), it would be grossly inefficient to make a copy of the sub-array. On
+-- the other hand, it'd be really annoying to track limit indices by hand.
+--
+-- This module defines the 'Bytes' type which exposes a standard array interface
+-- for a sub-arrays without copying and without manual index manipulation. --
+-- For mutable arrays, see 'Data.Bytes.Mutable'.
 module Data.Bytes
   ( -- * Types
     Bytes
@@ -40,12 +48,12 @@
   , takeWhileEnd
   , dropWhileEnd
     -- * Folds
-  , foldl
+  , Pure.foldl
   , Pure.foldl'
-  , foldr
-  , foldr'
+  , Pure.foldr
+  , Pure.foldr'
     -- * Folds with Indices
-  , ifoldl'
+  , Pure.ifoldl'
     -- * Common Folds
   , elem
     -- * Splitting
@@ -72,6 +80,7 @@
     -- ** Byte Sequence
   , isPrefixOf
   , isSuffixOf
+  , isInfixOf
   , stripPrefix
   , stripOptionalPrefix
   , stripSuffix
@@ -123,7 +132,7 @@
   , fromCString#
   , Pure.toByteString
   , Pure.pinnedToByteString
-  , fromByteString
+  , Pure.fromByteString
   , fromShortByteString
   , toShortByteString
   , toShortByteStringClone
@@ -132,33 +141,36 @@
   , BIO.hGet
   , readFile
   , BIO.hPut
+    -- * Unlifted Types
+  , lift
+  , unlift
   ) where
 
 import Prelude hiding (length,takeWhile,dropWhile,null,foldl,foldr,elem,replicate,any,all,readFile)
 
 import Control.Monad.Primitive (PrimMonad,primitive_,unsafeIOToPrim)
-import Control.Monad.ST (ST)
 import Control.Monad.ST.Run (runByteArrayST)
-import Data.ByteString (ByteString)
-import Data.ByteString.Short.Internal (ShortByteString(SBS))
-import Data.Bytes.Pure (length,fromByteArray)
+import Cstrlen (cstringLength#)
+import Data.Bits((.&.),(.|.),shiftL,finiteBitSize)
+import Data.Bytes.Pure (length,fromByteArray,foldr)
 import Data.Bytes.Types (Bytes(Bytes,array,offset))
-import Data.Char (ord)
+import Data.ByteString.Short.Internal (ShortByteString(SBS))
+import Data.Maybe (fromMaybe)
 import Data.Primitive (ByteArray(ByteArray))
 import Foreign.C.String (CString)
 import Foreign.Ptr (Ptr,plusPtr,castPtr)
-import GHC.Exts (Int(I#),Char(C#),Ptr(Ptr),word2Int#,chr#)
 import GHC.Exts (Addr#,Word#,Int#)
-import GHC.IO (unsafeIOToST)
-import GHC.Word (Word8(W8#))
-import Cstrlen (cstringLength#)
+import GHC.Exts (Int(I#),Ptr(Ptr))
+import GHC.Word (Word8(W8#),Word32)
+import UnliftedBytes (lift,unlift)
 
-import qualified Data.ByteString as ByteString
-import qualified Data.ByteString.Unsafe as ByteString
 import qualified Data.Bytes.Byte as Byte
 import qualified Data.Bytes.Chunks as Chunks
 import qualified Data.Bytes.IO as BIO
 import qualified Data.Bytes.Pure as Pure
+import qualified Data.Bytes.Text.Ascii as Ascii
+import qualified Data.Bytes.Text.AsciiExt as AsciiExt
+import qualified Data.Bytes.Text.Latin1 as Latin1
 import qualified Data.Bytes.Types as Types
 import qualified Data.Foldable as F
 import qualified Data.List as List
@@ -224,6 +236,67 @@
     then compareByteArrays a aOff b (bOff + bLen - aLen) aLen == EQ
     else False
 
+-- | Is the first argument an infix of the second argument?
+-- 
+-- Uses the Rabin-Karp algorithm: expected time @O(n+m)@, worst-case @O(nm)@.
+isInfixOf :: Bytes -- ^ String to search for
+          -> Bytes -- ^ String to search in
+          -> Bool
+isInfixOf p s = null p || (not . null) (snd $ breakSubstring p s)
+
+breakSubstring :: Bytes -- ^ String to search for
+               -> Bytes -- ^ String to search in
+               -> (Bytes,Bytes) -- ^ Head and tail of string broken at substring
+breakSubstring pat =
+  case lp of
+    0 -> (mempty,)
+    1 -> breakByte (unsafeHead pat)
+    _ -> if lp * 8 <= finiteBitSize (0 :: Word)
+             then shift
+             else karpRabin
+  where
+  unsafeSplitAt i s = (unsafeTake i s, unsafeDrop i s)
+  lp                = length pat
+  {-# INLINE breakByte #-}
+  breakByte b bytes = fromMaybe (mempty,bytes) $ Byte.split1 b bytes
+  {-# INLINE karpRabin #-}
+  karpRabin :: Bytes -> (Bytes, Bytes)
+  karpRabin src
+      | length src < lp = (src,mempty)
+      | otherwise = search (rollingHash $ unsafeTake lp src) lp
+    where
+    k           = 2891336453 :: Word32
+    rollingHash = Pure.foldl' (\h b -> h * k + fromIntegral b) 0
+    hp          = rollingHash pat
+    m           = k ^ lp
+    get = fromIntegral . unsafeIndex src
+    search !hs !i
+        | hp == hs && pat == unsafeTake lp b = u
+        | length src <= i                    = (src,mempty) -- not found
+        | otherwise                          = search hs' (i + 1)
+      where
+      u@(_, b) = unsafeSplitAt (i - lp) src
+      hs' = hs * k +
+            get i -
+            m * get (i - lp)
+  {-# INLINE shift #-}
+  shift :: Bytes -> (Bytes, Bytes)
+  shift !src
+      | length src < lp = (src,mempty)
+      | otherwise       = search (intoWord $ unsafeTake lp src) lp
+    where
+    intoWord :: Bytes -> Word
+    intoWord = Pure.foldl' (\w b -> (w `shiftL` 8) .|. fromIntegral b) 0
+    wp   = intoWord pat
+    mask = (1 `shiftL` (8 * lp)) - 1
+    search !w !i
+        | w == wp         = unsafeSplitAt (i - lp) src
+        | length src <= i = (src, mempty)
+        | otherwise       = search w' (i + 1)
+      where
+      b  = fromIntegral (unsafeIndex src i)
+      w' = mask .&. ((w `shiftL` 8) .|. b)
+
 -- | Find the longest string which is a prefix of both arguments.
 longestCommonPrefix :: Bytes -> Bytes -> Bytes
 longestCommonPrefix a b = loop 0
@@ -349,6 +422,11 @@
 {-# inline unsafeIndex #-}
 unsafeIndex (Bytes arr off _) ix = PM.indexByteArray arr (off + ix)
 
+-- | Access the first byte. The given 'Bytes' must be non-empty.
+{-# inline unsafeHead #-}
+unsafeHead :: Bytes -> Word8
+unsafeHead bs = unsafeIndex bs 0
+
 -- | /O(n)/ 'dropWhileEnd' @p@ @b@ returns the prefix remaining after
 -- dropping characters that satisfy the predicate @p@ from the end of
 -- @t@.
@@ -400,61 +478,27 @@
       else n
     else n
 
--- | Left fold over bytes, non-strict in the accumulator.
-foldl :: (a -> Word8 -> a) -> a -> Bytes -> a
-{-# inline foldl #-}
-foldl f a0 (Bytes arr off0 len0) =
-  go (off0 + len0 - 1) (len0 - 1) 
-  where
-  go !off !ix = case ix of
-    (-1) -> a0
-    _ -> f (go (off - 1) (ix - 1)) (PM.indexByteArray arr off)
-
--- | Right fold over bytes, non-strict in the accumulator.
-foldr :: (Word8 -> a -> a) -> a -> Bytes -> a
-{-# inline foldr #-}
-foldr f a0 (Bytes arr off0 len0) = go off0 len0 where
-  go !off !len = case len of
-    0 -> a0
-    _ -> f (PM.indexByteArray arr off) (go (off + 1) (len - 1))
-
--- | Left fold over bytes, strict in the accumulator. The reduction function
--- is applied to each element along with its index.
-ifoldl' :: (a -> Int -> Word8 -> a) -> a -> Bytes -> a
-{-# inline ifoldl' #-}
-ifoldl' f a0 (Bytes arr off0 len0) = go a0 0 off0 len0 where
-  go !a !ix !off !len = case len of
-    0 -> a
-    _ -> go (f a ix (PM.indexByteArray arr off)) (ix + 1) (off + 1) (len - 1)
-
--- | Right fold over bytes, strict in the accumulator.
-foldr' :: (Word8 -> a -> a) -> a -> Bytes -> a
-{-# inline foldr' #-}
-foldr' f a0 (Bytes arr off0 len0) =
-  go a0 (off0 + len0 - 1) (len0 - 1) 
-  where
-  go !a !off !ix = case ix of
-    (-1) -> a
-    _ -> go (f (PM.indexByteArray arr off) a) (off - 1) (ix - 1)
-
 -- | Convert a 'String' consisting of only characters in the ASCII block
 -- to a byte sequence. Any character with a codepoint above @U+007F@ is
 -- replaced by @U+0000@.
 fromAsciiString :: String -> Bytes
-fromAsciiString = fromByteArray
-  . Exts.fromList
-  . map (\c -> let i = ord c in if i < 128 then fromIntegral @Int @Word8 i else 0)
+{-# DEPRECATED fromAsciiString "use Data.Bytes.Ascii.fromString instead" #-}
+{-# INLINE fromAsciiString #-}
+fromAsciiString = Ascii.fromString
 
 -- | Convert a 'String' consisting of only characters representable
 -- by ISO-8859-1. These are encoded with ISO-8859-1. Any character
 -- with a codepoint above @U+00FF@ is replaced by an unspecified byte.
 fromLatinString :: String -> Bytes
-fromLatinString =
-  fromByteArray . Exts.fromList . map (fromIntegral @Int @Word8 . ord)
+{-# DEPRECATED fromLatinString "use Data.Bytes.Latin1.fromString instead" #-}
+{-# INLINE fromLatinString #-}
+fromLatinString = Latin1.fromString
 
 -- | Interpret a byte sequence as text encoded by ISO-8859-1.
 toLatinString :: Bytes -> String
-toLatinString = foldr (\(W8# w) xs -> C# (chr# (word2Int# w)) : xs) []
+{-# DEPRECATED toLatinString "use Data.Bytes.Latin1.toString instead" #-}
+{-# INLINE toLatinString #-}
+toLatinString = Latin1.toString
 
 -- | Copy a primitive string literal into managed memory.
 fromCString# :: Addr# -> Bytes
@@ -476,153 +520,87 @@
 -- | Is the byte sequence, when interpreted as ISO-8859-1-encoded text,
 -- a singleton whose element matches the character?
 equalsLatin1 :: Char -> Bytes -> Bool
-{-# inline equalsLatin1 #-}
-equalsLatin1 !c0 (Bytes arr off len) = case len of
-  1 -> c0 == indexCharArray arr off
-  _ -> False
+{-# DEPRECATED equalsLatin1 "use Data.Bytes.Text.Latin1.equals1 instead" #-}
+{-# INLINE equalsLatin1 #-}
+equalsLatin1 = Latin1.equals1
 
+
 -- | Is the byte sequence, when interpreted as ISO-8859-1-encoded text,
 -- a doubleton whose elements match the characters?
 equalsLatin2 :: Char -> Char -> Bytes -> Bool
-equalsLatin2 !c0 !c1 (Bytes arr off len) = case len of
-  2 -> c0 == indexCharArray arr off &&
-       c1 == indexCharArray arr (off + 1)
-  _ -> False
+{-# DEPRECATED equalsLatin2 "use Data.Bytes.Text.Latin1.equals2 instead" #-}
+{-# INLINE equalsLatin2 #-}
+equalsLatin2 = Latin1.equals2
 
 -- | Is the byte sequence, when interpreted as ISO-8859-1-encoded text,
 -- a tripleton whose elements match the characters?
 equalsLatin3 :: Char -> Char -> Char -> Bytes -> Bool
-equalsLatin3 !c0 !c1 !c2 (Bytes arr off len) = case len of
-  3 -> c0 == indexCharArray arr off &&
-       c1 == indexCharArray arr (off + 1) &&
-       c2 == indexCharArray arr (off + 2)
-  _ -> False
+{-# DEPRECATED equalsLatin3 "use Data.Bytes.Text.Latin1.equals3 instead" #-}
+{-# INLINE equalsLatin3 #-}
+equalsLatin3 = Latin1.equals3
 
 -- | Is the byte sequence, when interpreted as ISO-8859-1-encoded text,
 -- a quadrupleton whose elements match the characters?
 equalsLatin4 :: Char -> Char -> Char -> Char -> Bytes -> Bool
-equalsLatin4 !c0 !c1 !c2 !c3 (Bytes arr off len) = case len of
-  4 -> c0 == indexCharArray arr off &&
-       c1 == indexCharArray arr (off + 1) &&
-       c2 == indexCharArray arr (off + 2) &&
-       c3 == indexCharArray arr (off + 3)
-  _ -> False
+{-# DEPRECATED equalsLatin4 "use Data.Bytes.Text.Latin1.equals4 instead" #-}
+{-# INLINE equalsLatin4 #-}
+equalsLatin4 = Latin1.equals4
 
 -- | Is the byte sequence, when interpreted as ISO-8859-1-encoded text,
 -- a quintupleton whose elements match the characters?
 equalsLatin5 :: Char -> Char -> Char -> Char -> Char -> Bytes -> Bool
-equalsLatin5 !c0 !c1 !c2 !c3 !c4 (Bytes arr off len) = case len of
-  5 -> c0 == indexCharArray arr off &&
-       c1 == indexCharArray arr (off + 1) &&
-       c2 == indexCharArray arr (off + 2) &&
-       c3 == indexCharArray arr (off + 3) &&
-       c4 == indexCharArray arr (off + 4)
-  _ -> False
+{-# DEPRECATED equalsLatin5 "use Data.Bytes.Text.Latin1.equals5 instead" #-}
+{-# INLINE equalsLatin5 #-}
+equalsLatin5 = Latin1.equals5
 
 -- | Is the byte sequence, when interpreted as ISO-8859-1-encoded text,
 -- a sextupleton whose elements match the characters?
 equalsLatin6 :: Char -> Char -> Char -> Char -> Char -> Char -> Bytes -> Bool
-equalsLatin6 !c0 !c1 !c2 !c3 !c4 !c5 (Bytes arr off len) = case len of
-  6 -> c0 == indexCharArray arr off &&
-       c1 == indexCharArray arr (off + 1) &&
-       c2 == indexCharArray arr (off + 2) &&
-       c3 == indexCharArray arr (off + 3) &&
-       c4 == indexCharArray arr (off + 4) &&
-       c5 == indexCharArray arr (off + 5)
-  _ -> False
+{-# DEPRECATED equalsLatin6 "use Data.Bytes.Text.Latin1.equals6 instead" #-}
+{-# INLINE equalsLatin6 #-}
+equalsLatin6 = Latin1.equals6
 
 -- | Is the byte sequence, when interpreted as ISO-8859-1-encoded text,
 -- a septupleton whose elements match the characters?
 equalsLatin7 :: Char -> Char -> Char -> Char -> Char -> Char -> Char -> Bytes -> Bool
-equalsLatin7 !c0 !c1 !c2 !c3 !c4 !c5 !c6 (Bytes arr off len) = case len of
-  7 -> c0 == indexCharArray arr off &&
-       c1 == indexCharArray arr (off + 1) &&
-       c2 == indexCharArray arr (off + 2) &&
-       c3 == indexCharArray arr (off + 3) &&
-       c4 == indexCharArray arr (off + 4) &&
-       c5 == indexCharArray arr (off + 5) &&
-       c6 == indexCharArray arr (off + 6)
-  _ -> False
+{-# DEPRECATED equalsLatin7 "use Data.Bytes.Text.Latin1.equals7 instead" #-}
+{-# INLINE equalsLatin7 #-}
+equalsLatin7 = Latin1.equals7
 
 -- | Is the byte sequence, when interpreted as ISO-8859-1-encoded text,
 -- an octupleton whose elements match the characters?
 equalsLatin8 :: Char -> Char -> Char -> Char -> Char -> Char -> Char -> Char -> Bytes -> Bool
-equalsLatin8 !c0 !c1 !c2 !c3 !c4 !c5 !c6 !c7 (Bytes arr off len) = case len of
-  8 -> c0 == indexCharArray arr off &&
-       c1 == indexCharArray arr (off + 1) &&
-       c2 == indexCharArray arr (off + 2) &&
-       c3 == indexCharArray arr (off + 3) &&
-       c4 == indexCharArray arr (off + 4) &&
-       c5 == indexCharArray arr (off + 5) &&
-       c6 == indexCharArray arr (off + 6) &&
-       c7 == indexCharArray arr (off + 7)
-  _ -> False
+{-# DEPRECATED equalsLatin8 "use Data.Bytes.Text.Latin1.equals8 instead" #-}
+{-# INLINE equalsLatin8 #-}
+equalsLatin8 = Latin1.equals8
 
 -- | Is the byte sequence, when interpreted as ISO-8859-1-encoded text,
 -- a 9-tuple whose elements match the characters?
 equalsLatin9 :: Char -> Char -> Char -> Char -> Char -> Char -> Char -> Char -> Char -> Bytes -> Bool
-equalsLatin9 !c0 !c1 !c2 !c3 !c4 !c5 !c6 !c7 !c8 (Bytes arr off len) = case len of
-  9 -> c0 == indexCharArray arr off &&
-       c1 == indexCharArray arr (off + 1) &&
-       c2 == indexCharArray arr (off + 2) &&
-       c3 == indexCharArray arr (off + 3) &&
-       c4 == indexCharArray arr (off + 4) &&
-       c5 == indexCharArray arr (off + 5) &&
-       c6 == indexCharArray arr (off + 6) &&
-       c7 == indexCharArray arr (off + 7) &&
-       c8 == indexCharArray arr (off + 8)
-  _ -> False
+{-# DEPRECATED equalsLatin9 "use Data.Bytes.Text.Latin1.equals9 instead" #-}
+{-# INLINE equalsLatin9 #-}
+equalsLatin9 = Latin1.equals9
 
 -- | Is the byte sequence, when interpreted as ISO-8859-1-encoded text,
 -- a 10-tuple whose elements match the characters?
 equalsLatin10 :: Char -> Char -> Char -> Char -> Char -> Char -> Char -> Char -> Char -> Char -> Bytes -> Bool
-equalsLatin10 !c0 !c1 !c2 !c3 !c4 !c5 !c6 !c7 !c8 !c9 (Bytes arr off len) = case len of
-  10 -> c0 == indexCharArray arr off &&
-        c1 == indexCharArray arr (off + 1) &&
-        c2 == indexCharArray arr (off + 2) &&
-        c3 == indexCharArray arr (off + 3) &&
-        c4 == indexCharArray arr (off + 4) &&
-        c5 == indexCharArray arr (off + 5) &&
-        c6 == indexCharArray arr (off + 6) &&
-        c7 == indexCharArray arr (off + 7) &&
-        c8 == indexCharArray arr (off + 8) &&
-        c9 == indexCharArray arr (off + 9)
-  _ -> False
+{-# DEPRECATED equalsLatin10 "use Data.Bytes.Text.Latin1.equals10 instead" #-}
+{-# INLINE equalsLatin10 #-}
+equalsLatin10 = Latin1.equals10
 
 -- | Is the byte sequence, when interpreted as ISO-8859-1-encoded text,
 -- a 11-tuple whose elements match the characters?
 equalsLatin11 :: Char -> Char -> Char -> Char -> Char -> Char -> Char -> Char -> Char -> Char -> Char -> Bytes -> Bool
-equalsLatin11 !c0 !c1 !c2 !c3 !c4 !c5 !c6 !c7 !c8 !c9 !c10 (Bytes arr off len) = case len of
-  11 -> c0 == indexCharArray arr off &&
-        c1 == indexCharArray arr (off + 1) &&
-        c2 == indexCharArray arr (off + 2) &&
-        c3 == indexCharArray arr (off + 3) &&
-        c4 == indexCharArray arr (off + 4) &&
-        c5 == indexCharArray arr (off + 5) &&
-        c6 == indexCharArray arr (off + 6) &&
-        c7 == indexCharArray arr (off + 7) &&
-        c8 == indexCharArray arr (off + 8) &&
-        c9 == indexCharArray arr (off + 9) &&
-        c10 == indexCharArray arr (off + 10)
-  _ -> False
+{-# DEPRECATED equalsLatin11 "use Data.Bytes.Text.Latin1.equals11 instead" #-}
+{-# INLINE equalsLatin11 #-}
+equalsLatin11 = Latin1.equals11
 
 -- | Is the byte sequence, when interpreted as ISO-8859-1-encoded text,
 -- a 12-tuple whose elements match the characters?
 equalsLatin12 :: Char -> Char -> Char -> Char -> Char -> Char -> Char -> Char -> Char -> Char -> Char -> Char -> Bytes -> Bool
-equalsLatin12 !c0 !c1 !c2 !c3 !c4 !c5 !c6 !c7 !c8 !c9 !c10 !c11 (Bytes arr off len) = case len of
-  12 -> c0 == indexCharArray arr off &&
-        c1 == indexCharArray arr (off + 1) &&
-        c2 == indexCharArray arr (off + 2) &&
-        c3 == indexCharArray arr (off + 3) &&
-        c4 == indexCharArray arr (off + 4) &&
-        c5 == indexCharArray arr (off + 5) &&
-        c6 == indexCharArray arr (off + 6) &&
-        c7 == indexCharArray arr (off + 7) &&
-        c8 == indexCharArray arr (off + 8) &&
-        c9 == indexCharArray arr (off + 9) &&
-        c10 == indexCharArray arr (off + 10) &&
-        c11 == indexCharArray arr (off + 11)
-  _ -> False
+{-# DEPRECATED equalsLatin12 "use Data.Bytes.Text.Latin1.equals12 instead" #-}
+{-# INLINE equalsLatin12 #-}
+equalsLatin12 = Latin1.equals12
 
 -- | Is the byte sequence equal to the @NUL@-terminated C String?
 -- The C string must be a constant.
@@ -649,15 +627,12 @@
         False -> Nothing
 
 -- | Touch the byte array backing the byte sequence. This sometimes needed
--- after calling 'contents' so that the @ByteArray@ does not get garbage
+-- after calling 'Pure.contents' so that the @ByteArray@ does not get garbage
 -- collected.
 touch :: PrimMonad m => Bytes -> m ()
 touch (Bytes (ByteArray arr) _ _) = unsafeIOToPrim
   (primitive_ (\s -> Exts.touch# arr s))
 
-indexCharArray :: ByteArray -> Int -> Char
-indexCharArray (ByteArray arr) (I# off) = C# (Exts.indexCharArray# arr off)
-
 -- | Read an entire file strictly into a 'Bytes'.
 readFile :: FilePath -> IO Bytes
 readFile f = Chunks.concat <$> Chunks.readFile f
@@ -740,33 +715,6 @@
 -- @[0x41,0x5A]@ and leaves all other bytes alone. Unconditionally
 -- copies the bytes.
 toLowerAsciiByteArrayClone :: Bytes -> ByteArray
-toLowerAsciiByteArrayClone (Bytes src off0 len0) =
-  runByteArrayST action
-  where
-  action :: forall s. ST s ByteArray
-  action = do
-    dst <- PM.newByteArray len0
-    let go !off !ix !len = if len == 0
-          then pure ()
-          else do
-            let w = PM.indexByteArray src off :: Word8
-                w' = if w >= 0x41 && w <= 0x5A
-                  then w + 32
-                  else w
-            PM.writeByteArray dst ix w'
-            go (off + 1) (ix + 1) (len - 1)
-    go off0 0 len0
-    PM.unsafeFreezeByteArray dst
-
-
--- | /O(n)/ Copy a 'ByteString' to a byte sequence.
-fromByteString :: ByteString -> Bytes
-fromByteString !b = Bytes
-  ( runByteArrayST $ unsafeIOToST $ do 
-      dst@(PM.MutableByteArray dst# ) <- PM.newByteArray len
-      ByteString.unsafeUseAsCString b $ \src -> do
-        PM.copyPtrToMutablePrimArray (PM.MutablePrimArray dst# ) 0 src len
-      PM.unsafeFreezeByteArray dst
-  ) 0 len
-  where
-  !len = ByteString.length b
+{-# DEPRECATED toLowerAsciiByteArrayClone "use Data.Bytes/Text/AsciiExt.toLowerU" #-}
+{-# INLINE toLowerAsciiByteArrayClone #-}
+toLowerAsciiByteArrayClone = AsciiExt.toLowerU
diff --git a/src/Data/Bytes/Chunks.hs b/src/Data/Bytes/Chunks.hs
--- a/src/Data/Bytes/Chunks.hs
+++ b/src/Data/Bytes/Chunks.hs
@@ -290,7 +290,7 @@
   ) 0xcbf29ce484222325 b
 
 -- | Outputs 'Chunks' to the specified 'Handle'. This is implemented
--- with 'hPutBuf'.
+-- with 'IO.hPut'.
 hPut :: Handle -> Chunks -> IO ()
 hPut h = go where
   go ChunksNil = pure ()
diff --git a/src/Data/Bytes/IO.hs b/src/Data/Bytes/IO.hs
--- a/src/Data/Bytes/IO.hs
+++ b/src/Data/Bytes/IO.hs
@@ -22,12 +22,12 @@
 import qualified Data.Primitive as PM
 
 -- | Read 'Bytes' directly from the specified 'Handle'. The resulting
--- 'Bytes' are pinned. This is implemented with 'hGetBuf'.
+-- 'Bytes' are pinned. This is implemented with 'IO.hGetBuf'.
 hGet :: Handle -> Int -> IO Bytes
 hGet h i = createPinnedAndTrim i (\p -> IO.hGetBuf h p i)
 
 -- | Outputs 'Bytes' to the specified 'Handle'. This is implemented
--- with 'hPutBuf'.
+-- with 'IO.hPutBuf'.
 hPut :: Handle -> Bytes -> IO ()
 hPut h b0 = do
   let b1@(Bytes arr _ len) = pin b0
@@ -52,4 +52,3 @@
 touchByteArrayIO :: ByteArray -> IO ()
 touchByteArrayIO (ByteArray x) =
   IO (\s -> (# Exts.touch# x s, () #))
-
diff --git a/src/Data/Bytes/Internal.hs b/src/Data/Bytes/Internal.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Bytes/Internal.hs
@@ -0,0 +1,113 @@
+{-# language BangPatterns #-}
+{-# language DataKinds #-}
+{-# language MagicHash #-}
+{-# language TypeFamilies #-}
+{-# language DuplicateRecordFields #-}
+
+-- This needs to be in its own module to prevent a cyclic dependency
+-- between UnliftedBytes and Data.Bytes.Types
+module Data.Bytes.Internal
+  ( Bytes(..)
+  ) where
+
+import Control.Monad.ST (runST)
+import Control.Monad.ST.Run (runByteArrayST)
+import Data.Bits ((.&.),unsafeShiftR)
+import Data.Char (ord)
+import Data.Primitive (ByteArray(..))
+import Data.Word (Word8)
+import GHC.Base (unsafeChr)
+import GHC.Exts (Int(I#),unsafeCoerce#,sameMutableByteArray#)
+import GHC.Exts (isTrue#,compareByteArrays#,IsList(..))
+
+import qualified Data.List as L
+import qualified Data.Foldable as F
+import qualified Data.Primitive as PM
+
+-- | A slice of a 'ByteArray'.
+data Bytes = Bytes
+  { array :: {-# UNPACK #-} !ByteArray
+  , offset :: {-# UNPACK #-} !Int
+  , length :: {-# UNPACK #-} !Int
+  }
+
+instance IsList Bytes where
+  type Item Bytes = Word8
+  fromListN n xs = Bytes (fromListN n xs) 0 n
+  fromList xs = fromListN (L.length xs) xs
+  toList (Bytes arr off len) = toListLoop off len arr
+
+toListLoop :: Int -> Int -> ByteArray -> [Word8]
+toListLoop !off !len !arr = if len > 0
+  then PM.indexByteArray arr off : toListLoop (off + 1) (len - 1) arr
+  else []
+
+instance Show Bytes where
+  showsPrec _ (Bytes arr off len) s = if len == 0
+    then showString "[]" s
+    else showString "[0x"
+       $ showHexDigits (PM.indexByteArray arr off)
+       $ showLoop (off + 1) (len - 1) arr
+       $ showChar ']'
+       $ s
+
+showLoop :: Int -> Int -> ByteArray -> String -> String
+showLoop !ix !len !arr s = if len > 0
+  then ',':'0':'x':showHexDigits (PM.indexByteArray arr ix) (showLoop (ix + 1) (len - 1) arr s)
+  else s
+
+showHexDigits :: Word8 -> String -> String
+showHexDigits !w s = word4ToChar (unsafeShiftR w 4) : word4ToChar (0x0F .&. w) : s
+
+word4ToChar :: Word8 -> Char
+word4ToChar w = if w < 10
+  then unsafeChr (ord '0' + fromIntegral w)
+  else unsafeChr (ord 'a' + (fromIntegral w) - 10)
+
+instance Eq Bytes where
+  Bytes arr1 off1 len1 == Bytes arr2 off2 len2
+    | len1 /= len2 = False
+    | sameByteArray arr1 arr2 && off1 == off2 = True
+    | otherwise = compareByteArrays arr1 off1 arr2 off2 len1 == EQ
+
+instance Ord Bytes where
+  compare (Bytes arr1 off1 len1) (Bytes arr2 off2 len2)
+    | sameByteArray arr1 arr2 && off1 == off2 && len1 == len2 = EQ
+    | otherwise = compareByteArrays arr1 off1 arr2 off2 (min len1 len2) <> compare len1 len2
+
+instance Semigroup Bytes where
+  -- TODO: Do the trick to move the data constructor to the outside
+  -- of runST.
+  Bytes arrA offA lenA <> Bytes arrB offB lenB = runST $ do
+    marr <- PM.newByteArray (lenA + lenB)
+    PM.copyByteArray marr 0 arrA offA lenA
+    PM.copyByteArray marr lenA arrB offB lenB
+    r <- PM.unsafeFreezeByteArray marr
+    pure (Bytes r 0 (lenA + lenB))
+
+instance Monoid Bytes where
+  mempty = Bytes mempty 0 0
+  mconcat [] = mempty
+  mconcat [x] = x
+  mconcat bs = Bytes r 0 fullLen
+    where
+    !fullLen = L.foldl' (\acc (Bytes _ _ len) -> acc + len) 0 bs
+    r = runByteArrayST $ do
+      marr <- PM.newByteArray fullLen
+      !_ <- F.foldlM
+        (\ !currLen (Bytes arr off len) -> do
+          PM.copyByteArray marr currLen arr off len
+          pure (currLen + len)
+        ) 0 bs
+      PM.unsafeFreezeByteArray marr
+
+compareByteArrays :: ByteArray -> Int -> ByteArray -> Int -> Int -> Ordering
+{-# INLINE compareByteArrays #-}
+compareByteArrays (ByteArray ba1#) (I# off1#) (ByteArray ba2#) (I# off2#) (I# n#) =
+  compare (I# (compareByteArrays# ba1# off1# ba2# off2# n#)) 0
+
+sameByteArray :: ByteArray -> ByteArray -> Bool
+{-# INLINE sameByteArray #-}
+sameByteArray (ByteArray ba1#) (ByteArray ba2#) =
+  isTrue# (sameMutableByteArray# (unsafeCoerce# ba1#) (unsafeCoerce# ba2#))
+
diff --git a/src/Data/Bytes/Mutable.hs b/src/Data/Bytes/Mutable.hs
--- a/src/Data/Bytes/Mutable.hs
+++ b/src/Data/Bytes/Mutable.hs
@@ -1,6 +1,13 @@
 {-# language BangPatterns #-}
 {-# language LambdaCase #-}
 
+-- | If you are interested in sub-arrays of 'MutableByteArray's (e.g. writing
+-- quicksort), it would be grossly inefficient to make a copy of the sub-array.
+-- On the other hand, it'd be really annoying to track limit indices by hand.
+--
+-- This module defines the 'MutableBytes' type which exposes a standard array
+-- interface for a sub-arrays without copying and without manual index
+-- manipulation. For immutable arrays, see 'Data.Bytes'.
 module Data.Bytes.Mutable
   ( -- * Types
     MutableBytes
diff --git a/src/Data/Bytes/Pure.hs b/src/Data/Bytes/Pure.hs
--- a/src/Data/Bytes/Pure.hs
+++ b/src/Data/Bytes/Pure.hs
@@ -18,26 +18,35 @@
   , toPinnedByteArrayClone
   , fromByteArray
   , length
+  , foldl
   , foldl'
+  , foldr
+  , ifoldl'
+  , foldr'
   , fnv1a32
   , fnv1a64
   , toByteString
   , pinnedToByteString
+  , fromByteString
   ) where
 
-import Prelude hiding (length)
+import Prelude hiding (length,foldl,foldr)
 
 import Control.Monad.Primitive (PrimState,PrimMonad)
 import Control.Monad.ST.Run (runByteArrayST)
 import Data.Bits (xor)
-import Data.ByteString (ByteString)
 import Data.Bytes.Types (Bytes(Bytes))
+import Data.ByteString (ByteString)
 import Data.Primitive (ByteArray,MutableByteArray)
 import Data.Word (Word64,Word32,Word8)
 import Foreign.Ptr (Ptr,plusPtr)
+import GHC.IO (unsafeIOToST)
 
+import qualified Data.ByteString as ByteString
 import qualified Data.ByteString.Internal as ByteString
+import qualified Data.ByteString.Unsafe as ByteString
 import qualified Data.Primitive as PM
+import qualified Data.Primitive.Ptr as PM
 import qualified GHC.Exts as Exts
 import qualified GHC.ForeignPtr as ForeignPtr
 
@@ -124,6 +133,16 @@
   (\acc w -> (fromIntegral @Word8 @Word64 w `xor` acc) * 0x00000100000001B3
   ) 0xcbf29ce484222325 b
 
+-- | Left fold over bytes, non-strict in the accumulator.
+foldl :: (a -> Word8 -> a) -> a -> Bytes -> a
+{-# inline foldl #-}
+foldl f a0 (Bytes arr off0 len0) =
+  go (off0 + len0 - 1) (len0 - 1) 
+  where
+  go !off !ix = case ix of
+    (-1) -> a0
+    _ -> f (go (off - 1) (ix - 1)) (PM.indexByteArray arr off)
+
 -- | Left fold over bytes, strict in the accumulator.
 foldl' :: (a -> Word8 -> a) -> a -> Bytes -> a
 {-# inline foldl' #-}
@@ -132,6 +151,34 @@
     0 -> a
     _ -> go (f a (PM.indexByteArray arr off)) (off + 1) (len - 1)
 
+-- | Right fold over bytes, non-strict in the accumulator.
+foldr :: (Word8 -> a -> a) -> a -> Bytes -> a
+{-# inline foldr #-}
+foldr f a0 (Bytes arr off0 len0) = go off0 len0 where
+  go !off !len = case len of
+    0 -> a0
+    _ -> f (PM.indexByteArray arr off) (go (off + 1) (len - 1))
+
+-- | Left fold over bytes, strict in the accumulator. The reduction function
+-- is applied to each element along with its index.
+ifoldl' :: (a -> Int -> Word8 -> a) -> a -> Bytes -> a
+{-# inline ifoldl' #-}
+ifoldl' f a0 (Bytes arr off0 len0) = go a0 0 off0 len0 where
+  go !a !ix !off !len = case len of
+    0 -> a
+    _ -> go (f a ix (PM.indexByteArray arr off)) (ix + 1) (off + 1) (len - 1)
+
+-- | Right fold over bytes, strict in the accumulator.
+foldr' :: (Word8 -> a -> a) -> a -> Bytes -> a
+{-# inline foldr' #-}
+foldr' f a0 (Bytes arr off0 len0) =
+  go a0 (off0 + len0 - 1) (len0 - 1) 
+  where
+  go !a !off !ix = case ix of
+    (-1) -> a
+    _ -> go (f (PM.indexByteArray arr off) a) (off - 1) (ix - 1)
+
+
 -- | Yields a pointer to the beginning of the byte sequence. It is only safe
 -- to call this on a 'Bytes' backed by a pinned @ByteArray@.
 contents :: Bytes -> Ptr Word8
@@ -162,7 +209,8 @@
 toByteString :: Bytes -> ByteString
 toByteString !b = pinnedToByteString (pin b)
 
--- | /O(1)/ Precondition: bytes are pinned. Behavior is undefined otherwise.
+-- | Convert a pinned 'Bytes' to a 'ByteString'
+-- /O(1)/ Precondition: bytes are pinned. Behavior is undefined otherwise.
 pinnedToByteString :: Bytes -> ByteString
 pinnedToByteString (Bytes y@(PM.ByteArray x) off len) =
   ByteString.PS
@@ -171,3 +219,15 @@
       (ForeignPtr.PlainPtr (Exts.unsafeCoerce# x))
     )
     0 len
+
+-- | /O(n)/ Copy a 'ByteString' to a byte sequence.
+fromByteString :: ByteString -> Bytes
+fromByteString !b = Bytes
+  ( runByteArrayST $ unsafeIOToST $ do 
+      dst@(PM.MutableByteArray dst# ) <- PM.newByteArray len
+      ByteString.unsafeUseAsCString b $ \src -> do
+        PM.copyPtrToMutablePrimArray (PM.MutablePrimArray dst# ) 0 src len
+      PM.unsafeFreezeByteArray dst
+  ) 0 len
+  where
+  !len = ByteString.length b
diff --git a/src/Data/Bytes/Text/Ascii.hs b/src/Data/Bytes/Text/Ascii.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Bytes/Text/Ascii.hs
@@ -0,0 +1,29 @@
+{-# LANGUAGE TypeApplications #-}
+
+-- | This module treats 'Bytes' data as holding ASCII text. Providing bytes
+-- outside the ASCII range (@U+0000@ -- @U+007F@) may cause a failure or
+-- unspecified results, but such bytes will never be inspected.
+--
+-- For functions that can operate on ASCII-compatible encodings, see
+-- 'Data.Bytes.Text.AsciiExt'.
+module Data.Bytes.Text.Ascii
+  ( fromString
+  ) where
+
+import Data.Bytes.Types (Bytes)
+import Data.Char (ord)
+import Data.Word (Word8)
+
+import qualified Data.Bytes.Pure as Bytes
+import qualified GHC.Exts as Exts
+
+
+-- | Convert a 'String' consisting of only characters in the ASCII block
+-- to a byte sequence. Any character with a codepoint above @U+007F@ is
+-- replaced by @U+0000@.
+fromString :: String -> Bytes
+fromString = Bytes.fromByteArray
+  . Exts.fromList
+  . map (\c -> let i = ord c in if i < 128 then fromIntegral @Int @Word8 i else 0)
+
+-- TODO presumably also fromText and fromShortText
diff --git a/src/Data/Bytes/Text/AsciiExt.hs b/src/Data/Bytes/Text/AsciiExt.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Bytes/Text/AsciiExt.hs
@@ -0,0 +1,96 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE RankNTypes #-}
+
+-- | This module contains functions which operate on supersets of 'Bytes' containing ASCII-encoded text.
+-- That is, none of the functions here inspect bytes with a value greater than 127, and do not fail due to the presence of such bytes.
+
+-- For functions that can fail for bytes outside the ASCII range, see
+-- 'Data.Bytes.Ascii'. For functions that can inspect bytes outside ASCII, see
+-- any of the modules for ASCII-compatible encodings (e.g. 'Data.Bytes.Utf8',
+-- 'Data.Bytes.Latin1', and so on).
+module Data.Bytes.Text.AsciiExt
+  ( -- * Line-Oriented IO
+    hFoldLines
+  , hForLines_
+  -- ** Standard Handles
+  , forLines_
+  , foldLines
+  -- * Text Manipulation
+  , toLowerU
+  ) where
+
+import Control.Monad.ST (ST)
+import Control.Monad.ST.Run (runByteArrayST)
+import Data.Bytes.Types (Bytes(..))
+import Data.Primitive (ByteArray)
+import Data.Word (Word8)
+import System.IO (Handle, hIsEOF, stdin)
+
+import qualified Data.Bytes.Pure as Bytes
+import qualified Data.ByteString.Char8 as BC8
+import qualified Data.Primitive as PM
+
+-- | `hForLines_` over `stdin`
+forLines_ :: (Bytes -> IO a) -> IO ()
+{-# INLINEABLE forLines_ #-}
+forLines_ = hForLines_ stdin
+
+-- | `hFoldLines` over `stdin`
+foldLines :: a -> (a -> Bytes -> IO a) -> IO a
+{-# INLINEABLE foldLines #-}
+foldLines = hFoldLines stdin
+
+-- | Perform an action on each line of the input, discarding results.
+-- To maintain a running state, see 'hFoldLines'.
+--
+-- Lines are extracted with with 'BC8.hGetLine', which does not document its
+-- dectection algorithm. As of writing (bytestring v0.11.1.0), lines are
+-- delimited by a single @\n@ character (UNIX-style, as all things should be).
+hForLines_ :: Handle -> (Bytes -> IO a) -> IO ()
+hForLines_ h body = loop
+  where
+  loop = hIsEOF h >>= \case
+    False -> do
+      line <- Bytes.fromByteString <$> BC8.hGetLine h
+      _ <- body line
+      loop
+    True -> pure ()
+
+-- | Perform an action on each line of the input, threading state through the computation.
+-- If you do not need to keep a state, see `hForLines_`.
+--
+-- Lines are extracted with with 'BC8.hGetLine', which does not document its
+-- dectection algorithm. As of writing (bytestring v0.11.1.0), lines are
+-- delimited by a single @\n@ character (UNIX-style, as all things should be).
+hFoldLines :: Handle -> a -> (a -> Bytes -> IO a) -> IO a
+hFoldLines h z body = loop z
+  where
+  loop !x = hIsEOF h >>= \case
+    False -> do
+      line <- Bytes.fromByteString <$> BC8.hGetLine h
+      x' <- body x line
+      loop x'
+    True -> pure x
+
+-- | /O(n)/ Convert ASCII letters to lowercase. This adds @0x20@ to bytes in the
+-- range @[0x41,0x5A]@ (@A-Z@ ⇒ @a-z@) and leaves all other bytes alone.
+-- Unconditionally copies the bytes.
+toLowerU :: Bytes -> ByteArray
+toLowerU (Bytes src off0 len0) =
+  runByteArrayST action
+  where
+  action :: forall s. ST s ByteArray
+  action = do
+    dst <- PM.newByteArray len0
+    let go !off !ix !len = if len == 0
+          then pure ()
+          else do
+            let w = PM.indexByteArray src off :: Word8
+                w' = if w >= 0x41 && w <= 0x5A
+                  then w + 32
+                  else w
+            PM.writeByteArray dst ix w'
+            go (off + 1) (ix + 1) (len - 1)
+    go off0 0 len0
+    PM.unsafeFreezeByteArray dst
diff --git a/src/Data/Bytes/Text/Latin1.hs b/src/Data/Bytes/Text/Latin1.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Bytes/Text/Latin1.hs
@@ -0,0 +1,214 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE MagicHash #-}
+{-# LANGUAGE TypeApplications #-}
+
+-- | This module treats 'Bytes' data as holding text encoded in ISO-8859-1. This
+-- encoding can only encode codepoints strictly below @U+0100@, but this allows
+-- each codepoint to be placed directly into a single byte. This range consists
+-- of Unicode Basic Latin, Latin-1 Supplement and C0+C1 Controls, which includes
+-- ASCII.
+--
+-- Strictly, ISO-8859-1 is not to be confused with ISO/IEC 8859-1 (which was the
+-- default encoding for webpages before HTML5). ISO/IEC 8859-1 lacks encodings
+-- for the C0 and C1 control characters. 
+-- 
+-- With HTML5, the default encoding of webpages was changed to Windows-1252,
+-- which is _not_ compatible with ISO-8859-1. Windows-1252 uses the C1 Control
+-- range (@U+0080@ -- @U+009F@) mostly to encode a variety of printable
+-- characters. For this encoding, see 'Data.Bytes.Text.Windows1252'.
+module Data.Bytes.Text.Latin1
+  ( toString
+  , fromString
+  -- * Specialized Comparisons
+  , equals1
+  , equals2
+  , equals3
+  , equals4
+  , equals5
+  , equals6
+  , equals7
+  , equals8
+  , equals9
+  , equals10
+  , equals11
+  , equals12
+  ) where
+
+import Data.Bytes.Types (Bytes(..))
+import Data.Char (ord)
+import Data.Primitive (ByteArray(ByteArray))
+import GHC.Exts (Int(I#),Char(C#),word2Int#,chr#)
+import GHC.Word (Word8(W8#))
+
+import qualified Data.Bytes.Pure as Bytes
+import qualified GHC.Exts as Exts
+
+
+-- | Convert a 'String' consisting of only characters representable
+-- by ISO-8859-1. These are encoded with ISO-8859-1. Any character
+-- with a codepoint above @U+00FF@ is replaced by an unspecified byte.
+fromString :: String -> Bytes
+fromString =
+  Bytes.fromByteArray . Exts.fromList . map (fromIntegral @Int @Word8 . ord)
+
+-- | Interpret a byte sequence as text encoded by ISO-8859-1.
+toString :: Bytes -> String
+{-# INLINE toString #-}
+toString = Bytes.foldr (\(W8# w) xs -> C# (chr# (word2Int# w)) : xs) []
+
+-- TODO presumably also fromText and fromShortText
+
+
+-- | Is the byte sequence, when interpreted as ISO-8859-1-encoded text,
+-- a singleton whose element matches the character?
+equals1 :: Char -> Bytes -> Bool
+{-# INLINE equals1 #-}
+equals1 !c0 (Bytes arr off len) = case len of
+  1 -> c0 == indexCharArray arr off
+  _ -> False
+
+-- | Is the byte sequence, when interpreted as ISO-8859-1-encoded text,
+-- a doubleton whose elements match the characters?
+equals2 :: Char -> Char -> Bytes -> Bool
+equals2 !c0 !c1 (Bytes arr off len) = case len of
+  2 -> c0 == indexCharArray arr off &&
+       c1 == indexCharArray arr (off + 1)
+  _ -> False
+
+-- | Is the byte sequence, when interpreted as ISO-8859-1-encoded text,
+-- a tripleton whose elements match the characters?
+equals3 :: Char -> Char -> Char -> Bytes -> Bool
+equals3 !c0 !c1 !c2 (Bytes arr off len) = case len of
+  3 -> c0 == indexCharArray arr off &&
+       c1 == indexCharArray arr (off + 1) &&
+       c2 == indexCharArray arr (off + 2)
+  _ -> False
+
+-- | Is the byte sequence, when interpreted as ISO-8859-1-encoded text,
+-- a quadrupleton whose elements match the characters?
+equals4 :: Char -> Char -> Char -> Char -> Bytes -> Bool
+equals4 !c0 !c1 !c2 !c3 (Bytes arr off len) = case len of
+  4 -> c0 == indexCharArray arr off &&
+       c1 == indexCharArray arr (off + 1) &&
+       c2 == indexCharArray arr (off + 2) &&
+       c3 == indexCharArray arr (off + 3)
+  _ -> False
+
+-- | Is the byte sequence, when interpreted as ISO-8859-1-encoded text,
+-- a quintupleton whose elements match the characters?
+equals5 :: Char -> Char -> Char -> Char -> Char -> Bytes -> Bool
+equals5 !c0 !c1 !c2 !c3 !c4 (Bytes arr off len) = case len of
+  5 -> c0 == indexCharArray arr off &&
+       c1 == indexCharArray arr (off + 1) &&
+       c2 == indexCharArray arr (off + 2) &&
+       c3 == indexCharArray arr (off + 3) &&
+       c4 == indexCharArray arr (off + 4)
+  _ -> False
+
+-- | Is the byte sequence, when interpreted as ISO-8859-1-encoded text,
+-- a sextupleton whose elements match the characters?
+equals6 :: Char -> Char -> Char -> Char -> Char -> Char -> Bytes -> Bool
+equals6 !c0 !c1 !c2 !c3 !c4 !c5 (Bytes arr off len) = case len of
+  6 -> c0 == indexCharArray arr off &&
+       c1 == indexCharArray arr (off + 1) &&
+       c2 == indexCharArray arr (off + 2) &&
+       c3 == indexCharArray arr (off + 3) &&
+       c4 == indexCharArray arr (off + 4) &&
+       c5 == indexCharArray arr (off + 5)
+  _ -> False
+
+-- | Is the byte sequence, when interpreted as ISO-8859-1-encoded text,
+-- a septupleton whose elements match the characters?
+equals7 :: Char -> Char -> Char -> Char -> Char -> Char -> Char -> Bytes -> Bool
+equals7 !c0 !c1 !c2 !c3 !c4 !c5 !c6 (Bytes arr off len) = case len of
+  7 -> c0 == indexCharArray arr off &&
+       c1 == indexCharArray arr (off + 1) &&
+       c2 == indexCharArray arr (off + 2) &&
+       c3 == indexCharArray arr (off + 3) &&
+       c4 == indexCharArray arr (off + 4) &&
+       c5 == indexCharArray arr (off + 5) &&
+       c6 == indexCharArray arr (off + 6)
+  _ -> False
+
+-- | Is the byte sequence, when interpreted as ISO-8859-1-encoded text,
+-- an octupleton whose elements match the characters?
+equals8 :: Char -> Char -> Char -> Char -> Char -> Char -> Char -> Char -> Bytes -> Bool
+equals8 !c0 !c1 !c2 !c3 !c4 !c5 !c6 !c7 (Bytes arr off len) = case len of
+  8 -> c0 == indexCharArray arr off &&
+       c1 == indexCharArray arr (off + 1) &&
+       c2 == indexCharArray arr (off + 2) &&
+       c3 == indexCharArray arr (off + 3) &&
+       c4 == indexCharArray arr (off + 4) &&
+       c5 == indexCharArray arr (off + 5) &&
+       c6 == indexCharArray arr (off + 6) &&
+       c7 == indexCharArray arr (off + 7)
+  _ -> False
+
+-- | Is the byte sequence, when interpreted as ISO-8859-1-encoded text,
+-- a 9-tuple whose elements match the characters?
+equals9 :: Char -> Char -> Char -> Char -> Char -> Char -> Char -> Char -> Char -> Bytes -> Bool
+equals9 !c0 !c1 !c2 !c3 !c4 !c5 !c6 !c7 !c8 (Bytes arr off len) = case len of
+  9 -> c0 == indexCharArray arr off &&
+       c1 == indexCharArray arr (off + 1) &&
+       c2 == indexCharArray arr (off + 2) &&
+       c3 == indexCharArray arr (off + 3) &&
+       c4 == indexCharArray arr (off + 4) &&
+       c5 == indexCharArray arr (off + 5) &&
+       c6 == indexCharArray arr (off + 6) &&
+       c7 == indexCharArray arr (off + 7) &&
+       c8 == indexCharArray arr (off + 8)
+  _ -> False
+
+-- | Is the byte sequence, when interpreted as ISO-8859-1-encoded text,
+-- a 10-tuple whose elements match the characters?
+equals10 :: Char -> Char -> Char -> Char -> Char -> Char -> Char -> Char -> Char -> Char -> Bytes -> Bool
+equals10 !c0 !c1 !c2 !c3 !c4 !c5 !c6 !c7 !c8 !c9 (Bytes arr off len) = case len of
+  10 -> c0 == indexCharArray arr off &&
+        c1 == indexCharArray arr (off + 1) &&
+        c2 == indexCharArray arr (off + 2) &&
+        c3 == indexCharArray arr (off + 3) &&
+        c4 == indexCharArray arr (off + 4) &&
+        c5 == indexCharArray arr (off + 5) &&
+        c6 == indexCharArray arr (off + 6) &&
+        c7 == indexCharArray arr (off + 7) &&
+        c8 == indexCharArray arr (off + 8) &&
+        c9 == indexCharArray arr (off + 9)
+  _ -> False
+
+-- | Is the byte sequence, when interpreted as ISO-8859-1-encoded text,
+-- a 11-tuple whose elements match the characters?
+equals11 :: Char -> Char -> Char -> Char -> Char -> Char -> Char -> Char -> Char -> Char -> Char -> Bytes -> Bool
+equals11 !c0 !c1 !c2 !c3 !c4 !c5 !c6 !c7 !c8 !c9 !c10 (Bytes arr off len) = case len of
+  11 -> c0 == indexCharArray arr off &&
+        c1 == indexCharArray arr (off + 1) &&
+        c2 == indexCharArray arr (off + 2) &&
+        c3 == indexCharArray arr (off + 3) &&
+        c4 == indexCharArray arr (off + 4) &&
+        c5 == indexCharArray arr (off + 5) &&
+        c6 == indexCharArray arr (off + 6) &&
+        c7 == indexCharArray arr (off + 7) &&
+        c8 == indexCharArray arr (off + 8) &&
+        c9 == indexCharArray arr (off + 9) &&
+        c10 == indexCharArray arr (off + 10)
+  _ -> False
+
+-- | Is the byte sequence, when interpreted as ISO-8859-1-encoded text,
+-- a 12-tuple whose elements match the characters?
+equals12 :: Char -> Char -> Char -> Char -> Char -> Char -> Char -> Char -> Char -> Char -> Char -> Char -> Bytes -> Bool
+equals12 !c0 !c1 !c2 !c3 !c4 !c5 !c6 !c7 !c8 !c9 !c10 !c11 (Bytes arr off len) = case len of
+  12 -> c0 == indexCharArray arr off &&
+        c1 == indexCharArray arr (off + 1) &&
+        c2 == indexCharArray arr (off + 2) &&
+        c3 == indexCharArray arr (off + 3) &&
+        c4 == indexCharArray arr (off + 4) &&
+        c5 == indexCharArray arr (off + 5) &&
+        c6 == indexCharArray arr (off + 6) &&
+        c7 == indexCharArray arr (off + 7) &&
+        c8 == indexCharArray arr (off + 8) &&
+        c9 == indexCharArray arr (off + 9) &&
+        c10 == indexCharArray arr (off + 10) &&
+        c11 == indexCharArray arr (off + 11)
+  _ -> False
+
+indexCharArray :: ByteArray -> Int -> Char
+indexCharArray (ByteArray arr) (I# off) = C# (Exts.indexCharArray# arr off)
diff --git a/src/Data/Bytes/Text/Utf8.hs b/src/Data/Bytes/Text/Utf8.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Bytes/Text/Utf8.hs
@@ -0,0 +1,3 @@
+-- | Placeholder module in case there is demand for treating 'Bytes' as
+-- UTF8-encoded text
+module Data.Bytes.Text.Utf8 () where
diff --git a/src/Data/Bytes/Text/Windows1252.hs b/src/Data/Bytes/Text/Windows1252.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Bytes/Text/Windows1252.hs
@@ -0,0 +1,3 @@
+-- | Placeholder module in case there is demand for treating 'Bytes' as
+-- Windows-1252-encoded text
+module Data.Bytes.Text.Windows1252 () where
diff --git a/src/Data/Bytes/Types.hs b/src/Data/Bytes/Types.hs
--- a/src/Data/Bytes/Types.hs
+++ b/src/Data/Bytes/Types.hs
@@ -1,4 +1,5 @@
 {-# language BangPatterns #-}
+{-# language DataKinds #-}
 {-# language MagicHash #-}
 {-# language TypeFamilies #-}
 {-# language DuplicateRecordFields #-}
@@ -8,31 +9,31 @@
   , Bytes#(..)
   , MutableBytes(..)
   , UnmanagedBytes(..)
+  , BytesN(..)
+  , ByteArrayN(..)
   ) where
 
-import Control.Monad.ST (runST)
-import Control.Monad.ST.Run (runByteArrayST)
-import Data.Bits ((.&.),unsafeShiftR)
-import Data.Char (ord)
+import Data.Bytes.Internal (Bytes(..))
 import Data.Primitive (ByteArray(..),MutableByteArray(..))
 import Data.Primitive.Addr (Addr)
-import Data.Word (Word8)
-import GHC.Base (unsafeChr)
-import GHC.Exts (Int(I#),unsafeCoerce#,sameMutableByteArray#)
-import GHC.Exts (isTrue#,compareByteArrays#,IsList(..))
+import GHC.TypeNats (Nat)
 import UnliftedBytes (Bytes#(..))
 
-import qualified Data.List as L
-import qualified Data.Foldable as F
-import qualified Data.Primitive as PM
-
--- | A slice of a 'ByteArray'.
-data Bytes = Bytes
+-- | A slice of a 'ByteArray' whose compile-time-known length is represented
+-- by a phantom type variable. Consumers of this data constructor must be
+-- careful to preserve the expected invariant.
+data BytesN (n :: Nat) = BytesN
   { array :: {-# UNPACK #-} !ByteArray
   , offset :: {-# UNPACK #-} !Int
-  , length :: {-# UNPACK #-} !Int
   }
 
+-- | A 'ByteArray' whose compile-time-known length is represented
+-- by a phantom type variable. Consumers of this data constructor must be
+-- careful to preserve the expected invariant.
+newtype ByteArrayN (n :: Nat) = ByteArrayN
+  { array :: ByteArray
+  }
+
 -- | A slice of a 'MutableByteArray'.
 data MutableBytes s = MutableBytes
   { array :: {-# UNPACK #-} !(MutableByteArray s)
@@ -45,83 +46,3 @@
   { address :: {-# UNPACK #-} !Addr
   , length :: {-# UNPACK #-} !Int
   }
-
-instance IsList Bytes where
-  type Item Bytes = Word8
-  fromListN n xs = Bytes (fromListN n xs) 0 n
-  fromList xs = fromListN (L.length xs) xs
-  toList (Bytes arr off len) = toListLoop off len arr
-
-toListLoop :: Int -> Int -> ByteArray -> [Word8]
-toListLoop !off !len !arr = if len > 0
-  then PM.indexByteArray arr off : toListLoop (off + 1) (len - 1) arr
-  else []
-
-instance Show Bytes where
-  showsPrec _ (Bytes arr off len) s = if len == 0
-    then showString "[]" s
-    else showString "[0x"
-       $ showHexDigits (PM.indexByteArray arr off)
-       $ showLoop (off + 1) (len - 1) arr
-       $ showChar ']'
-       $ s
-
-showLoop :: Int -> Int -> ByteArray -> String -> String
-showLoop !ix !len !arr s = if len > 0
-  then ',':'0':'x':showHexDigits (PM.indexByteArray arr ix) (showLoop (ix + 1) (len - 1) arr s)
-  else s
-
-showHexDigits :: Word8 -> String -> String
-showHexDigits !w s = word4ToChar (unsafeShiftR w 4) : word4ToChar (0x0F .&. w) : s
-
-word4ToChar :: Word8 -> Char
-word4ToChar w = if w < 10
-  then unsafeChr (ord '0' + fromIntegral w)
-  else unsafeChr (ord 'a' + (fromIntegral w) - 10)
-
-instance Eq Bytes where
-  Bytes arr1 off1 len1 == Bytes arr2 off2 len2
-    | len1 /= len2 = False
-    | sameByteArray arr1 arr2 && off1 == off2 = True
-    | otherwise = compareByteArrays arr1 off1 arr2 off2 len1 == EQ
-
-instance Ord Bytes where
-  compare (Bytes arr1 off1 len1) (Bytes arr2 off2 len2)
-    | sameByteArray arr1 arr2 && off1 == off2 && len1 == len2 = EQ
-    | otherwise = compareByteArrays arr1 off1 arr2 off2 (min len1 len2) <> compare len1 len2
-
-instance Semigroup Bytes where
-  -- TODO: Do the trick to move the data constructor to the outside
-  -- of runST.
-  Bytes arrA offA lenA <> Bytes arrB offB lenB = runST $ do
-    marr <- PM.newByteArray (lenA + lenB)
-    PM.copyByteArray marr 0 arrA offA lenA
-    PM.copyByteArray marr lenA arrB offB lenB
-    r <- PM.unsafeFreezeByteArray marr
-    pure (Bytes r 0 (lenA + lenB))
-
-instance Monoid Bytes where
-  mempty = Bytes mempty 0 0
-  mconcat [] = mempty
-  mconcat [x] = x
-  mconcat bs = Bytes r 0 fullLen
-    where
-    !fullLen = L.foldl' (\acc (Bytes _ _ len) -> acc + len) 0 bs
-    r = runByteArrayST $ do
-      marr <- PM.newByteArray fullLen
-      !_ <- F.foldlM
-        (\ !currLen (Bytes arr off len) -> do
-          PM.copyByteArray marr currLen arr off len
-          pure (currLen + len)
-        ) 0 bs
-      PM.unsafeFreezeByteArray marr
-
-compareByteArrays :: ByteArray -> Int -> ByteArray -> Int -> Int -> Ordering
-{-# INLINE compareByteArrays #-}
-compareByteArrays (ByteArray ba1#) (I# off1#) (ByteArray ba2#) (I# off2#) (I# n#) =
-  compare (I# (compareByteArrays# ba1# off1# ba2# off2# n#)) 0
-
-sameByteArray :: ByteArray -> ByteArray -> Bool
-{-# INLINE sameByteArray #-}
-sameByteArray (ByteArray ba1#) (ByteArray ba2#) =
-  isTrue# (sameMutableByteArray# (unsafeCoerce# ba1#) (unsafeCoerce# ba2#))
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -20,6 +20,8 @@
 
 import qualified Data.ByteString as ByteString
 import qualified Data.Bytes as Bytes
+import qualified Data.Bytes.Text.Ascii as Ascii
+import qualified Data.Bytes.Text.AsciiExt as AsciiExt
 import qualified Data.Bytes.Chunks as Chunks
 import qualified Data.Foldable as Foldable
 import qualified Data.List as List
@@ -44,7 +46,7 @@
     ]
   , testGroup "isPrefixOf"
     [ testCase "A" $
-        Bytes.toLowerAsciiByteArrayClone (bytes "FooBar")
+        AsciiExt.toLowerU (bytes "FooBar")
         @=?
         pack "foobar"
     , testCase "B" $ THU.assertBool "" $
@@ -56,13 +58,31 @@
     , testCase "B" $ THU.assertBool "" $
         not (Bytes.isSuffixOf (bytes "h") (bytes "hey man"))
     ]
+  , testGroup "isInfixOf"
+    [ testCase "small pattern A" $ THU.assertBool "" $
+        Bytes.isInfixOf (bytes "y m") (bytes "hey man")
+    , testCase "small pattern B" $ THU.assertBool "" $
+        Bytes.isInfixOf (bytes "h") (bytes "hey man")
+    , testCase "small pattern C" $ THU.assertBool "" $
+        Bytes.isInfixOf (bytes "an") (bytes "hey man")
+    , testCase "small pattern D" $ THU.assertBool "" $
+        not (Bytes.isInfixOf (bytes "Y M") (bytes "hey man"))
+    , testCase "large pattern A" $ THU.assertBool "" $
+        Bytes.isInfixOf (bytes "Hello, hello!") (bytes "I say hello. Hello, hello! I don't know why you say goodbye, I say hello!")
+    , testCase "large pattern D" $ THU.assertBool "" $
+        not (Bytes.isInfixOf (bytes "Hello, hello!") (bytes "I don't know why you say goodbye, I say hello!"))
+    , testCase "edge: empty pattern" $ THU.assertBool "" $
+        Bytes.isInfixOf (bytes "") (bytes "hello hello!")
+    , testCase "edge: empty string" $ THU.assertBool "" $
+        not (Bytes.isInfixOf (bytes "hello hello!") (bytes ""))
+    ]
   , testGroup "stripOptionalSuffix"
     [ testCase "A" $
-        Bytes.fromAsciiString "hey m"
+        Ascii.fromString "hey m"
         @=?
         Bytes.stripOptionalSuffix (bytes "an") (bytes "hey man")
     , testCase "B" $
-        Bytes.fromAsciiString "hey man"
+        Ascii.fromString "hey man"
         @=?
         Bytes.stripOptionalSuffix (bytes "h") (bytes "hey man")
     ]
@@ -78,17 +98,17 @@
     ]
   , testGroup "dropWhileEnd"
     [ testCase "A" $
-        Bytes.fromAsciiString "aabbcc"
+        Ascii.fromString "aabbcc"
         @=?
         Bytes.dropWhileEnd (== c2w 'b') (bytes "aabbccbb")
     ]
   , testGroup "takeWhileEnd"
     [ testCase "A" $
-        Bytes.fromAsciiString "bb"
+        Ascii.fromString "bb"
         @=?
         Bytes.takeWhileEnd (== c2w 'b') (bytes "aabbccbb")
     , testCase "B" $
-        Bytes.fromAsciiString ""
+        Ascii.fromString ""
         @=?
         Bytes.takeWhileEnd (/= c2w '\n') (bytes "aabbccbb\n")
     , testCase "C" $
@@ -147,13 +167,13 @@
       ===
       map Bytes.fromByteArray (Exts.toList (Bytes.splitU x (slicedPack xs)))
   , testCase "splitInit-A" $
-      [Bytes.fromAsciiString "hello", Bytes.fromAsciiString "world"]
+      [Ascii.fromString "hello", Ascii.fromString "world"]
       @=?
-      (Bytes.splitInit 0x0A (Bytes.fromAsciiString "hello\nworld\n"))
+      (Bytes.splitInit 0x0A (Ascii.fromString "hello\nworld\n"))
   , testCase "splitInit-B" $
-      [Bytes.fromAsciiString "hello", Bytes.fromAsciiString "world"]
+      [Ascii.fromString "hello", Ascii.fromString "world"]
       @=?
-      (Bytes.splitInit 0x0A (Bytes.fromAsciiString "hello\nworld\nthere"))
+      (Bytes.splitInit 0x0A (Ascii.fromString "hello\nworld\nthere"))
   , testProperty "splitEnd1" $ \(x :: Word8) (xs :: [Word8]) ->
       case ByteString.split x (ByteString.pack xs) of
         [] -> Bytes.splitEnd1 x (slicedPack xs) === Nothing
