on master help sign in
Refactor Stream.Top, move transformers, containers ops

Keep only those operation that depend only on "base" package, move out operations that depend on transformers, containers to Stream.Extra. Add crossWith for cross product operation Rename, refactor, document set-like, join ops

commita6e62aec7e31e9210840c214a23ddd3672d96d36 authorHarendra Kumar <harendra@composewell.com> dateSat 17 Dec 2022, 00:50 (4 years ago) parenta7512bc
7 files changed+365−289
benchmark/Streamly/Benchmark/Data/Stream/Expand.hs+16−16 core/src/Streamly/Data/Stream.hs+11 core/src/Streamly/Internal/Data/Stream/Top.hs+134−253 core/src/Streamly/Internal/Data/Stream/Type.hs+39 core/src/Streamly/Internal/Data/Unfold.hs+6 core/src/Streamly/Internal/Data/Unfold/Type.hs+9−6 src/Streamly/Internal/Data/Stream/Extra.hs+150−14
benchmark/Streamly/Benchmark/Data/Stream/Expand.hs+16−16
@@ -465,22 +465,22 @@ o_n_heap_buffering :: Int -> [Benchmark]
465465o_n_heap_buffering value =
466466 [ bgroup "buffered"
467467 [
468− benchIOSrc1 "joinInner (sqrtVal)"
469− $ joinWith S.joinInner sqrtVal
470− , benchIOSrc1 "joinInnerMap"
471− $ joinMapWith S.joinInnerMap halfVal
472− , benchIOSrc1 "joinLeft (sqrtVal)"
473− $ joinWith S.joinLeft sqrtVal
474− , benchIOSrc1 "joinLeftMap "
475− $ joinMapWith S.joinLeftMap halfVal
476− , benchIOSrc1 "joinOuter (sqrtVal)"
477− $ joinWith S.joinOuter sqrtVal
478− , benchIOSrc1 "joinOuterMap"
479− $ joinMapWith S.joinOuterMap halfVal
480− , benchIOSrc1 "intersectBy (sqrtVal)"
481− $ joinWith S.intersectBy sqrtVal
482− , benchIOSrc1 "intersectBySorted"
483− $ joinMapWith (S.intersectBySorted compare) halfVal
468+ benchIOSrc1 "joinInnerGeneric (sqrtVal)"
469+ $ joinWith S.joinInnerGeneric sqrtVal
470+ , benchIOSrc1 "joinInner"
471+ $ joinMapWith S.joinInner halfVal
472+ , benchIOSrc1 "joinLeftGeneric (sqrtVal)"
473+ $ joinWith S.joinLeftGeneric sqrtVal
474+ , benchIOSrc1 "joinLeft "
475+ $ joinMapWith S.joinLeft halfVal
476+ , benchIOSrc1 "joinOuterGeneric (sqrtVal)"
477+ $ joinWith S.joinOuterGeneric sqrtVal
478+ , benchIOSrc1 "joinOuter"
479+ $ joinMapWith S.joinOuter halfVal
480+ , benchIOSrc1 "filterInStreamGenericBy (sqrtVal)"
481+ $ joinWith S.filterInStreamGenericBy sqrtVal
482+ , benchIOSrc1 "filterInStreamAscBy"
483+ $ joinMapWith (S.filterInStreamAscBy compare) halfVal
484484 ]
485485 ]
486486
core/src/Streamly/Data/Stream.hs+11
@@ -388,6 +388,17 @@ module Streamly.Data.Stream
388388 -- , zipWith2
389389 -- , zipWithM2
390390
391+ -- ** Cross Product
392+ -- XXX The argument order in this operation is such that it seems we are
393+ -- transforming the first stream using the second stream because the second
394+ -- stream is evaluated many times or buffered and better be finite, first
395+ -- stream could potentially be infinite. In the tradition of using the
396+ -- transformed stream at the end we can have a flipped version called
397+ -- "crossMap" or "nestWith".
398+ , crossWith
399+ -- , cross
400+ -- , joinInner
401+
391402 -- * Unfold Each
392403 , unfoldMany
393404 , intercalate
core/src/Streamly/Internal/Data/Stream/Top.hs+134−253
@@ -21,42 +21,42 @@ module Streamly.Internal.Data.Stream.Top
2121 -- * Nesting
2222 -- ** Set like operations
2323 -- | These are not exactly set operations because streams are not
24− -- necessarily sets, they may have duplicated elements.
25− , intersectBy
26− , intersectBySorted
27− , differenceBy
28− , mergeDifferenceBy
29− , unionBy
30− , mergeUnionBy
24+ -- necessarily sets, they may have duplicated elements. These operations
25+ -- are generic i.e. they work on streams of unconstrained types, therefore,
26+ -- they have quadratic performance characterstics. For better performance
27+ -- using Set structures see the Streamly.Internal.Data.Stream.Container
28+ -- module.
29+ , filterInStreamGenericBy
30+ , deleteInStreamGenericBy
31+ , unionWithStreamGenericBy
32+
33+ -- ** Set like operations on sorted streams
34+ , filterInStreamAscBy
35+ , deleteInStreamAscBy
36+ , unionWithStreamAscBy
3137
3238 -- ** Join operations
33− , crossJoin
34− , joinInner
35− , joinInnerMerge
36− , joinLeft
37− , mergeLeftJoin
38− , joinOuter
39− , mergeOuterJoin
39+ , joinInnerGeneric
40+
41+ -- * Joins on sorted stream
42+ , joinInnerAscBy
43+ , joinLeftAscBy
44+ , joinOuterAscBy
4045 )
4146where
4247
4348#include "inline.hs"
4449
4550import Control.Monad.IO.Class (MonadIO(..))
46−import Control.Monad.Trans.State.Strict (get, put)
47−import Data.Function ((&))
4851import Data.IORef (newIORef, readIORef, modifyIORef')
49−import Data.Maybe (isJust)
52+import Streamly.Internal.Data.Fold.Type (Fold)
5053import Streamly.Internal.Data.Stream.Common ()
51−import Streamly.Internal.Data.Stream.Cross (CrossStream (..))
52−import Streamly.Internal.Data.Stream.Type (Stream, fromStreamD, toStreamD)
54+import Streamly.Internal.Data.Stream.Type
55+ (Stream, fromStreamD, toStreamD, cross)
5356
5457import qualified Data.List as List
55−import qualified Streamly.Internal.Data.Array.Generic as Array
56−import qualified Streamly.Internal.Data.Array.Mut.Type as MA
5758import qualified Streamly.Internal.Data.Fold as Fold
5859import qualified Streamly.Internal.Data.Parser as Parser
59−import qualified Streamly.Internal.Data.Stream.Transformer as Stream
6060import qualified Streamly.Internal.Data.Stream.Eliminate as Stream
6161import qualified Streamly.Internal.Data.Stream.Generate as Stream
6262import qualified Streamly.Internal.Data.Stream.Expand as Stream
@@ -140,59 +140,37 @@ sortBy cmp =
140140-- source changes during iterations. We can also use an Unfold instead of
141141-- stream. We probably need a way to distinguish streams that can be read
142142-- mutliple times without any interference (e.g. unfolding a stream using an
143−-- immutable handle would work i.e. using pread/pwrite instead of maintianing
143+-- immutable handle would work i.e. using pread/pwrite instead of maintaining
144144-- an offset in the handle).
145145
146−-- XXX We can do this concurrently.
147−--
148−-- | This is the same as 'Streamly.Internal.Data.Unfold.outerProduct' but less
149−-- efficient.
150−--
151−-- The second stream is evaluated multiple times. If the second stream is
152−-- consume-once stream then it can be cached in an 'Data.Array.Array' before
153−-- calling this function. Caching may also improve performance if the stream is
154−-- expensive to evaluate.
155−--
156−-- Time: O(m x n)
157−--
158−-- /Pre-release/
159−{-# INLINE crossJoin #-}
160−crossJoin :: Monad m => Stream m a -> Stream m b -> Stream m (a, b)
161−crossJoin s1 s2 = unCrossStream $ do
162− -- XXX use concatMap instead?
163− a <- CrossStream s1
164− b <- CrossStream s2
165− return (a, b)
166−
167146-- XXX We can do this concurrently.
168147-- XXX If the second stream is sorted and passed as an Array we could use
169148-- binary search if we have an Ord instance or Ordering returning function. The
170149-- time complexity would then become (m x log n).
150+
151+-- | Like 'cross' but emits only those tuples where @a == b@ using the
152+-- supplied equality predicate.
171153--
172−-- | For all elements in @Stream m a@, for all elements in @Stream m b@ if @a@ and @b@
173−-- are equal by the given equality pedicate then return the tuple (a, b).
174−--
175−-- The second stream is evaluated multiple times. If the stream is a
176−-- consume-once stream then the caller should cache it (e.g. in a
177−-- 'Data.Array.Array') before calling this function. Caching may also improve
178−-- performance if the stream is expensive to evaluate.
154+-- Definition:
179155--
180−-- For space efficiency use the smaller stream as the second stream.
156+-- >>> joinInnerGeneric eq s1 s2 = Stream.filter (\(a, b) -> a `eq` b) $ Stream.cross s1 s2
181157--
182−-- You should almost always use joinInnerMap instead of joinInner. joinInnerMap
183−-- is an order of magnitude faster. joinInner may be used when the second
184−-- stream is generated from a seed, therefore, need not be stored in memory and
185−-- the amount of memory it takes is a concern.
158+-- You should almost always prefer @joinInnerOrd@ over 'joinInnerGeneric' if
159+-- possible. @joinInnerOrd@ is an order of magnitude faster but may take more
160+-- space for caching the second stream.
186161--
187−-- Space: O(n) assuming the second stream is cached in memory.
162+-- See 'Streamly.Internal.Data.Unfold.joinInnerGeneric' for a much faster fused
163+-- alternative.
188164--
189165-- Time: O(m x n)
190166--
191167-- /Pre-release/
192−{-# INLINE joinInner #-}
193−joinInner :: Monad m =>
168+{-# INLINE joinInnerGeneric #-}
169+joinInnerGeneric :: Monad m =>
194170 (a -> b -> Bool) -> Stream m a -> Stream m b -> Stream m (a, b)
195−joinInner eq s1 s2 = do
171+joinInnerGeneric eq s1 s2 = Stream.filter (\(a, b) -> a `eq` b) $ cross s1 s2
172+{-
173+joinInnerGeneric eq s1 s2 = do
196174 -- ConcatMap works faster than bind
197175 Stream.concatMap (\a ->
198176 Stream.concatMap (\b ->
@@ -201,165 +179,46 @@ joinInner eq s1 s2 = do
201179 else Stream.nil
202180 ) s2
203181 ) s1
182+-}
204183
205−-- | Like 'joinInner' but works only on sorted streams.
184+-- | A more efficient 'joinInner' for sorted streams.
206185--
207186-- Space: O(1)
208187--
209188-- Time: O(m + n)
210189--
211190-- /Unimplemented/
212−{-# INLINE joinInnerMerge #-}
213−joinInnerMerge ::
191+{-# INLINE joinInnerAscBy #-}
192+joinInnerAscBy ::
214193 (a -> b -> Ordering) -> Stream m a -> Stream m b -> Stream m (a, b)
215−joinInnerMerge = undefined
194+joinInnerAscBy = undefined
216195
217−-- XXX We can do this concurrently.
218−-- XXX If the second stream is sorted and passed as an Array or a seek capable
219−-- stream then we could use binary search if we have an Ord instance or
220−-- Ordering returning function. The time complexity would then become (m x log
221−-- n).
222−--
223−-- | For all elements in @Stream m a@, for all elements in @Stream m b@ if @a@ and @b@
224−-- are equal then return the tuple @(a, Just b)@. If @a@ is not present in @t
225−-- m b@ then return @(a, Nothing)@.
226−--
227−-- The second stream is evaluated multiple times. If the stream is a
228−-- consume-once stream then the caller should cache it in an 'Data.Array.Array'
229−-- before calling this function. Caching may also improve performance if the
230−-- stream is expensive to evaluate.
231−--
232−-- @
233−-- rightJoin = flip joinLeft
234−-- @
235−--
236−-- Space: O(n) assuming the second stream is cached in memory.
237−--
238−-- Time: O(m x n)
239−--
240−-- /Unimplemented/
241−{-# INLINE joinLeft #-}
242−joinLeft :: Monad m =>
243− (a -> b -> Bool) -> Stream m a -> Stream m b -> Stream m (a, Maybe b)
244−joinLeft eq s1 s2 = Stream.evalStateT (return False) $ unCrossStream $ do
245− a <- CrossStream (Stream.liftInner s1)
246− -- XXX should we use StreamD monad here?
247− -- XXX Is there a better way to perform some action at the end of a loop
248− -- iteration?
249− CrossStream (Stream.fromEffect $ put False)
250− let final = Stream.concatEffect $ do
251− r <- get
252− if r
253− then pure Stream.nil
254− else pure (Stream.fromPure Nothing)
255− b <- CrossStream (fmap Just (Stream.liftInner s2) <> final)
256− case b of
257− Just b1 ->
258− if a `eq` b1
259− then do
260− CrossStream (Stream.fromEffect $ put True)
261− return (a, Just b1)
262− else CrossStream Stream.nil
263− Nothing -> return (a, Nothing)
264−
265−-- | Like 'joinLeft' but works only on sorted streams.
196+-- | A more efficient 'joinLeft' for sorted streams.
266197--
267198-- Space: O(1)
268199--
269200-- Time: O(m + n)
270201--
271202-- /Unimplemented/
272−{-# INLINE mergeLeftJoin #-}
273−mergeLeftJoin :: -- Monad m =>
203+{-# INLINE joinLeftAscBy #-}
204+joinLeftAscBy :: -- Monad m =>
274205 (a -> b -> Ordering) -> Stream m a -> Stream m b -> Stream m (a, Maybe b)
275−mergeLeftJoin _eq _s1 _s2 = undefined
206+joinLeftAscBy _eq _s1 _s2 = undefined
276207
277−-- XXX We can do this concurrently.
278−--
279−-- | For all elements in @t m a@, for all elements in @t m b@ if @a@ and @b@
280−-- are equal by the given equality pedicate then return the tuple (Just a, Just
281−-- b). If @a@ is not found in @t m b@ then return (a, Nothing), return
282−-- (Nothing, b) for vice-versa.
283−--
284−-- For space efficiency use the smaller stream as the second stream.
285−--
286−-- Space: O(n)
287−--
288−-- Time: O(m x n)
289−--
290−-- /Pre-release/
291−{-# INLINE joinOuter #-}
292−joinOuter :: MonadIO m =>
293− (a -> b -> Bool)
294− -> Stream m a
295− -> Stream m b
296− -> Stream m (Maybe a, Maybe b)
297−joinOuter eq s1 s =
298− Stream.concatEffect $ do
299− inputArr <- Array.fromStream s
300− let len = Array.length inputArr
301− foundArr <-
302− Stream.fold
303− (MA.writeN len)
304− (Stream.fromList (Prelude.replicate len False))
305− return $ go inputArr foundArr <> leftOver inputArr foundArr
306−
307− where
308−
309− leftOver inputArr foundArr =
310− let stream1 = Array.read inputArr
311− stream2 = Stream.unfold MA.reader foundArr
312− in Stream.filter
313− isJust
314− ( Stream.zipWith (\x y ->
315− if y
316− then Nothing
317− else Just (Nothing, Just x)
318− ) stream1 stream2
319− ) & Stream.catMaybes
320−
321− evalState = Stream.evalStateT (return False) . unCrossStream
322−
323− go inputArr foundArr = evalState $ do
324− a <- CrossStream (Stream.liftInner s1)
325− -- XXX should we use StreamD monad here?
326− -- XXX Is there a better way to perform some action at the end of a loop
327− -- iteration?
328− CrossStream (Stream.fromEffect $ put False)
329− let final = Stream.concatEffect $ do
330− r <- get
331− if r
332− then pure Stream.nil
333− else pure (Stream.fromPure Nothing)
334− (i, b) <-
335− let stream = Array.read inputArr
336− in CrossStream
337− (Stream.indexed $ fmap Just (Stream.liftInner stream) <> final)
338−
339− case b of
340− Just b1 ->
341− if a `eq` b1
342− then do
343− CrossStream (Stream.fromEffect $ put True)
344− MA.putIndex i True foundArr
345− return (Just a, Just b1)
346− else CrossStream Stream.nil
347− Nothing -> return (Just a, Nothing)
348−
349−-- | Like 'joinOuter' but works only on sorted streams.
208+-- | A more efficient 'joinOuter' for sorted streams.
350209--
351210-- Space: O(1)
352211--
353212-- Time: O(m + n)
354213--
355214-- /Unimplemented/
356−{-# INLINE mergeOuterJoin #-}
357−mergeOuterJoin :: -- Monad m =>
215+{-# INLINE joinOuterAscBy #-}
216+joinOuterAscBy :: -- Monad m =>
358217 (a -> b -> Ordering)
359218 -> Stream m a
360219 -> Stream m b
361220 -> Stream m (Maybe a, Maybe b)
362−mergeOuterJoin _eq _s1 _s2 = undefined
221+joinOuterAscBy _eq _s1 _s2 = undefined
363222
364223------------------------------------------------------------------------------
365224-- Set operations (special joins)
@@ -369,19 +228,39 @@ mergeOuterJoin _eq _s1 _s2 = undefined
369228-- the best would be to use an Array with linear search. If the second stream
370229-- is sorted we can also use a binary search, using Ord constraint.
371230
372−-- | 'intersectBy' is essentially a filtering operation that retains only those
373−-- elements in the first stream that are present in the second stream.
231+-- | Keep only those elements in the second stream that are present in the
232+-- first stream too. The first stream is folded to a container using the
233+-- supplied fold and then the elements in the container are looked up using the
234+-- supplied lookup function.
374235--
375−-- >>> Stream.fold Fold.toList $ Stream.intersectBy (==) (Stream.fromList [1,2,2,4]) (Stream.fromList [2,1,1,3])
376−-- [1,2,2]
236+-- The first stream must be finite and must not block.
237+{-# INLINE filterStreamWith #-}
238+filterStreamWith :: Monad m =>
239+ Fold m a (f a)
240+ -> (a -> f a -> Bool)
241+ -> Stream m a
242+ -> Stream m a
243+ -> Stream m a
244+filterStreamWith fld member s1 s2 =
245+ Stream.concatEffect
246+ $ do
247+ xs <- Stream.fold fld s1
248+ return $ Stream.filter (`member` xs) s2
249+
250+-- | 'filterInStreamGenericBy' retains only those elements in the second stream that
251+-- are present in the first stream.
377252--
378−-- >>> Stream.fold Fold.toList $ Stream.intersectBy (==) (Stream.fromList [2,1,1,3]) (Stream.fromList [1,2,2,4])
253+-- >>> Stream.fold Fold.toList $ Stream.filterInStreamGenericBy (==) (Stream.fromList [1,2,2,4]) (Stream.fromList [2,1,1,3])
379254-- [2,1,1]
380255--
381−-- 'intersectBy' is similar to but not the same as 'joinInner':
256+-- >>> Stream.fold Fold.toList $ Stream.filterInStreamGenericBy (==) (Stream.fromList [2,1,1,3]) (Stream.fromList [1,2,2,4])
257+-- [1,2,2]
258+--
259+-- Similar to the list intersectBy operation but with the stream argument order
260+-- flipped.
382261--
383−-- >>> Stream.fold Fold.toList $ fmap fst $ Stream.joinInner (==) (Stream.fromList [1,2,2,4]) (Stream.fromList [2,1,1,3])
384−-- [1,1,2,2]
262+-- The first stream must be finite and must not block. Second stream is
263+-- processed only after the first stream is fully realized.
385264--
386265-- Space: O(n) where @n@ is the number of elements in the second stream.
387266--
@@ -389,48 +268,49 @@ mergeOuterJoin _eq _s1 _s2 = undefined
389268-- @n@ is the number of elements in the second stream.
390269--
391270-- /Pre-release/
392−{-# INLINE intersectBy #-}
393−intersectBy :: Monad m =>
271+{-# INLINE filterInStreamGenericBy #-}
272+filterInStreamGenericBy :: Monad m =>
394273 (a -> a -> Bool) -> Stream m a -> Stream m a -> Stream m a
395−intersectBy eq s1 s2 =
396− Stream.concatEffect
397− $ do
398− -- This may work well when s2 is small
399− xs <- Stream.fold Fold.toListRev $ Stream.uniqBy eq s2
400− return $ Stream.filter (\x -> List.any (eq x) xs) s1
274+filterInStreamGenericBy eq =
275+ -- XXX Use an (unboxed) array instead.
276+ filterStreamWith
277+ (Fold.scanMaybe (Fold.uniqBy eq) Fold.toListRev)
278+ (List.any . eq)
401279
402−-- | Like 'intersectBy' but works only on streams sorted in ascending order.
280+-- | Like 'filterInStreamGenericBy' but assumes that the input streams are sorted in
281+-- ascending order. To use it on streams sorted in descending order pass an
282+-- inverted comparison function returning GT for less than and LT for greater
283+-- than.
403284--
404285-- Space: O(1)
405286--
406287-- Time: O(m+n)
407288--
408289-- /Pre-release/
409−{-# INLINE intersectBySorted #-}
410−intersectBySorted :: (Monad m) =>
290+{-# INLINE filterInStreamAscBy #-}
291+filterInStreamAscBy :: Monad m =>
411292 (a -> a -> Ordering) -> Stream m a -> Stream m a -> Stream m a
412−intersectBySorted eq s1 =
293+filterInStreamAscBy eq s1 s2 =
413294 fromStreamD
414− . StreamD.intersectBySorted eq (toStreamD s1)
415− . toStreamD
416−
417−-- Roughly joinLeft s1 s2 = s1 `difference` s2 + s1 `intersection` s2
295+ $ StreamD.intersectBySorted eq (toStreamD s2)
296+ $ toStreamD s1
418297
419−-- | Delete first occurrences of those elements from the first stream that are
420−-- present in the second stream. If an element occurs multiple times in the
421−-- second stream as many occurrences of it are deleted from the first stream.
298+-- | Delete all elements of the first stream from the seconds stream. If an
299+-- element occurs multiple times in the first stream as many occurrences of it
300+-- are deleted from the second stream.
422301--
423−-- >>> Stream.fold Fold.toList $ Stream.differenceBy (==) (Stream.fromList [1,2,2]) (Stream.fromList [1,2,3])
302+-- >>> Stream.fold Fold.toList $ Stream.deleteInStreamGenericBy (==) (Stream.fromList [1,2,3]) (Stream.fromList [1,2,2])
424303-- [2]
425304--
426305-- The following laws hold:
427306--
428−-- @
429−-- (s1 `serial` s2) `differenceBy eq` s1 === s2
430−-- (s1 `wSerial` s2) `differenceBy eq` s1 === s2
431−-- @
307+-- > deleteInStreamGenericBy (==) s1 (s1 `append` s2) === s2
308+-- > deleteInStreamGenericBy (==) s1 (s1 `interleave` s2) === s2
309+--
310+-- Same as the list 'Data.List.//' operation but with argument order flipped.
432311--
433−-- Same as the list 'Data.List.//' operation.
312+-- The first stream must be finite and must not block. Second stream is
313+-- processed only after the first stream is fully realized.
434314--
435315-- Space: O(m) where @m@ is the number of elements in the first stream.
436316--
@@ -438,10 +318,10 @@ intersectBySorted eq s1 =
438318-- @n@ is the number of elements in the second stream.
439319--
440320-- /Pre-release/
441−{-# INLINE differenceBy #-}
442−differenceBy :: (Monad m) =>
321+{-# INLINE deleteInStreamGenericBy #-}
322+deleteInStreamGenericBy :: Monad m =>
443323 (a -> a -> Bool) -> Stream m a -> Stream m a -> Stream m a
444−differenceBy eq s1 s2 =
324+deleteInStreamGenericBy eq s1 s2 =
445325 Stream.concatEffect
446326 $ do
447327 -- This may work well if s1 is small
@@ -449,47 +329,48 @@ differenceBy eq s1 s2 =
449329 -- not emitting an element if it was successfully deleted from s2.
450330 -- we will need a deleteBy that can return whether the element was
451331 -- deleted or not.
452− xs <- Stream.fold Fold.toList s1
332+ xs <- Stream.fold Fold.toList s2
453333 let f = Fold.foldl' (flip (List.deleteBy eq)) xs
454− fmap Stream.fromList $ Stream.fold f s2
334+ fmap Stream.fromList $ Stream.fold f s1
455335
456−-- | Like 'differenceBy' but works only on sorted streams.
336+-- | A more efficient 'deleteInStreamGenericBy' for streams sorted in ascending order.
457337--
458338-- Space: O(1)
459339--
460340-- /Unimplemented/
461−{-# INLINE mergeDifferenceBy #-}
462−mergeDifferenceBy :: -- (Monad m) =>
341+{-# INLINE deleteInStreamAscBy #-}
342+deleteInStreamAscBy :: -- (Monad m) =>
463343 (a -> a -> Ordering) -> Stream m a -> Stream m a -> Stream m a
464−mergeDifferenceBy _eq _s1 _s2 = undefined
344+deleteInStreamAscBy _eq _s1 _s2 = undefined
465345
466−-- | This is essentially an append operation that appends all the extra
467−-- occurrences of elements from the second stream that are not already present
468−-- in the first stream.
346+-- XXX Remove the MonadIO constraint. We can just cache one stream and then
347+-- implement using differenceEqBy.
348+
349+-- | This essentially appends to the second stream all the occurrences of
350+-- elements in the first stream that are not already present in the second
351+-- stream.
469352--
470−-- >>> Stream.fold Fold.toList $ Stream.unionBy (==) (Stream.fromList [1,2,2,4]) (Stream.fromList [1,1,2,3])
471−-- [1,2,2,4,3]
353+-- Equivalent to the following except that @s2@ is evaluated only once:
472354--
473−-- Equivalent to the following except that @s1@ is evaluated only once:
355+-- >>> unionWithStreamGenericBy eq s1 s2 = s2 `Stream.append` (Stream.deleteInStreamGenericBy eq s2 s1)
474356--
475−-- @
476−-- unionBy eq s1 s2 = s1 \`serial` (s2 `differenceBy eq` s1)
477−-- @
357+-- Example:
478358--
479−-- Similar to 'joinOuter' but not the same.
359+-- >>> Stream.fold Fold.toList $ Stream.unionWithStreamGenericBy (==) (Stream.fromList [1,1,2,3]) (Stream.fromList [1,2,2,4])
360+-- [1,2,2,4,3]
480361--
481362-- Space: O(n)
482363--
483364-- Time: O(m x n)
484365--
485366-- /Pre-release/
486−{-# INLINE unionBy #-}
487−unionBy :: MonadIO m =>
367+{-# INLINE unionWithStreamGenericBy #-}
368+unionWithStreamGenericBy :: MonadIO m =>
488369 (a -> a -> Bool) -> Stream m a -> Stream m a -> Stream m a
489−unionBy eq s1 s2 =
370+unionWithStreamGenericBy eq s1 s2 =
490371 Stream.concatEffect
491372 $ do
492− xs <- Stream.fold Fold.toList s2
373+ xs <- Stream.fold Fold.toList s1
493374 -- XXX we can use postscanlMAfter' instead of IORef
494375 ref <- liftIO $ newIORef $! List.nubBy eq xs
495376 let f x = do
@@ -499,14 +380,14 @@ unionBy eq s1 s2 =
499380 $ do
500381 xs1 <- liftIO $ readIORef ref
501382 return $ Stream.fromList xs1
502− return $ Stream.mapM f s1 <> s3
383+ return $ Stream.mapM f s2 <> s3
503384
504−-- | Like 'unionBy' but works only on sorted streams.
385+-- | A more efficient 'unionWithStreamGenericBy' for sorted streams.
505386--
506387-- Space: O(1)
507388--
508389-- /Unimplemented/
509−{-# INLINE mergeUnionBy #-}
510−mergeUnionBy :: -- (Monad m) =>
390+{-# INLINE unionWithStreamAscBy #-}
391+unionWithStreamAscBy :: -- (Monad m) =>
511392 (a -> a -> Ordering) -> Stream m a -> Stream m a -> Stream m a
512−mergeUnionBy _eq _s1 _s2 = undefined
393+unionWithStreamAscBy _eq _s1 _s2 = undefined
core/src/Streamly/Internal/Data/Stream/Type.hs+39
@@ -32,6 +32,8 @@ module Streamly.Internal.Data.Stream.Type
3232 , crossApply
3333 , crossApplySnd
3434 , crossApplyFst
35+ , crossWith
36+ , cross
3537
3638 -- * Bind/Concat
3739 , bindWith
@@ -387,6 +389,10 @@ fromEffect = fromStreamK . K.fromEffect
387389-- Applicative
388390-------------------------------------------------------------------------------
389391
392+-- | Apply a stream of functions to a stream of values and flatten the results.
393+--
394+-- Note that the second stream is evaluated multiple times.
395+--
390396{-# INLINE crossApply #-}
391397crossApply :: Monad m => Stream m (a -> b) -> Stream m a -> Stream m b
392398crossApply m1 m2 =
@@ -402,6 +408,39 @@ crossApplyFst :: Monad m => Stream m a -> Stream m b -> Stream m a
402408crossApplyFst m1 m2 =
403409 fromStreamD $ D.crossApplyFst (toStreamD m1) (toStreamD m2)
404410
411+-- |
412+-- Definition:
413+--
414+-- >>> crossWith f m1 m2 = fmap f m1 `Stream.crossApply` m2
415+--
416+-- Note that the second stream is evaluated multiple times.
417+--
418+{-# INLINE crossWith #-}
419+crossWith :: Monad m => (a -> b -> c) -> Stream m a -> Stream m b -> Stream m c
420+crossWith f m1 m2 = fmap f m1 `crossApply` m2
421+
422+-- | Given a @Stream m a@ and @Stream m b@ generate a stream with all possible
423+-- combinations of the tuple @(a, b)@.
424+--
425+-- Definition:
426+--
427+-- >>> cross = Stream.crossWith (,)
428+--
429+-- The second stream is evaluated multiple times. If that is not desired it can
430+-- be cached in an 'Data.Array.Array' and then generated from the array before
431+-- calling this function. Caching may also improve performance if the stream is
432+-- expensive to evaluate.
433+--
434+-- See 'Streamly.Internal.Data.Unfold.cross' for a much faster fused
435+-- alternative.
436+--
437+-- Time: O(m x n)
438+--
439+-- /Pre-release/
440+{-# INLINE cross #-}
441+cross :: Monad m => Stream m a -> Stream m b -> Stream m (a, b)
442+cross = crossWith (,)
443+
405444-------------------------------------------------------------------------------
406445-- Bind/Concat
407446-------------------------------------------------------------------------------
core/src/Streamly/Internal/Data/Unfold.hs+6
@@ -237,6 +237,7 @@ module Streamly.Internal.Data.Unfold
237237 , crossWithM
238238 , crossWith
239239 , cross
240+ , joinInnerGeneric
240241 , crossApply
241242
242243 -- ** Nesting
@@ -829,6 +830,11 @@ dropWhileM f (Unfold step inject) = Unfold step' inject'
829830dropWhile :: Monad m => (b -> Bool) -> Unfold m a b -> Unfold m a b
830831dropWhile f = dropWhileM (return . f)
831832
833+{-# INLINE_NORMAL joinInnerGeneric #-}
834+joinInnerGeneric :: Monad m =>
835+ (b -> c -> Bool) -> Unfold m a b -> Unfold m a c -> Unfold m a (b, c)
836+joinInnerGeneric eq s1 s2 = filter (\(a, b) -> a `eq` b) $ cross s1 s2
837+
832838------------------------------------------------------------------------------
833839-- Exceptions
834840------------------------------------------------------------------------------
core/src/Streamly/Internal/Data/Unfold/Type.hs+9−6
@@ -513,14 +513,17 @@ crossWith f = crossWithM (\b c -> return $ f b c)
513513
514514-- | See 'crossWith'.
515515--
516−-- > cross = crossWith (,)
516+-- Definition:
517517--
518−-- To cross the streams from a tuple we can write:
518+-- >>> cross = Unfold.crossWith (,)
519519--
520−-- @
521−-- crossProduct :: Monad m => Unfold m a b -> Unfold m c d -> Unfold m (a, c) (b, d)
522−-- crossProduct u1 u2 = cross (lmap fst u1) (lmap snd u2)
523−-- @
520+-- To create a cross product of the streams generated from a tuple we can
521+-- write:
522+--
523+-- >>> :{
524+-- cross :: Monad m => Unfold m a b -> Unfold m c d -> Unfold m (a, c) (b, d)
525+-- cross u1 u2 = Unfold.cross (Unfold.lmap fst u1) (Unfold.lmap snd u2)
526+-- :}
524527--
525528-- /Pre-release/
526529{-# INLINE_NORMAL cross #-}
src/Streamly/Internal/Data/Stream/Extra.hs+150−14
@@ -5,27 +5,44 @@
55-- Maintainer : streamly@composewell.com
66-- Stability : experimental
77-- Portability : GHC
8+--
9+-- Stream operations that require transformers or containers like Set or Map.
10+
11+-- Rename this to Stream.Container?
812
913module Streamly.Internal.Data.Stream.Extra
1014 (
1115 nub
12− , joinInnerMap
13− , joinLeftMap
14− , joinOuterMap
16+
17+ -- * Joins for unconstrained types
18+ , joinLeftGeneric
19+ , joinOuterGeneric
20+
21+ -- * Joins with Ord constraint
22+ , joinInner
23+ , joinLeft
24+ , joinOuter
1525 )
1626where
1727
1828#include "inline.hs"
1929
2030import Control.Monad.IO.Class (MonadIO)
31+import Control.Monad.Trans.State.Strict (get, put)
32+import Data.Function ((&))
33+import Data.Maybe (isJust)
2134import Streamly.Internal.Data.Stream.StreamD.Step (Step(..))
2235import Streamly.Internal.Data.Stream.Type (Stream)
36+import Streamly.Internal.Data.Stream.Cross (CrossStream(..))
2337
2438import qualified Data.Map.Strict as Map
2539import qualified Data.Set as Set
40+import qualified Streamly.Internal.Data.Array.Generic as Array
41+import qualified Streamly.Internal.Data.Array.Mut.Type as MA
2642import qualified Streamly.Data.Fold as Fold
2743import qualified Streamly.Data.Stream as Stream
2844import qualified Streamly.Internal.Data.Stream.StreamD as D
45+import qualified Streamly.Internal.Data.Stream.Transformer as Stream
2946
3047-- $setup
3148-- >>> :m
@@ -74,10 +91,10 @@ toMap =
7491-- Time: O(m + n)
7592--
7693-- /Pre-release/
77−{-# INLINE joinInnerMap #-}
78−joinInnerMap :: (Monad m, Ord k) =>
94+{-# INLINE joinInner #-}
95+joinInner :: (Monad m, Ord k) =>
7996 Stream m (k, a) -> Stream m (k, b) -> Stream m (k, a, b)
80−joinInnerMap s1 s2 =
97+joinInner s1 s2 =
8198 Stream.concatEffect $ do
8299 km <- toMap s2
83100 pure $ Stream.mapMaybe (joinAB km) s1
@@ -89,17 +106,64 @@ joinInnerMap s1 s2 =
89106 Just b -> Just (k, a, b)
90107 Nothing -> Nothing
91108
92−-- | Like 'joinLeft' but uses a hashmap for efficiency.
109+-- XXX We can do this concurrently.
110+-- XXX If the second stream is sorted and passed as an Array or a seek capable
111+-- stream then we could use binary search if we have an Ord instance or
112+-- Ordering returning function. The time complexity would then become (m x log
113+-- n).
114+
115+-- | Like 'joinInner' but emit @(a, Just b)@, and additionally, for those @a@'s
116+-- that are not equal to any @b@ emit @(a, Nothing)@.
117+--
118+-- The second stream is evaluated multiple times. If the stream is a
119+-- consume-once stream then the caller should cache it in an 'Data.Array.Array'
120+-- before calling this function. Caching may also improve performance if the
121+-- stream is expensive to evaluate.
122+--
123+-- >>> joinRightGeneric eq = flip (Stream.joinLeftGeneric eq)
124+--
125+-- Space: O(n) assuming the second stream is cached in memory.
126+--
127+-- Time: O(m x n)
128+--
129+-- /Unimplemented/
130+{-# INLINE joinLeftGeneric #-}
131+joinLeftGeneric :: Monad m =>
132+ (a -> b -> Bool) -> Stream m a -> Stream m b -> Stream m (a, Maybe b)
133+joinLeftGeneric eq s1 s2 = Stream.evalStateT (return False) $ unCrossStream $ do
134+ a <- CrossStream (Stream.liftInner s1)
135+ -- XXX should we use StreamD monad here?
136+ -- XXX Is there a better way to perform some action at the end of a loop
137+ -- iteration?
138+ CrossStream (Stream.fromEffect $ put False)
139+ let final = Stream.concatEffect $ do
140+ r <- get
141+ if r
142+ then pure Stream.nil
143+ else pure (Stream.fromPure Nothing)
144+ b <- CrossStream (fmap Just (Stream.liftInner s2) <> final)
145+ case b of
146+ Just b1 ->
147+ if a `eq` b1
148+ then do
149+ CrossStream (Stream.fromEffect $ put True)
150+ return (a, Just b1)
151+ else CrossStream Stream.nil
152+ Nothing -> return (a, Nothing)
153+
154+-- XXX rename to joinLeftOrd?
155+
156+-- | A more efficient 'joinLeft' using a hashmap for efficiency.
93157--
94158-- Space: O(n)
95159--
96160-- Time: O(m + n)
97161--
98162-- /Pre-release/
99−{-# INLINE joinLeftMap #-}
100−joinLeftMap :: (Ord k, Monad m) =>
163+{-# INLINE joinLeft #-}
164+joinLeft :: (Ord k, Monad m) =>
101165 Stream m (k, a) -> Stream m (k, b) -> Stream m (k, a, Maybe b)
102−joinLeftMap s1 s2 =
166+joinLeft s1 s2 =
103167 Stream.concatEffect $ do
104168 km <- toMap s2
105169 return $ fmap (joinAB km) s1
@@ -111,10 +175,82 @@ joinLeftMap s1 s2 =
111175 Just b -> (k, a, Just b)
112176 Nothing -> (k, a, Nothing)
113177
178+-- XXX We can do this concurrently.
179+
180+-- | Like 'joinLeft' but emits a @(Just a, Just b)@. Like 'joinLeft', for those
181+-- @a@'s that are not equal to any @b@ emit @(Just a, Nothing)@, but
182+-- additionally, for those @b@'s that are not equal to any @a@ emit @(Nothing,
183+-- Just b)@.
184+--
185+-- For space efficiency use the smaller stream as the second stream.
186+--
187+-- Space: O(n)
188+--
189+-- Time: O(m x n)
190+--
191+-- /Pre-release/
192+{-# INLINE joinOuterGeneric #-}
193+joinOuterGeneric :: MonadIO m =>
194+ (a -> b -> Bool)
195+ -> Stream m a
196+ -> Stream m b
197+ -> Stream m (Maybe a, Maybe b)
198+joinOuterGeneric eq s1 s =
199+ Stream.concatEffect $ do
200+ inputArr <- Array.fromStream s
201+ let len = Array.length inputArr
202+ foundArr <-
203+ Stream.fold
204+ (MA.writeN len)
205+ (Stream.fromList (Prelude.replicate len False))
206+ return $ go inputArr foundArr <> leftOver inputArr foundArr
207+
208+ where
209+
210+ leftOver inputArr foundArr =
211+ let stream1 = Array.read inputArr
212+ stream2 = Stream.unfold MA.reader foundArr
213+ in Stream.filter
214+ isJust
215+ ( Stream.zipWith (\x y ->
216+ if y
217+ then Nothing
218+ else Just (Nothing, Just x)
219+ ) stream1 stream2
220+ ) & Stream.catMaybes
221+
222+ evalState = Stream.evalStateT (return False) . unCrossStream
223+
224+ go inputArr foundArr = evalState $ do
225+ a <- CrossStream (Stream.liftInner s1)
226+ -- XXX should we use StreamD monad here?
227+ -- XXX Is there a better way to perform some action at the end of a loop
228+ -- iteration?
229+ CrossStream (Stream.fromEffect $ put False)
230+ let final = Stream.concatEffect $ do
231+ r <- get
232+ if r
233+ then pure Stream.nil
234+ else pure (Stream.fromPure Nothing)
235+ (i, b) <-
236+ let stream = Array.read inputArr
237+ in CrossStream
238+ (Stream.indexed $ fmap Just (Stream.liftInner stream) <> final)
239+
240+ case b of
241+ Just b1 ->
242+ if a `eq` b1
243+ then do
244+ CrossStream (Stream.fromEffect $ put True)
245+ MA.putIndex i True foundArr
246+ return (Just a, Just b1)
247+ else CrossStream Stream.nil
248+ Nothing -> return (Just a, Nothing)
249+
114250-- Put the b's that have been paired, in another hash or mutate the hash to set
115251-- a flag. At the end go through @Stream m b@ and find those that are not in that
116252-- hash to return (Nothing, b).
117−--
253+
118254-- | Like 'joinOuter' but uses a 'Map' for efficiency.
119255--
120256-- Space: O(m + n)
@@ -122,11 +258,11 @@ joinLeftMap s1 s2 =
122258-- Time: O(m + n)
123259--
124260-- /Pre-release/
125−{-# INLINE joinOuterMap #-}
126−joinOuterMap ::
261+{-# INLINE joinOuter #-}
262+joinOuter ::
127263 (Ord k, MonadIO m) =>
128264 Stream m (k, a) -> Stream m (k, b) -> Stream m (k, Maybe a, Maybe b)
129−joinOuterMap s1 s2 =
265+joinOuter s1 s2 =
130266 Stream.concatEffect $ do
131267 km1 <- kvFold s1
132268 km2 <- kvFold s2