scanner (empty) → 0.1
raw patch · 14 files changed
+831/−0 lines, 14 filesdep +attoparsecdep +basedep +bytestringsetup-changedbinary-added
Dependencies added: attoparsec, base, bytestring, criterion, hspec, scanner, text
Files
- LICENSE +30/−0
- README.md +22/−0
- Setup.hs +2/−0
- bench/bench.hs +95/−0
- bench/bench.png binary
- compat/Data/Either.hs +25/−0
- compat/Prelude.hs +28/−0
- examples/Redis/Atto.hs +68/−0
- examples/Redis/Reply.hs +16/−0
- examples/Redis/Scanner.hs +76/−0
- lib/Scanner.hs +116/−0
- lib/Scanner/Internal.hs +167/−0
- scanner.cabal +55/−0
- spec/spec.hs +131/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2016, Yuras Shumovich++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Yuras Shumovich nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,22 @@+# scanner+Fast non-backtracking incremental combinator parsing for bytestrings++[](https://travis-ci.org/Yuras/scanner)++It is often convinient to use backtracking to parse some sofisticated+input. Unfortunately it kills performance, so usually you should avoid+backtracking.++Often (actually always, but it could be too hard sometimes) you can+implement your parser without any backtracking. It that case all the+bookkeeping usuall parser combinators do becomes unnecessary. The+scanner libarary is designed for such cases. It is often 2 times faster+then attoparsec.++As an example, please checkout redis protocol parser included into the+repo, both using attoparsec and scanner libraries:+https://github.com/Yuras/scanner/tree/master/examples/Redis++Benchmark results:++
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ bench/bench.hs view
@@ -0,0 +1,95 @@+{-# LANGUAGE OverloadedStrings #-}++module Main+( main+)+where++import qualified Scanner++import qualified Redis.Reply as Redis+import qualified Redis.Atto+import qualified Redis.Scanner++import Data.ByteString (ByteString)+import qualified Data.ByteString as ByteString+import qualified Data.Attoparsec.ByteString as Atto++import Criterion+import Criterion.Main++main :: IO ()+main = do+ let smallStringInput = "+OK\r\n"+ longStringInput = "+11111111111111111111111111122222222222222222222233333333333333333333333444444444444444444445555555555555555555555666666666666666666677777777777777777777888888888888888888888999999999999999999000000000000000000\r\n"+ intInput = ":123\r\n"+ bulkInput = "$10\r\n0123456789\r\n"+ multiInput = "*3\r\n+A\r\n+B\r\n+C\r\n"+ print (redisByteStringReply smallStringInput)+ print (redisAttoReply smallStringInput)+ print (redisScannerReply smallStringInput)+ print (redisAttoReply intInput)+ print (redisScannerReply intInput)+ print (redisAttoReply bulkInput)+ print (redisScannerReply bulkInput)+ print (redisAttoReply multiInput)+ print (redisScannerReply multiInput)+ defaultMain+ [ bgroup "small string"+ [ bench "Atto" $ whnf redisAttoReply smallStringInput+ , bench "Scanner" $ whnf redisScannerReply smallStringInput+ , bench "ByteString" $ whnf redisByteStringReply smallStringInput+ ]++ , bgroup "long string"+ [ bench "Atto" $ whnf redisAttoReply longStringInput+ , bench "Scanner" $ whnf redisScannerReply longStringInput+ , bench "ByteString" $ whnf redisByteStringReply longStringInput+ ]++ , bgroup "integer"+ [ bench "Atto" $ whnf redisAttoReply intInput+ , bench "Scanner" $ whnf redisScannerReply intInput+ ]++ , bgroup "bulk"+ [ bench "Atto" $ whnf redisAttoReply bulkInput+ , bench "Scanner" $ whnf redisScannerReply bulkInput+ ]++ , bgroup "multi"+ [ bench "Atto" $ whnf redisAttoReply multiInput+ , bench "Scanner" $ whnf redisScannerReply multiInput+ ]+ ]++{-# NOINLINE redisAttoReply #-}+redisAttoReply :: ByteString -> Either String Redis.Reply+redisAttoReply bs = case Atto.parse Redis.Atto.reply bs of+ Atto.Done _ r -> Right r+ Atto.Fail _ _ err -> Left err+ Atto.Partial _ -> Left "Not enough input"++{-# NOINLINE redisScannerReply #-}+redisScannerReply :: ByteString -> Either String Redis.Reply+redisScannerReply bs = case Scanner.scan Redis.Scanner.reply bs of+ Scanner.Done _ r -> Right r+ Scanner.Fail _ err -> Left err+ Scanner.More _ -> Left "Not enought input"++{-# NOINLINE redisByteStringReply #-}+redisByteStringReply :: ByteString -> Either String Redis.Reply+redisByteStringReply bs = case ByteString.uncons bs of+ Just (c, bs') -> case c of+ 43 -> let (l, r) = ByteString.span (/= 13) bs'+ in case ByteString.uncons r of+ Just (c', bs'') -> case c' of+ 13 -> case ByteString.uncons bs'' of+ Just (c'', _) -> case c'' of+ 10 -> Right (Redis.String l)+ _ -> Left "Unexpected input"+ Nothing -> Left "Not enough input"+ _ -> Left "Unexpected input"+ Nothing -> Left "Not enought input"+ _ -> Left "Unknown type"+ Nothing -> Left "Not enought input"
+ bench/bench.png view
binary file changed (absent → 22407 bytes)
+ compat/Data/Either.hs view
@@ -0,0 +1,25 @@+{-# LANGUAGE PackageImports #-}+{-# LANGUAGE CPP #-}++module Data.Either+(+ module Export,+#if MIN_VERSION_base(4,7,0)+#else+ isRight,+ isLeft,+#endif+)+where++import "base" Data.Either as Export++#if MIN_VERSION_base(4,7,0)+#else+isRight :: Either a b -> Bool+isRight (Right _) = True+isRight _ = False++isLeft :: Either a b -> Bool+isLeft = not . isRight+#endif
+ compat/Prelude.hs view
@@ -0,0 +1,28 @@+{-# LANGUAGE PackageImports #-}+{-# LANGUAGE CPP #-}++module Prelude+(+ module P,++#if MIN_VERSION_base(4,8,0)+#else+ (<$>),+ Monoid(..),+ Applicative(..),+#endif+)+where++#if MIN_VERSION_base(4,6,0)+import "base" Prelude as P+#else+import "base" Prelude as P hiding (catch)+#endif++#if MIN_VERSION_base(4,8,0)+#else+import Data.Functor((<$>))+import Data.Monoid(Monoid(..))+import Control.Applicative(Applicative(..))+#endif
+ examples/Redis/Atto.hs view
@@ -0,0 +1,68 @@+{-# LANGUAGE OverloadedStrings #-}++module Redis.Atto+( reply+)+where++import Redis.Reply++import Prelude hiding (error)+import Data.ByteString (ByteString)+import Data.Attoparsec.ByteString (Parser)+import qualified Data.Attoparsec.ByteString as Atto (takeTill)+import qualified Data.Attoparsec.ByteString.Char8 as Atto hiding (takeTill)+import Control.Monad++{-# INLINE reply #-}+reply :: Parser Reply+reply = do+ c <- Atto.anyChar+ case c of+ '+' -> string+ '-' -> error+ ':' -> integer+ '$' -> bulk+ '*' -> multi+ _ -> fail "Unknown reply type"++{-# INLINE string #-}+string :: Parser Reply+string = String <$> line++{-# INLINE error #-}+error :: Parser Reply+error = Error <$> line++{-# INLINE integer #-}+integer :: Parser Reply+integer = Integer <$> integral++{-# INLINE bulk #-}+bulk :: Parser Reply+bulk = Bulk <$> do+ len <- integral+ if len < 0+ then return Nothing+ else Just <$> Atto.take len <* eol++-- don't inline it to break the circle between reply and multi+{-# NOINLINE multi #-}+multi :: Parser Reply+multi = Multi <$> do+ len <- integral+ if len < 0+ then return Nothing+ else Just <$> Atto.count len reply++{-# INLINE integral #-}+integral :: Integral i => Parser i+integral = Atto.signed Atto.decimal <* eol++{-# INLINE line #-}+line :: Parser ByteString+line = Atto.takeTill (== 13) <* eol++{-# INLINE eol #-}+eol :: Parser ()+eol = void $ Atto.string "\r\n"
+ examples/Redis/Reply.hs view
@@ -0,0 +1,16 @@++module Redis.Reply+( Reply (..)+)+where++import Data.Int+import Data.ByteString (ByteString)++data Reply+ = String ByteString+ | Error ByteString+ | Integer Int64+ | Bulk (Maybe ByteString)+ | Multi (Maybe [Reply])+ deriving (Show, Eq)
+ examples/Redis/Scanner.hs view
@@ -0,0 +1,76 @@+{-# LANGUAGE OverloadedStrings #-}++module Redis.Scanner+( reply+)+where++import Scanner (Scanner)+import qualified Scanner++import Redis.Reply++import Prelude hiding (error)+import Data.ByteString (ByteString)+import qualified Data.Text.Encoding as Text+import qualified Data.Text.Read as Text+import Control.Monad++{-# INLINE reply #-}+reply :: Scanner Reply+reply = do+ c <- Scanner.anyChar8+ case c of+ '+' -> string+ '-' -> error+ ':' -> integer+ '$' -> bulk+ '*' -> multi+ _ -> fail "Unknown reply type"++{-# INLINE string #-}+string :: Scanner Reply+string = String <$> line++{-# INLINE error #-}+error :: Scanner Reply+error = Error <$> line++{-# INLINE integer #-}+integer :: Scanner Reply+integer = Integer <$> integral++{-# INLINE bulk #-}+bulk :: Scanner Reply+bulk = Bulk <$> do+ len <- integral+ if len < 0+ then return Nothing+ else Just <$> Scanner.take len <* eol++-- don't inline it to break the circle between reply and multi+{-# NOINLINE multi #-}+multi :: Scanner Reply+multi = Multi <$> do+ len <- integral+ if len < 0+ then return Nothing+ else Just <$> replicateM len reply++{-# INLINE integral #-}+integral :: Integral i => Scanner i+integral = do+ str <- line+ case Text.signed Text.decimal (Text.decodeUtf8 str) of+ Left err -> fail (show err)+ Right (l, _) -> return l++{-# INLINE line #-}+line :: Scanner ByteString+line = Scanner.takeWhileChar8 (/= '\r') <* eol++{-# INLINE eol #-}+eol :: Scanner ()+eol = do+ Scanner.char8 '\r'+ Scanner.char8 '\n'
+ lib/Scanner.hs view
@@ -0,0 +1,116 @@++-- | Fast not-backtracking incremental scanner for bytestrings+--+-- Unlike attoparsec or most of other parser combinator libraries,+-- scanner doesn't support backtracking. But you probably don't need it+-- anyway, at least if you need fast parser.+--+-- Scanner processes input incrementally. When more input is needed, scanner+-- returns `More` continuation. All the already processed input is discarded.++module Scanner+( Scanner+, Result (..)+, scan+, scanOnly+, scanLazy+, anyWord8+, anyChar8+, word8+, char8+, take+, takeWhile+, takeWhileChar8+, string+, skipWhile+, skipSpace+, lookAhead+, lookAheadChar8+)+where++import Scanner.Internal++import Prelude hiding (take, takeWhile)+import Data.Word+import qualified Data.Char as Char+import Data.ByteString (ByteString)+import qualified Data.ByteString as ByteString+import qualified Data.ByteString.Lazy as Lazy (ByteString)+import qualified Data.ByteString.Lazy as Lazy.ByteString+import Control.Monad+import GHC.Base (unsafeChr)++-- | Scan the complete input, without resupplying+scanOnly :: Scanner a -> ByteString -> Either String a+scanOnly s bs = go (scan s bs)+ where+ go res = case res of+ Done _ r -> Right r+ Fail _ err -> Left err+ More more -> go (more ByteString.empty)++-- | Scan lazy bytestring by resupplying scanner with chunks+scanLazy :: Scanner a -> Lazy.ByteString -> Either String a+scanLazy s lbs = go (scan s) (Lazy.ByteString.toChunks lbs)+ where+ go more chunks =+ let (chunk, chunks') = case chunks of+ [] -> (ByteString.empty, [])+ (c:cs) -> (c, cs)+ in case more chunk of+ Done _ r -> Right r+ Fail _ err -> Left err+ More more' -> go more' chunks'++-- | Consume the next 8-bit char+--+-- It fails if end of input+{-# INLINE anyChar8 #-}+anyChar8 :: Scanner Char+anyChar8 = w2c <$> anyWord8++-- | Consume the specified word or fail+{-# INLINE word8 #-}+word8 :: Word8 -> Scanner ()+word8 w = do+ w' <- anyWord8+ unless (w' == w) $+ fail "unexpected word"++-- | Consume the specified 8-bit char or fail+{-# INLINE char8 #-}+char8 :: Char -> Scanner ()+char8 = word8 . c2w++-- | Take input while the predicate is `True`+{-# INLINE takeWhileChar8 #-}+takeWhileChar8 :: (Char -> Bool) -> Scanner ByteString+takeWhileChar8 p = takeWhile (p . w2c)++-- | Return the next byte, if any, without consuming it+{-# INLINE lookAheadChar8 #-}+lookAheadChar8 :: Scanner (Maybe Char)+lookAheadChar8 = fmap w2c <$> lookAhead++-- | Skip any input while the preducate is `True`+{-# INLINE skipWhile #-}+skipWhile :: (Word8 -> Bool) -> Scanner ()+skipWhile = void . takeWhile++-- | Skip space+{-# INLINE skipSpace #-}+skipSpace :: Scanner ()+skipSpace = skipWhile isSpaceWord8++{-# INLINE isSpaceWord8 #-}+isSpaceWord8 :: Word8 -> Bool+isSpaceWord8 w = w == 32 || w <= 13++{-# INLINE w2c #-}+w2c :: Word8 -> Char+w2c = unsafeChr . fromIntegral++{-# INLINE c2w #-}+c2w :: Char -> Word8+c2w = fromIntegral . Char.ord
+ lib/Scanner/Internal.hs view
@@ -0,0 +1,167 @@+{-# LANGUAGE RankNTypes #-}+{-# OPTIONS_HADDOCK not-home #-}++-- | Scanner implementation++module Scanner.Internal+where++import Prelude hiding (takeWhile)+import Data.Word+import Data.ByteString (ByteString)+import qualified Data.ByteString as ByteString+import Control.Monad++-- | CPS scanner without backtracking+data Scanner a = Scanner+ { run :: forall r. ByteString -> Next a r -> Result r+ }++-- | Scanner continuation+type Next a r = ByteString -> a -> Result r++-- | Scanner result+data Result r+ -- | Successful result with the rest of input+ = Done ByteString r++ -- | Scanner failed with rest of input and error message+ | Fail ByteString String++ -- | Need more input+ | More (ByteString -> Result r)++-- | Run scanner with the input+scan :: Scanner r -> ByteString -> Result r+scan s bs = run s bs Done++instance Functor Scanner where+ {-# INLINE fmap #-}+ fmap f (Scanner s) = Scanner $ \bs next ->+ s bs $ \bs' a ->+ next bs' (f a)++instance Applicative Scanner where+ {-# INLINE pure #-}+ pure = return+ {-# INLINE (<*>) #-}+ (<*>) = ap++ {-# INLINE (*>) #-}+ (*>) = (>>)++ {-# INLINE (<*) #-}+ s1 <* s2 = s1 >>= \a -> s2 >> return a++instance Monad Scanner where+ {-# INLINE return #-}+ return a = Scanner $ \bs next ->+ next bs a++ {-# INLINE (>>=) #-}+ s1 >>= s2 = Scanner $ \bs next ->+ run s1 bs $ \bs' a ->+ run (s2 a) bs' next++ {-# INLINE fail #-}+ fail err = Scanner $ \bs _ ->+ Fail bs err++-- | Consume the next word+--+-- It fails if end of input+{-# INLINE anyWord8 #-}+anyWord8 :: Scanner Word8+anyWord8 = Scanner $ \bs next ->+ case ByteString.uncons bs of+ Just (c, bs') -> next bs' c+ _ -> More $ \bs' -> slowPath bs' next+ where+ slowPath bs next =+ case ByteString.uncons bs of+ Just (c, bs') -> next bs' c+ _ -> Fail ByteString.empty "No more input"++-- | Take input while the predicate is `True`+{-# INLINE takeWhile #-}+takeWhile :: (Word8 -> Bool) -> Scanner ByteString+takeWhile p = Scanner $ \bs next ->+ let (l, r) = ByteString.span p bs+ in if ByteString.null r+ then More $ \bs' ->+ if ByteString.null bs'+ then next ByteString.empty l+ else run (slowPath l) bs' next+ else next r l+ where+ slowPath l = go [l]+ go res = do+ chunk <- takeChunk+ done <- endOfInput+ if done || ByteString.null chunk+ then return . ByteString.concat . reverse $ (chunk : res)+ else go (chunk : res)+ takeChunk = Scanner $ \bs next ->+ let (l, r) = ByteString.span p bs+ in next r l++-- | Take the specified number of bytes+{-# INLINE take #-}+take :: Int -> Scanner ByteString+take n = Scanner $ \bs next ->+ let len = ByteString.length bs+ in if len >= n+ then let (l, r) = ByteString.splitAt n bs+ in next r l+ else More $ \bs' ->+ if ByteString.null bs'+ then Fail ByteString.empty "No more input"+ else run (slowPath bs len) bs' next+ where+ slowPath bs len = go [bs] (n - len)+ go res 0 = return . ByteString.concat . reverse $ res+ go res i = Scanner $ \bs next ->+ let len = ByteString.length bs+ in if len >= i+ then let (l, r) = ByteString.splitAt i bs+ in next r (ByteString.concat . reverse $ (l : res))+ else More $ \bs' ->+ if ByteString.null bs'+ then Fail ByteString.empty "No more input"+ else run (go (bs : res) (i - len)) bs' next++-- | Returns `True` when there is no more input+{-# INLINE endOfInput #-}+endOfInput :: Scanner Bool+endOfInput = Scanner $ \bs next ->+ if ByteString.null bs+ then More $ \bs' -> next bs' (ByteString.null bs')+ else next bs False++-- | Consume the specified string+--+-- Warning: it is not optimized yet, so for for small string it is better+-- to consume it byte-by-byte using `Scanner.word8`+{-# INLINE string #-}+string :: ByteString -> Scanner ()+string str = Scanner $ \bs next ->+ let bsL = ByteString.length bs+ strL = ByteString.length str+ in if ByteString.isPrefixOf str bs+ then next (ByteString.drop strL bs) ()+ else if ByteString.isPrefixOf bs str+ then More $ \bs' -> run (string (ByteString.drop bsL str)) bs' next+ else Fail bs "unexpected input"++-- | Return the next byte, if any, without consuming it+{-# INLINE lookAhead #-}+lookAhead :: Scanner (Maybe Word8)+lookAhead = Scanner $ \bs next ->+ case ByteString.uncons bs of+ Just (c, _) -> next bs (Just c)+ _ -> More $ \bs' -> slowPath bs' next+ where+ slowPath bs next =+ case ByteString.uncons bs of+ Just (c, _) -> next bs (Just c)+ _ -> next ByteString.empty Nothing
+ scanner.cabal view
@@ -0,0 +1,55 @@+name: scanner+version: 0.1+synopsis: Fast non-backtracking incremental combinator parsing for bytestrings+homepage: https://github.com/Yuras/scanner+license: BSD3+license-file: LICENSE+author: Yuras Shumovich+maintainer: shumovichy@gmail.com+copyright: (c) Yuras Shumovich 2016+category: Parsing+build-type: Simple+cabal-version: >=1.10+extra-source-files: README.md bench/bench.png+description: Parser combinator library designed to be fast. It doesn't+ support backtracking.++source-repository head+ type: git+ location: git@github.com:Yuras/scanner.git++library+ exposed-modules: Scanner+ Scanner.Internal+ other-modules: Prelude+ Data.Either+ build-depends: base <5+ , bytestring+ hs-source-dirs: lib, compat+ ghc-options: -O2+ default-language: Haskell2010++test-suite spec+ type: exitcode-stdio-1.0+ hs-source-dirs: spec, compat+ main-is: spec.hs+ build-depends: base+ , bytestring+ , hspec+ , scanner+ default-language: Haskell2010++benchmark bench+ type: exitcode-stdio-1.0+ hs-source-dirs: bench, examples, compat+ main-is: bench.hs+ other-modules: Redis.Reply+ Redis.Atto+ Redis.Scanner+ default-language: Haskell2010+ build-depends: base+ , bytestring+ , text+ , attoparsec+ , criterion+ , scanner
+ spec/spec.hs view
@@ -0,0 +1,131 @@+{-# LANGUAGE OverloadedStrings #-}++module Main+( main+)+where++import Scanner++import Prelude hiding (take, takeWhile)+import Data.Either+import qualified Data.ByteString as ByteString+import qualified Data.ByteString.Lazy as Lazy.ByteString+import Test.Hspec++main :: IO ()+main = hspec $ do+ anyWord8Spec+ stringSpec+ takeSpec+ takeWhileSpec+ lookAheadSpec++anyWord8Spec :: Spec+anyWord8Spec = describe "anyWord8" $ do+ it "should return the current byte" $ do+ let bs = ByteString.pack [42, 43]+ scanOnly anyWord8 bs `shouldBe` Right 42++ it "should consume the current byte" $ do+ let bs = ByteString.pack [42, 43]+ scanOnly (anyWord8 *> anyWord8) bs `shouldBe` Right 43++ let bs' = Lazy.ByteString.fromChunks+ [ ByteString.pack [42]+ , ByteString.pack [43]+ , ByteString.pack [44]+ ]+ scanLazy (anyWord8 *> anyWord8 *> anyWord8) bs' `shouldBe` Right 44++ it "should ask for more input" $ do+ let bs = Lazy.ByteString.fromChunks+ [ ByteString.pack [42]+ , ByteString.pack [43]+ ]+ scanLazy (anyWord8 *> anyWord8) bs `shouldBe` Right 43++ it "should fail on end of input" $ do+ let bs = ByteString.empty+ scanOnly anyWord8 bs `shouldSatisfy` isLeft++stringSpec :: Spec+stringSpec = describe "string" $ do+ it "should consume the string" $ do+ let bs = "hello world"+ scanOnly (string "hello" *> anyWord8) bs `shouldBe` Right 32++ it "should ask for more input" $ do+ let bs = Lazy.ByteString.fromChunks+ [ "hel"+ , "lo"+ ]+ scanLazy (string "hello") bs `shouldBe` Right ()++ it "should fail on wrong input" $ do+ let bs = "helo world"+ scanOnly (string "hello") bs `shouldSatisfy` isLeft++takeSpec :: Spec+takeSpec = describe "take" $ do+ it "should return the first n bytes" $ do+ let bs = "hello world"+ scanOnly (take 5) bs `shouldBe` Right "hello"++ it "should ask for more input" $ do+ let bs = Lazy.ByteString.fromChunks+ [ "he"+ , "l"+ , "lo world"+ ]+ scanLazy (take 5) bs `shouldBe` Right "hello"++ it "should fail on end of input" $ do+ let bs = "hell"+ scanOnly (take 5) bs `shouldSatisfy` isLeft++ let bs' = Lazy.ByteString.fromChunks+ [ "he"+ , "l"+ , "l"+ ]+ scanLazy (take 5) bs' `shouldSatisfy` isLeft++takeWhileSpec :: Spec+takeWhileSpec = describe "takeWhile" $ do+ it "should return bytes according to the predicate" $ do+ let bs = "hello world"+ scanOnly (takeWhile (/= 32)) bs `shouldBe` Right "hello"++ it "should ask for more input" $ do+ let bs = Lazy.ByteString.fromChunks+ [ "he"+ , "l"+ , "lo world"+ ]+ scanLazy (takeWhile (/= 32)) bs `shouldBe` Right "hello"++ it "should return everything is predicate where becomes False" $ do+ let bs = "hello"+ scanOnly (takeWhile (/= 32)) bs `shouldBe` Right "hello"++lookAheadSpec :: Spec+lookAheadSpec = describe "lookAhead" $ do+ it "should return the next byte" $ do+ let bs = ByteString.pack [42, 43]+ scanOnly lookAhead bs `shouldBe` Right (Just 42)++ it "should return Nothing on end of input" $ do+ let bs = ByteString.empty+ scanOnly lookAhead bs `shouldBe` Right Nothing++ it "should not consume input" $ do+ let bs = ByteString.pack [42, 43]+ scanOnly (lookAhead *> anyWord8) bs `shouldBe` Right 42++ it "should ask for more input" $ do+ let bs = Lazy.ByteString.fromChunks+ [ ByteString.pack [42]+ , ByteString.pack [43]+ ]+ scanLazy (anyWord8 *> lookAhead) bs `shouldBe` Right (Just 43)