Skip to content
Open
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
36 changes: 26 additions & 10 deletions src/Data/List/Linear.hs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE LinearTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE NoImplicitPrelude #-}
Expand Down Expand Up @@ -354,18 +354,34 @@ zip3 :: (Consumable a, Consumable b, Consumable c) => [a] %1 -> [b] %1 -> [c] %1
zip3 = zipWith3 (,,)

zipWith :: (Consumable a, Consumable b) => (a %1 -> b %1 -> c) -> [a] %1 -> [b] %1 -> [c]
zipWith f xs ys =
zipWith' f xs ys & \(ret, leftovers) ->
leftovers `lseq` ret
zipWith f =
zipWithk f (:) [] consume2 consume2
where
consume2 :: forall x y z. (Consumable x, Consumable y) => x %1 -> y %1 -> [z]
consume2 x y = x `lseq` y `lseq` []
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In zipWith3 you do (x, y) `lseq` [] instead, any reason to prefer one or the other?


-- | Same as 'zipWith', but returns the leftovers instead of consuming them.
-- Because the leftovers are returned at toplevel, @zipWith'@ is pretty strict:
-- forcing the first cons cell of the returned list forces all the recursive
-- calls.
zipWith' :: (a %1 -> b %1 -> c) -> [a] %1 -> [b] %1 -> ([c], Maybe (Either (NonEmpty a) (NonEmpty b)))
zipWith' _ [] [] = ([], Nothing)
zipWith' _ (a : as) [] = ([], Just (Left (a :| as)))
zipWith' _ [] (b : bs) = ([], Just (Right (b :| bs)))
zipWith' f (a : as) (b : bs) =
case zipWith' f as bs of
(cs, rest) -> (f a b : cs, rest)
zipWith' f =
zipWithk
f
(\c !(cs, rest) -> ((c : cs), rest))
([], Nothing)
(\a as -> ([], Just (Left (a :| as))))
(\b bs -> ([], Just (Right (b :| bs))))

zipWithk :: forall r a b c. (a %1 -> b %1 -> c) -> (c %1 -> r %1 -> r) -> r -> (a %1 -> [a] %1 -> r) -> (b %1 -> [b] %1 -> r) -> [a] %1 -> [b] %1 -> r
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
zipWithk :: forall r a b c. (a %1 -> b %1 -> c) -> (c %1 -> r %1 -> r) -> r -> (a %1 -> [a] %1 -> r) -> (b %1 -> [b] %1 -> r) -> [a] %1 -> [b] %1 -> r
zipWithK :: forall r a b c. (a %1 -> b %1 -> c) -> (c %1 -> r %1 -> r) -> r -> (a %1 -> [a] %1 -> r) -> (b %1 -> [b] %1 -> r) -> [a] %1 -> [b] %1 -> r

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might be better to split the argument types on several lines and document them, if this function is exported? Which I think it should be.

zipWithk f cons nil lefta leftb =
go
where
go :: [a] %1 -> [b] %1 -> r
go [] [] = nil
go (a : as) [] = lefta a as
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not entierely clear why the left{a,b} functions are taking head and tail as separate arguments. I guess the motivation is to make a NonEmpty list in zipWith' without having to call NonEmpty.fromList?

go [] (b : bs) = leftb b bs
go (a : as) (b : bs) = cons (f a b) (go as bs)

zipWith3 :: forall a b c d. (Consumable a, Consumable b, Consumable c) => (a %1 -> b %1 -> c %1 -> d) -> [a] %1 -> [b] %1 -> [c] %1 -> [d]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does zipWith3 doesn't get the same treatment with a zipWithK3?

zipWith3 _ [] ys zs = (ys, zs) `lseq` []
Expand Down
21 changes: 20 additions & 1 deletion test/Test/Data/List.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
module Test.Data.List (listTests) where

import qualified Data.List.Linear as List
import qualified Data.Num.Linear as Num
import Hedgehog
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range
Expand All @@ -17,7 +18,9 @@ listTests =
testGroup
"List tests"
[ testPropertyNamed "take n ++ drop n = id" "take_drop" take_drop,
testPropertyNamed "length . take n = const n" "take_length" take_length
testPropertyNamed "length . take n = const n" "take_length" take_length,
testPropertyNamed "zipWith is lazy" "zipWith_lazy" zipWith_lazy,
testPropertyNamed "zipWith3 is lazy" "zipWith3_lazy" zipWith3_lazy
]

take_drop :: Property
Expand All @@ -41,3 +44,19 @@ take_length = property $ do
False -> do
annotate "Prelude.length xs < n"
Prelude.length (List.take n xs) === Prelude.length xs

zipWith_lazy :: Property
zipWith_lazy = property $ do
_ <- eval $ Prelude.head xs
Prelude.return ()
where
xs :: [Word]
xs = List.zipWith (Num.+) (0 : error "bottom") [0 .. 42]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the motivation for using static bounds for ranges instead of property-supplied ones?
(same below)


zipWith3_lazy :: Property
zipWith3_lazy = property $ do
_ <- eval $ Prelude.head xs
Prelude.return ()
where
xs :: [Word]
xs = List.zipWith3 (\x y z -> x Num.+ y Num.+ z) (0 : error "bottom") [0 .. 42] [0 .. 57]