diff --git a/Data/Attoparsec/ByteString.hs b/Data/Attoparsec/ByteString.hs
--- a/Data/Attoparsec/ByteString.hs
+++ b/Data/Attoparsec/ByteString.hs
@@ -46,6 +46,7 @@
     , I.word8
     , I.anyWord8
     , I.notWord8
+    , I.peekWord8
     , I.satisfy
     , I.satisfyWith
     , I.skip
diff --git a/Data/Attoparsec/ByteString/Char8.hs b/Data/Attoparsec/ByteString/Char8.hs
--- a/Data/Attoparsec/ByteString/Char8.hs
+++ b/Data/Attoparsec/ByteString/Char8.hs
@@ -44,6 +44,7 @@
     , char8
     , anyChar
     , notChar
+    , peekChar
     , satisfy
 
     -- ** Special character parsers
@@ -75,6 +76,11 @@
     , takeWhile1
     , takeTill
 
+    -- ** String combinators
+    -- $specalt
+    , (.*>)
+    , (<*.)
+
     -- ** Consume all remaining input
     , I.takeByteString
     , I.takeLazyByteString
@@ -98,7 +104,7 @@
     , I.atEnd
     ) where
 
-import Control.Applicative ((*>), (<$>), (<|>))
+import Control.Applicative ((*>), (<*), (<$>), (<|>))
 import Data.Attoparsec.ByteString.FastSet (charClass, memberChar)
 import Data.Attoparsec.ByteString.Internal (Parser, (<?>))
 import Data.Attoparsec.Combinator
@@ -214,6 +220,16 @@
 anyChar = satisfy $ const True
 {-# INLINE anyChar #-}
 
+-- | Match any character. Returns 'Nothing' if end of input has been
+-- reached. Does not consume any input.
+--
+-- /Note/: Because this parser does not fail, do not use it with
+-- combinators such as 'many', because such parsers loop until a
+-- failure occurs.  Careless use will thus result in an infinite loop.
+peekChar :: Parser (Maybe Char)
+peekChar = (fmap w2c) `fmap` I.peekWord8
+{-# INLINE peekChar #-}
+
 -- | Fast predicate for matching ASCII space characters.
 --
 -- /Note/: This predicate only gives correct answers for the ASCII
@@ -327,6 +343,33 @@
 skipSpace = I.skipWhile isSpace_w8
 {-# INLINE skipSpace #-}
 
+-- $specalt
+--
+-- The '.*>' and '<*.' combinators are intended for use with the
+-- @OverloadedStrings@ language extension.  They simplify the common
+-- task of matching a statically known string, then immediately
+-- parsing something else.
+--
+-- An example makes this easier to understand:
+--
+-- @{-\# LANGUAGE OverloadedStrings #-}
+--
+-- shoeSize = \"Shoe size: \" '.*>' 'decimal'
+-- @
+--
+-- If we were to try to use '*>' above instead, the type checker would
+-- not be able to tell which 'IsString' instance to use for the text
+-- in quotes.  We would have to be explicit, using either a type
+-- signature or the 'I.string' parser.
+
+-- | Type-specialized version of '*>' for 'B.ByteString'.
+(.*>) :: B.ByteString -> Parser a -> Parser a
+s .*> f = I.string s *> f
+
+-- | Type-specialized version of '<*' for 'B.ByteString'.
+(<*.) :: Parser a -> B.ByteString -> Parser a
+f <*. s = f <* I.string s
+
 -- | A predicate that matches either a carriage return @\'\\r\'@ or
 -- newline @\'\\n\'@ character.
 isEndOfLine :: Word8 -> Bool
@@ -408,7 +451,7 @@
 -- >rational "3.1"   == Done 3.1 ""
 -- >rational "3e4"   == Done 30000.0 ""
 -- >rational "3.1e4" == Done 31000.0, ""
-
+--
 -- Examples with behaviour identical to 'read':
 --
 -- >rational ".3"    == Fail "input does not start with a digit"
diff --git a/Data/Attoparsec/ByteString/Internal.hs b/Data/Attoparsec/ByteString/Internal.hs
--- a/Data/Attoparsec/ByteString/Internal.hs
+++ b/Data/Attoparsec/ByteString/Internal.hs
@@ -33,6 +33,7 @@
     , skip
     , word8
     , notWord8
+    , peekWord8
 
     -- ** Byte classes
     , inClass
@@ -58,7 +59,6 @@
     -- * State observation and manipulation functions
     , endOfInput
     , atEnd
-    , ensure
 
     -- * Utilities
     , endOfLine
@@ -399,6 +399,22 @@
 notWord8 :: Word8 -> Parser Word8
 notWord8 c = satisfy (/= c) <?> "not " ++ show c
 {-# INLINE notWord8 #-}
+
+-- | Match any byte. Returns 'Nothing' if end of input has been
+-- reached. Does not consume any input.
+--
+-- /Note/: Because this parser does not fail, do not use it with
+-- combinators such as 'many', because such parsers loop until a
+-- failure occurs.  Careless use will thus result in an infinite loop.
+peekWord8 :: Parser (Maybe Word8)
+peekWord8 = T.Parser $ \i0 a0 m0 _kf ks ->
+            let ks' i a m = let w = B.unsafeHead (unI i)
+                            in w `seq` ks i a m (Just w)
+                kf' i a m = ks i a m Nothing
+            in if B.null (unI i0)
+               then prompt i0 a0 m0 kf' ks'
+               else ks' i0 a0 m0
+{-# INLINE peekWord8 #-}
 
 -- | Match only if all input has been consumed.
 endOfInput :: Parser ()
diff --git a/Data/Attoparsec/ByteString/Lazy.hs b/Data/Attoparsec/ByteString/Lazy.hs
--- a/Data/Attoparsec/ByteString/Lazy.hs
+++ b/Data/Attoparsec/ByteString/Lazy.hs
@@ -33,6 +33,7 @@
     , eitherResult
     ) where
 
+import Control.DeepSeq (NFData(rnf))
 import Data.ByteString.Lazy.Internal (ByteString(..), chunk)
 import qualified Data.ByteString as B
 import qualified Data.Attoparsec.ByteString as A
@@ -52,6 +53,16 @@
               -- ^ The parse succeeded.  The 'ByteString' is the
               -- input that had not yet been consumed (if any) when
               -- the parse succeeded.
+
+instance NFData r => NFData (Result r) where
+    rnf (Fail bs ctxs msg) = rnfBS bs `seq` rnf ctxs `seq` rnf msg
+    rnf (Done bs r)        = rnfBS bs `seq` rnf r
+    {-# INLINE rnf #-}
+
+rnfBS :: ByteString -> ()
+rnfBS (Chunk _ xs) = rnfBS xs
+rnfBS Empty        = ()
+{-# INLINE rnfBS #-}
 
 instance Show r => Show (Result r) where
     show (Fail bs stk msg) =
diff --git a/Data/Attoparsec/Text.hs b/Data/Attoparsec/Text.hs
--- a/Data/Attoparsec/Text.hs
+++ b/Data/Attoparsec/Text.hs
@@ -1,4 +1,5 @@
-{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE BangPatterns, FlexibleInstances, TypeSynonymInstances #-}
+
 -- |
 -- Module      :  Data.Attoparsec.Text
 -- Copyright   :  Bryan O'Sullivan 2011
@@ -50,6 +51,7 @@
     , I.satisfy
     , I.satisfyWith
     , I.skip
+    , I.peekChar
 
     -- ** Special character parsers
     , digit
@@ -62,7 +64,7 @@
 
     -- * Efficient string handling
     , I.string
-    , stringCI
+    , I.stringCI
     , skipSpace
     , I.skipWhile
     , I.scan
@@ -71,6 +73,11 @@
     , I.takeWhile1
     , I.takeTill
 
+    -- ** String combinators
+    -- $specalt
+    , (.*>)
+    , (<*.)
+
     -- ** Consume all remaining input
     , I.takeText
     , I.takeLazyText
@@ -94,7 +101,7 @@
     , I.atEnd
     ) where
 
-import Control.Applicative ((<$>), (*>), (<|>))
+import Control.Applicative ((<$>), (*>), (<*), (<|>))
 import Data.Attoparsec.Combinator
 import Data.Attoparsec.Number (Number(..))
 import Data.Attoparsec.Text.Internal ((<?>), Parser, Result, parse, takeWhile1)
@@ -316,7 +323,7 @@
 -- >rational "3.1"   == Done 3.1 ""
 -- >rational "3e4"   == Done 30000.0 ""
 -- >rational "3.1e4" == Done 31000.0, ""
-
+--
 -- Examples with behaviour identical to 'read':
 --
 -- >rational ".3"    == Fail "input does not start with a digit"
@@ -392,15 +399,37 @@
 space = I.satisfy isSpace <?> "space"
 {-# INLINE space #-}
 
--- | Satisfy a literal string, ignoring case.
-stringCI :: Text -> Parser Text
-stringCI = I.stringTransform T.toCaseFold
-{-# INLINE stringCI #-}
-
 -- | Skip over white space.
 skipSpace :: Parser ()
 skipSpace = I.skipWhile isSpace
 {-# INLINE skipSpace #-}
+
+-- $specalt
+--
+-- The '.*>' and '<*.' combinators are intended for use with the
+-- @OverloadedStrings@ language extension.  They simplify the common
+-- task of matching a statically known string, then immediately
+-- parsing something else.
+--
+-- An example makes this easier to understand:
+--
+-- @{-\# LANGUAGE OverloadedStrings #-}
+--
+-- shoeSize = \"Shoe size: \" '.*>' 'decimal'
+-- @
+--
+-- If we were to try to use '*>' above instead, the type checker would
+-- not be able to tell which 'IsString' instance to use for the text
+-- in quotes.  We would have to be explicit, using either a type
+-- signature or the 'I.string' parser.
+
+-- | Type-specialized version of '*>' for 'Text'.
+(.*>) :: Text -> Parser a -> Parser a
+s .*> f = I.string s *> f
+
+-- | Type-specialized version of '<*' for 'Text'.
+(<*.) :: Parser a -> Text -> Parser a
+f <*. s = f <* I.string s
 
 data T = T !Integer !Int
 
diff --git a/Data/Attoparsec/Text/Internal.hs b/Data/Attoparsec/Text/Internal.hs
--- a/Data/Attoparsec/Text/Internal.hs
+++ b/Data/Attoparsec/Text/Internal.hs
@@ -1,4 +1,6 @@
-{-# LANGUAGE BangPatterns, Rank2Types, OverloadedStrings, RecordWildCards #-}
+{-# LANGUAGE BangPatterns, FlexibleInstances, OverloadedStrings, Rank2Types,
+    RecordWildCards, TypeSynonymInstances #-}
+{-# OPTIONS_GHC -fno-warn-orphans #-}
 -- |
 -- Module      :  Data.Attoparsec.Text.Internal
 -- Copyright   :  Bryan O'Sullivan 2011
@@ -33,6 +35,7 @@
     , skip
     , char
     , notChar
+    , peekChar
 
     -- ** Character classes
     , inClass
@@ -41,7 +44,7 @@
     -- * Efficient string handling
     , skipWhile
     , string
-    , stringTransform
+    , stringCI
     , take
     , scan
     , takeWhile
@@ -55,7 +58,6 @@
     -- * State observation and manipulation functions
     , endOfInput
     , atEnd
-    , ensure
 
     -- * Utilities
     , endOfLine
@@ -65,6 +67,7 @@
 import Control.Monad (when)
 import Data.Attoparsec.Combinator
 import Data.Attoparsec.Internal.Types hiding (Parser, Input, Added, Failure, Success)
+import Data.String (IsString(..))
 import Data.Text (Text)
 import Prelude hiding (getChar, take, takeWhile)
 import qualified Data.Attoparsec.Internal.Types as T
@@ -79,6 +82,9 @@
 type Failure r = T.Failure Text r
 type Success a r = T.Success Text a r
 
+instance IsString (Parser Text) where
+    fromString = string . T.pack
+
 -- | If at least @n@ characters of input are available, return the
 -- current input, otherwise fail.
 ensure :: Int -> Parser Text
@@ -219,10 +225,25 @@
 string s = takeWith (T.length s) (==s)
 {-# INLINE string #-}
 
-stringTransform :: (Text -> Text) -> Text
-                -> Parser Text
-stringTransform f s = takeWith (T.length s) ((==f s) . f)
-{-# INLINE stringTransform #-}
+-- | Satisfy a literal string, ignoring case.
+--
+-- Note: this function is currently quite inefficient. Unicode case
+-- folding can change the length of a string (\"&#223;\" becomes
+-- "ss"), which makes a simple, efficient implementation tricky.  We
+-- have (for now) chosen simplicity over efficiency.
+stringCI :: Text -> Parser Text
+stringCI s = go 0
+  where
+    go !n
+      | n > T.length fs = fail "stringCI"
+      | otherwise = do
+      t <- ensure n
+      let h = unsafeTake n t
+      if T.toCaseFold h == fs
+        then put (unsafeDrop n t) >> return h
+        else go (n+1)
+    fs = T.toCaseFold s
+{-# INLINE stringCI #-}
 
 -- | Skip past input for as long as the predicate returns 'True'.
 skipWhile :: (Char -> Bool) -> Parser ()
@@ -381,6 +402,22 @@
 notChar :: Char -> Parser Char
 notChar c = satisfy (/= c) <?> "not " ++ show c
 {-# INLINE notChar #-}
+
+-- | Match any character. Returns 'Nothing' if end of input has been
+-- reached. Does not consume any input.
+--
+-- /Note/: Because this parser does not fail, do not use it with
+-- combinators such as 'many', because such parsers loop until a
+-- failure occurs.  Careless use will thus result in an infinite loop.
+peekChar :: Parser (Maybe Char)
+peekChar = T.Parser $ \i0 a0 m0 _kf ks ->
+       let ks' i a m = let w = unsafeHead (unI i)
+                       in w `seq` ks i a m (Just w)
+           kf' i a m = ks i a m Nothing
+       in if T.null (unI i0)
+          then prompt i0 a0 m0 kf' ks'
+          else ks' i0 a0 m0
+{-# INLINE peekChar #-}
 
 -- | Match only if all input has been consumed.
 endOfInput :: Parser ()
diff --git a/Data/Attoparsec/Text/Lazy.hs b/Data/Attoparsec/Text/Lazy.hs
--- a/Data/Attoparsec/Text/Lazy.hs
+++ b/Data/Attoparsec/Text/Lazy.hs
@@ -33,6 +33,7 @@
     , eitherResult
     ) where
 
+import Control.DeepSeq (NFData(rnf))
 import Data.Text.Lazy.Internal (Text(..), chunk)
 import qualified Data.Attoparsec.Internal.Types as T
 import qualified Data.Attoparsec.Text as A
@@ -56,6 +57,11 @@
     show (Fail bs stk msg) =
         "Fail " ++ show bs ++ " " ++ show stk ++ " " ++ show msg
     show (Done bs r)       = "Done " ++ show bs ++ " " ++ show r
+
+instance NFData r => NFData (Result r) where
+    rnf (Fail bs ctxs msg) = rnf bs `seq` rnf ctxs `seq` rnf msg
+    rnf (Done bs r)        = rnf bs `seq` rnf r
+    {-# INLINE rnf #-}
 
 fmapR :: (a -> b) -> Result a -> Result b
 fmapR _ (Fail st stk msg) = Fail st stk msg
diff --git a/attoparsec.cabal b/attoparsec.cabal
--- a/attoparsec.cabal
+++ b/attoparsec.cabal
@@ -1,5 +1,5 @@
 name:            attoparsec
-version:         0.10.1.1
+version:         0.10.2.0
 license:         BSD3
 license-file:    LICENSE
 category:        Text, Parsing
@@ -17,15 +17,17 @@
     efficiently with network protocols and complicated text/binary
     file formats.
 extra-source-files:
+    README.markdown
+    benchmarks/Benchmarks.hs
     benchmarks/Makefile
-    benchmarks/med.txt.bz2
     benchmarks/Tiny.hs
+    benchmarks/attoparsec-benchmarks.cabal
+    benchmarks/med.txt.bz2
     examples/Makefile
     examples/Parsec_RFC2616.hs
-    examples/rfc2616.c
     examples/RFC2616.hs
     examples/TestRFC2616.hs
-    README.markdown
+    examples/rfc2616.c
     tests/Makefile
     tests/QC.hs
     tests/QC/*.hs
@@ -61,7 +63,7 @@
                    Data.Attoparsec.Internal.Types
                    Data.Attoparsec.Text.FastSet
                    Data.Attoparsec.Text.Internal
-  ghc-options:     -Wall
+  ghc-options: -O2 -Wall
 
   if flag(developer)
     ghc-prof-options: -auto-all
diff --git a/benchmarks/Benchmarks.hs b/benchmarks/Benchmarks.hs
new file mode 100644
--- /dev/null
+++ b/benchmarks/Benchmarks.hs
@@ -0,0 +1,74 @@
+{-# LANGUAGE BangPatterns #-}
+
+import Control.Applicative
+import Control.DeepSeq (NFData(rnf))
+import Criterion.Main (bench, bgroup, defaultMain, nf, whnf)
+import Data.ByteString.Internal (ByteString(..))
+import Data.Char
+import Text.Parsec.Text ()
+import Text.Parsec.Text.Lazy ()
+import qualified Data.Attoparsec.ByteString as AB
+import qualified Data.Attoparsec.ByteString.Char8 as AC
+import qualified Data.Attoparsec.ByteString.Lazy as ABL
+import qualified Data.Attoparsec.Text as AT
+import qualified Data.Attoparsec.Text.Lazy as ATL
+import qualified Data.ByteString as B
+import qualified Data.ByteString.Char8 as BC
+import qualified Data.ByteString.Lazy as BL
+import qualified Data.Text as T
+import qualified Data.Text.Lazy as TL
+import qualified Text.Parsec as P
+
+instance NFData ByteString where
+    rnf (PS _ _ _) = ()
+
+instance NFData P.ParseError where
+    rnf = rnf . show
+
+chunksOf :: Int -> [a] -> [[a]]
+chunksOf k = go
+  where go xs = case splitAt k xs of
+                  ([],_)  -> []
+                  (y, ys) -> y : go ys
+
+fromLazy :: BL.ByteString -> B.ByteString
+fromLazy = B.concat . BL.toChunks
+
+main = do
+  let s  = take 1024 . cycle $ ['a'..'z'] ++ ['A'..'Z']
+      !b = BC.pack s
+      !bl = BL.fromChunks . map BC.pack . chunksOf 4 $ s
+      !t = T.pack s
+      !tl = TL.fromChunks . map T.pack . chunksOf 4 $ s
+  defaultMain [
+     bgroup "many" [
+       bgroup "attoparsec" [
+         bench "B" $ nf (AB.parse (many (AC.satisfy AC.isAlpha_ascii))) b
+       , bench "BL" $ nf (ABL.parse (many (AC.satisfy AC.isAlpha_ascii))) bl
+       , bench "T" $ nf (AT.parse (many (AT.satisfy AC.isAlpha_ascii))) t
+       , bench "TL" $ nf (ATL.parse (many (AT.satisfy AC.isAlpha_ascii))) tl
+       ]
+     , bgroup "parsec" [
+         bench "S" $ nf (P.parse (many (P.satisfy AC.isAlpha_ascii)) "") s
+       , bench "B" $ nf (P.parse (many (P.satisfy AC.isAlpha_ascii)) "") b
+       , bench "BL" $ nf (P.parse (many (P.satisfy AC.isAlpha_ascii)) "") bl
+       , bench "T" $ nf (P.parse (many (P.satisfy AC.isAlpha_ascii)) "") t
+       , bench "TL" $ nf (P.parse (many (P.satisfy AC.isAlpha_ascii)) "") tl
+       ]
+     ]
+   , bgroup "comparison" [
+       bgroup "many-vs-takeWhile" [
+         bench "many" $ nf (AB.parse (many (AC.satisfy AC.isAlpha_ascii))) b
+       , bench "takeWhile" $ nf (AB.parse (AC.takeWhile AC.isAlpha_ascii)) b
+       ]
+     , bgroup "letter-vs-isAlpha" [
+         bench "letter" $ nf (AB.parse (many AC.letter_ascii)) b
+       , bench "isAlpha" $ nf (AB.parse (many (AC.satisfy AC.isAlpha_ascii))) b
+       ]
+     ]
+   , bgroup "takeWhile" [
+       bench "isAlpha" $ nf (ABL.parse (AC.takeWhile isAlpha)) bl
+     , bench "isAlpha_ascii" $ nf (ABL.parse (AC.takeWhile AC.isAlpha_ascii)) bl
+     , bench "isAlpha_iso8859_15" $ nf (ABL.parse (AC.takeWhile AC.isAlpha_iso8859_15)) bl
+     ]
+   ]
diff --git a/benchmarks/attoparsec-benchmarks.cabal b/benchmarks/attoparsec-benchmarks.cabal
new file mode 100644
--- /dev/null
+++ b/benchmarks/attoparsec-benchmarks.cabal
@@ -0,0 +1,18 @@
+-- These benchmarks are not intended to be installed.
+-- So don't install 'em.
+
+name: attoparsec-benchmarks
+version: 0
+cabal-version: >=1.2
+build-type: Simple
+
+executable attoparsec-benchmarks
+  main-is: Benchmarks.hs
+  build-depends:
+    attoparsec,
+    base,
+    bytestring,
+    criterion >= 0.5,
+    deepseq == 1.1.*,
+    parsec >= 3.1.2,
+    text
diff --git a/tests/QC/ByteString.hs b/tests/QC/ByteString.hs
--- a/tests/QC/ByteString.hs
+++ b/tests/QC/ByteString.hs
@@ -2,7 +2,7 @@
 {-# OPTIONS_GHC -fno-warn-orphans #-}
 module QC.ByteString (tests) where
 
-import Control.Applicative ((<$>))
+import Control.Applicative ((<$>), (<*>))
 import Prelude hiding (takeWhile)
 import Test.Framework.Providers.QuickCheck2 (testProperty)
 import Test.QuickCheck
@@ -48,6 +48,11 @@
     where v = L.head bs
           bs = L.pack s
 
+peekWord8 s
+    | L.null s  = p == Just (Nothing, s)
+    | otherwise = p == Just (Just (L.head s), s)
+  where p = maybeP ((,) <$> P.peekWord8 <*> P.takeLazyByteString) s
+
 string s t = maybeP (P.string s') (s `L.append` t) == Just s'
   where s' = toStrict s
 
@@ -94,6 +99,7 @@
     testProperty "word8" word8,
     testProperty "notWord8" notWord8,
     testProperty "anyWord8" anyWord8,
+    testProperty "peekWord8" peekWord8,
     testProperty "string" string,
     testProperty "skipWhile" skipWhile,
     testProperty "takeCount" takeCount,
diff --git a/tests/QC/Text.hs b/tests/QC/Text.hs
--- a/tests/QC/Text.hs
+++ b/tests/QC/Text.hs
@@ -2,7 +2,7 @@
 {-# OPTIONS_GHC -fno-warn-orphans #-}
 module QC.Text (tests) where
 
-import Control.Applicative ((<$>))
+import Control.Applicative ((<$>), (<*>))
 import Prelude hiding (takeWhile)
 import Test.Framework.Providers.QuickCheck2 (testProperty)
 import Test.QuickCheck
@@ -48,9 +48,17 @@
     where v = L.head bs
           bs = L.pack s
 
+peekChar s
+    | L.null s  = p == Just (Nothing, s)
+    | otherwise = p == Just (Just (L.head s), s)
+  where p = maybeP ((,) <$> P.peekChar <*> P.takeLazyText) s
+
 string s t = maybeP (P.string s') (s `L.append` t) == Just s'
   where s' = toStrict s
 
+stringCI s = P.parseOnly (P.stringCI fs) s == Right s
+  where fs = T.toCaseFold s
+
 toStrict = T.concat . L.toChunks
 
 skipWhile w s =
@@ -94,7 +102,9 @@
     testProperty "char" char,
     testProperty "notChar" notChar,
     testProperty "anyChar" anyChar,
+    testProperty "peekChar" peekChar,
     testProperty "string" string,
+    testProperty "stringCI" stringCI,
     testProperty "skipWhile" skipWhile,
     testProperty "takeCount" takeCount,
     testProperty "takeWhile" takeWhile,
