Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make D.B.Lazy.zipWith properly lazy #668

Merged
merged 2 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 18 additions & 11 deletions Data/ByteString/Lazy.hs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
{-# OPTIONS_GHC -fno-warn-incomplete-patterns #-}
{-# OPTIONS_HADDOCK prune #-}
{-# LANGUAGE Trustworthy #-}

Expand Down Expand Up @@ -1095,6 +1094,7 @@ splitWith p (Chunk c0 cs0) = comb [] (S.splitWith p c0) cs0
comb acc [s] Empty = [revChunks (s:acc)]
comb acc [s] (Chunk c cs) = comb (s:acc) (S.splitWith p c) cs
comb acc (s:ss) cs = revChunks (s:acc) : comb [] ss cs
comb _ [] _ = error "Strict splitWith returned [] for nonempty input"
{-# INLINE splitWith #-}

-- | /O(n)/ Break a 'ByteString' into pieces separated by the byte
Expand Down Expand Up @@ -1122,6 +1122,7 @@ split w (Chunk c0 cs0) = comb [] (S.split w c0) cs0
comb acc [s] Empty = [revChunks (s:acc)]
comb acc [s] (Chunk c cs) = comb (s:acc) (S.split w c) cs
comb acc (s:ss) cs = revChunks (s:acc) : comb [] ss cs
comb _ [] _ = error "Strict split returned [] for nonempty input"
{-# INLINE split #-}

-- | The 'group' function takes a ByteString and returns a list of
Expand Down Expand Up @@ -1441,16 +1442,22 @@ zipWith _ Empty _ = []
zipWith _ _ Empty = []
zipWith f (Chunk a as) (Chunk b bs) = go a as b bs
where
go x xs y ys = f (S.unsafeHead x) (S.unsafeHead y)
: to (S.unsafeTail x) xs (S.unsafeTail y) ys

to x Empty _ _ | S.null x = []
to _ _ y Empty | S.null y = []
to x xs y ys | not (S.null x)
&& not (S.null y) = go x xs y ys
to x xs _ (Chunk y' ys) | not (S.null x) = go x xs y' ys
to _ (Chunk x' xs) y ys | not (S.null y) = go x' xs y ys
to _ (Chunk x' xs) _ (Chunk y' ys) = go x' xs y' ys
-- This loop is written in a slightly awkward way but ensures we
-- don't have to allocate any 'Chunk' objects to pass to a recursive
-- call. We have in some sense performed SpecConstr manually.
go !x xs !y ys = let
-- Creating a thunk for reading one byte would
-- be wasteful, so we evaluate these eagerly.
-- See also #558 for a similar issue with uncons.
!xHead = S.unsafeHead x
!yHead = S.unsafeHead y
in f xHead yHead : to (S.unsafeTail x) xs (S.unsafeTail y) ys

to !x xs !y ys
| Chunk x' xs' <- chunk x xs
, Chunk y' ys' <- chunk y ys
= go x' xs' y' ys'
| otherwise = []

-- | A specialised version of `zipWith` for the common case of a
-- simultaneous map over two ByteStrings, to build a 3rd.
Expand Down
25 changes: 25 additions & 0 deletions tests/Properties/ByteString.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
-- License : BSD-style

{-# LANGUAGE CPP #-}

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE ScopedTypeVariables #-}
Expand Down Expand Up @@ -603,6 +604,16 @@ tests =
\f x y -> (B.zipWith f x y :: [Int]) === zipWith f (B.unpack x) (B.unpack y)
, testProperty "packZipWith" $
\f x y -> B.unpack (B.packZipWith ((toElem .) . f) x y) === zipWith ((toElem .) . f) (B.unpack x) (B.unpack y)
# ifdef BYTESTRING_LAZY
-- Don't use (===) in these laziness tests:
-- We don't want printing the test case to fail!
, testProperty "zip is lazy" $ lazyZipTest $
\x y -> B.zip x y == zip (B.unpack x) (B.unpack y)
, testProperty "zipWith is lazy" $ \f -> lazyZipTest $
\x y -> (B.zipWith f x y :: [Int]) == zipWith f (B.unpack x) (B.unpack y)
, testProperty "packZipWith is lazy" $ \f -> lazyZipTest $
\x y -> B.unpack (B.packZipWith ((toElem .) . f) x y) == zipWith ((toElem .) . f) (B.unpack x) (B.unpack y)
# endif
, testProperty "unzip" $
\(fmap (toElem *** toElem) -> xs) -> (B.unpack *** B.unpack) (B.unzip xs) === unzip xs
#endif
Expand Down Expand Up @@ -796,3 +807,17 @@ readIntegerUnsigned xs = case readMaybe ys of
where
(ys, zs) = span isDigit xs
#endif

#ifdef BYTESTRING_LAZY
lazyZipTest
:: Testable prop
=> (BYTESTRING_TYPE -> BYTESTRING_TYPE -> prop)
-> BYTESTRING_TYPE -> BYTESTRING_TYPE -> Property
lazyZipTest fun = \x0 y0 -> let
msg = "Input chunks are: " ++ show (B.toChunks x0, B.toChunks y0)
(x, y) | B.length x0 <= B.length y0
= (x0, y0 <> error "too strict")
| otherwise
= (x0 <> error "too strict", y0)
in counterexample msg (fun x y)
#endif
2 changes: 2 additions & 0 deletions tests/QuickCheckUtils.hs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ instance Arbitrary L.ByteString where
(sizedByteString
(n `div` numChunks))

shrink = map L.fromChunks . shrink . L.toChunks

instance CoArbitrary L.ByteString where
coarbitrary s = coarbitrary (L.unpack s)

Expand Down
Loading