@@ -21,42 +21,42 @@ module Streamly.Internal.Data.Stream.Top
21 -- * Nesting
21 -- * Nesting
22 -- ** Set like operations
22 -- ** Set like operations
23 -- | These are not exactly set operations because streams are not
23 -- | These are not exactly set operations because streams are not
24− -- necessarily sets, they may have duplicated elements.
24+ -- necessarily sets, they may have duplicated elements. These operations
25− , intersectBy
25+ -- are generic i.e. they work on streams of unconstrained types, therefore,
26− , intersectBySorted
26+ -- they have quadratic performance characterstics. For better performance
27− , differenceBy
27+ -- using Set structures see the Streamly.Internal.Data.Stream.Container
28− , mergeDifferenceBy
28+ -- module.
29− , unionBy
29+ , filterInStreamGenericBy
30− , mergeUnionBy
30+ , deleteInStreamGenericBy
31+ , unionWithStreamGenericBy
33+ -- ** Set like operations on sorted streams
34+ , filterInStreamAscBy
35+ , deleteInStreamAscBy
36+ , unionWithStreamAscBy
32 -- ** Join operations
38 -- ** Join operations
33− , crossJoin
39+ , joinInnerGeneric
35− , joinInnerMerge
41+ -- * Joins on sorted stream
36− , joinLeft
42+ , joinInnerAscBy
37− , mergeLeftJoin
43+ , joinLeftAscBy
38− , joinOuter
44+ , joinOuterAscBy
43#include "inline.hs"
48#include "inline.hs"
45import Control.Monad.IO.Class (MonadIO(..))
50import Control.Monad.IO.Class (MonadIO(..))
46−import Control.Monad.Trans.State.Strict (get, put)
47−import Data.Function ((&))
48import Data.IORef (newIORef, readIORef, modifyIORef')
51import Data.IORef (newIORef, readIORef, modifyIORef')
49−import Data.Maybe (isJust)
52+import Streamly.Internal.Data.Fold.Type (Fold)
50import Streamly.Internal.Data.Stream.Common ()
53import Streamly.Internal.Data.Stream.Common ()
51−import Streamly.Internal.Data.Stream.Cross (CrossStream (..))
54+import Streamly.Internal.Data.Stream.Type
52−import Streamly.Internal.Data.Stream.Type (Stream, fromStreamD, toStreamD)
55+ (Stream, fromStreamD, toStreamD, cross)
54import qualified Data.List as List
57import 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
57import qualified Streamly.Internal.Data.Fold as Fold
58import qualified Streamly.Internal.Data.Fold as Fold
58import qualified Streamly.Internal.Data.Parser as Parser
59import qualified Streamly.Internal.Data.Parser as Parser
59−import qualified Streamly.Internal.Data.Stream.Transformer as Stream
60import qualified Streamly.Internal.Data.Stream.Eliminate as Stream
60import qualified Streamly.Internal.Data.Stream.Eliminate as Stream
61import qualified Streamly.Internal.Data.Stream.Generate as Stream
61import qualified Streamly.Internal.Data.Stream.Generate as Stream
62import qualified Streamly.Internal.Data.Stream.Expand as Stream
62import qualified Streamly.Internal.Data.Stream.Expand as Stream
@@ -140,59 +140,37 @@ sortBy cmp =
140-- source changes during iterations. We can also use an Unfold instead of
140-- source changes during iterations. We can also use an Unfold instead of
141-- stream. We probably need a way to distinguish streams that can be read
141-- stream. We probably need a way to distinguish streams that can be read
142-- mutliple times without any interference (e.g. unfolding a stream using an
142-- 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
144-- an offset in the handle).
144-- an offset in the handle).
146−-- XXX We can do this concurrently.
148−-- | This is the same as 'Streamly.Internal.Data.Unfold.outerProduct' but less
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.
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?
167-- XXX We can do this concurrently.
146-- XXX We can do this concurrently.
168-- XXX If the second stream is sorted and passed as an Array we could use
147-- XXX If the second stream is sorted and passed as an Array we could use
169-- binary search if we have an Ord instance or Ordering returning function. The
148-- binary search if we have an Ord instance or Ordering returning function. The
170-- time complexity would then become (m x log n).
149-- time complexity would then become (m x log n).
151+-- | Like 'cross' but emits only those tuples where @a == b@ using the
152+-- supplied equality predicate.
172−-- | For all elements in @Stream m a@, for all elements in @Stream m b@ if @a@ and @b@
154+-- Definition:
173−-- are equal by the given equality pedicate then return the tuple (a, b).
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.
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
182−-- You should almost always use joinInnerMap instead of joinInner. joinInnerMap
158+-- You should almost always prefer @joinInnerOrd@ over 'joinInnerGeneric' if
183−-- is an order of magnitude faster. joinInner may be used when the second
159+-- possible. @joinInnerOrd@ is an order of magnitude faster but may take more
184−-- stream is generated from a seed, therefore, need not be stored in memory and
160+-- space for caching the second stream.
185−-- the amount of memory it takes is a concern.
187−-- Space: O(n) assuming the second stream is cached in memory.
162+-- See 'Streamly.Internal.Data.Unfold.joinInnerGeneric' for a much faster fused
189-- Time: O(m x n)
165-- Time: O(m x n)
191-- /Pre-release/
167-- /Pre-release/
192−{-# INLINE joinInner #-}
168+{-# INLINE joinInnerGeneric #-}
193−joinInner :: Monad m =>
169+joinInnerGeneric :: Monad m =>
194 (a -> b -> Bool) -> Stream m a -> Stream m b -> Stream m (a, b)
170 (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
173+joinInnerGeneric eq s1 s2 = do
196 -- ConcatMap works faster than bind
174 -- ConcatMap works faster than bind
197 Stream.concatMap (\a ->
175 Stream.concatMap (\a ->
198 Stream.concatMap (\b ->
176 Stream.concatMap (\b ->
@@ -201,165 +179,46 @@ joinInner eq s1 s2 = do
201 else Stream.nil
179 else Stream.nil
205−-- | Like 'joinInner' but works only on sorted streams.
184+-- | A more efficient 'joinInner' for sorted streams.
207-- Space: O(1)
186-- Space: O(1)
209-- Time: O(m + n)
188-- Time: O(m + n)
211-- /Unimplemented/
190-- /Unimplemented/
212−{-# INLINE joinInnerMerge #-}
191+{-# INLINE joinInnerAscBy #-}
213−joinInnerMerge ::
192+joinInnerAscBy ::
214 (a -> b -> Ordering) -> Stream m a -> Stream m b -> Stream m (a, b)
193 (a -> b -> Ordering) -> Stream m a -> Stream m b -> Stream m (a, b)
215−joinInnerMerge = undefined
194+joinInnerAscBy = undefined
217−-- XXX We can do this concurrently.
196+-- | A more efficient 'joinLeft' for sorted streams.
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
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)@.
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.
233−-- rightJoin = flip joinLeft
236−-- Space: O(n) assuming the second stream is cached in memory.
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
249− CrossStream (Stream.fromEffect $ put False)
250− let final = Stream.concatEffect $ do
253− then pure Stream.nil
254− else pure (Stream.fromPure Nothing)
255− b <- CrossStream (fmap Just (Stream.liftInner s2) <> final)
260− CrossStream (Stream.fromEffect $ put True)
262− else CrossStream Stream.nil
263− Nothing -> return (a, Nothing)
265−-- | Like 'joinLeft' but works only on sorted streams.
267-- Space: O(1)
198-- Space: O(1)
269-- Time: O(m + n)
200-- Time: O(m + n)
271-- /Unimplemented/
202-- /Unimplemented/
272−{-# INLINE mergeLeftJoin #-}
203+{-# INLINE joinLeftAscBy #-}
273−mergeLeftJoin :: -- Monad m =>
204+joinLeftAscBy :: -- Monad m =>
274 (a -> b -> Ordering) -> Stream m a -> Stream m b -> Stream m (a, Maybe b)
205 (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
277−-- XXX We can do this concurrently.
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.
284−-- For space efficiency use the smaller stream as the second stream.
291−{-# INLINE joinOuter #-}
292−joinOuter :: MonadIO m =>
296− -> Stream m (Maybe a, Maybe b)
298− Stream.concatEffect $ do
299− inputArr <- Array.fromStream s
300− let len = Array.length inputArr
304− (Stream.fromList (Prelude.replicate len False))
305− return $ go inputArr foundArr <> leftOver inputArr foundArr
309− leftOver inputArr foundArr =
310− let stream1 = Array.read inputArr
311− stream2 = Stream.unfold MA.reader foundArr
314− ( Stream.zipWith (\x y ->
317− else Just (Nothing, Just x)
319− ) & Stream.catMaybes
321− evalState = Stream.evalStateT (return False) . unCrossStream
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
328− CrossStream (Stream.fromEffect $ put False)
329− let final = Stream.concatEffect $ do
332− then pure Stream.nil
333− else pure (Stream.fromPure Nothing)
335− let stream = Array.read inputArr
337− (Stream.indexed $ fmap Just (Stream.liftInner stream) <> final)
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)
349−-- | Like 'joinOuter' but works only on sorted streams.
208+-- | A more efficient 'joinOuter' for sorted streams.
351-- Space: O(1)
210-- Space: O(1)
353-- Time: O(m + n)
212-- Time: O(m + n)
355-- /Unimplemented/
214-- /Unimplemented/
356−{-# INLINE mergeOuterJoin #-}
215+{-# INLINE joinOuterAscBy #-}
357−mergeOuterJoin :: -- Monad m =>
216+joinOuterAscBy :: -- Monad m =>
358 (a -> b -> Ordering)
217 (a -> b -> Ordering)
359 -> Stream m a
218 -> Stream m a
360 -> Stream m b
219 -> Stream m b
361 -> Stream m (Maybe a, Maybe b)
220 -> Stream m (Maybe a, Maybe b)
362−mergeOuterJoin _eq _s1 _s2 = undefined
221+joinOuterAscBy _eq _s1 _s2 = undefined
364------------------------------------------------------------------------------
223------------------------------------------------------------------------------
365-- Set operations (special joins)
224-- Set operations (special joins)
@@ -369,19 +228,39 @@ mergeOuterJoin _eq _s1 _s2 = undefined
369-- the best would be to use an Array with linear search. If the second stream
228-- the best would be to use an Array with linear search. If the second stream
370-- is sorted we can also use a binary search, using Ord constraint.
229-- is sorted we can also use a binary search, using Ord constraint.
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.
375−-- >>> Stream.fold Fold.toList $ Stream.intersectBy (==) (Stream.fromList [1,2,2,4]) (Stream.fromList [2,1,1,3])
236+-- The first stream must be finite and must not block.
376−-- [1,2,2]
237+{-# INLINE filterStreamWith #-}
238+filterStreamWith :: Monad m =>
240+ -> (a -> f a -> Bool)
244+filterStreamWith fld member s1 s2 =
247+ xs <- Stream.fold fld s1
248+ return $ Stream.filter (`member` xs) s2
250+-- | 'filterInStreamGenericBy' retains only those elements in the second stream that
251+-- are present in the first stream.
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])
379-- [2,1,1]
254-- [2,1,1]
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])
259+-- Similar to the list intersectBy operation but with the stream argument order
383−-- >>> Stream.fold Fold.toList $ fmap fst $ Stream.joinInner (==) (Stream.fromList [1,2,2,4]) (Stream.fromList [2,1,1,3])
262+-- The first stream must be finite and must not block. Second stream is
384−-- [1,1,2,2]
263+-- processed only after the first stream is fully realized.
386-- Space: O(n) where @n@ is the number of elements in the second stream.
265-- Space: O(n) where @n@ is the number of elements in the second stream.
@@ -389,48 +268,49 @@ mergeOuterJoin _eq _s1 _s2 = undefined
389-- @n@ is the number of elements in the second stream.
268-- @n@ is the number of elements in the second stream.
391-- /Pre-release/
270-- /Pre-release/
392−{-# INLINE intersectBy #-}
271+{-# INLINE filterInStreamGenericBy #-}
393−intersectBy :: Monad m =>
272+filterInStreamGenericBy :: Monad m =>
394 (a -> a -> Bool) -> Stream m a -> Stream m a -> Stream m a
273 (a -> a -> Bool) -> Stream m a -> Stream m a -> Stream m a
395−intersectBy eq s1 s2 =
274+filterInStreamGenericBy eq =
396− Stream.concatEffect
275+ -- XXX Use an (unboxed) array instead.
397− $ do
276+ filterStreamWith
398− -- This may work well when s2 is small
277+ (Fold.scanMaybe (Fold.uniqBy eq) Fold.toListRev)
399− xs <- Stream.fold Fold.toListRev $ Stream.uniqBy eq s2
278+ (List.any . eq)
400− return $ Stream.filter (\x -> List.any (eq x) xs) s1
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
404-- Space: O(1)
285-- Space: O(1)
406-- Time: O(m+n)
287-- Time: O(m+n)
408-- /Pre-release/
289-- /Pre-release/
409−{-# INLINE intersectBySorted #-}
290+{-# INLINE filterInStreamAscBy #-}
410−intersectBySorted :: (Monad m) =>
291+filterInStreamAscBy :: Monad m =>
411 (a -> a -> Ordering) -> Stream m a -> Stream m a -> Stream m a
292 (a -> a -> Ordering) -> Stream m a -> Stream m a -> Stream m a
412−intersectBySorted eq s1 =
293+filterInStreamAscBy eq s1 s2 =
413 fromStreamD
294 fromStreamD
414− . StreamD.intersectBySorted eq (toStreamD s1)
295+ $ StreamD.intersectBySorted eq (toStreamD s2)
415− . toStreamD
296+ $ toStreamD s1
417−-- Roughly joinLeft s1 s2 = s1 `difference` s2 + s1 `intersection` s2
419−-- | Delete first occurrences of those elements from the first stream that are
298+-- | Delete all elements of the first stream from the seconds stream. If an
420−-- present in the second stream. If an element occurs multiple times in the
299+-- element occurs multiple times in the first stream as many occurrences of it
421−-- second stream as many occurrences of it are deleted from the first stream.
300+-- are deleted from the second stream.
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])
426-- The following laws hold:
305-- The following laws hold:
428−-- @
307+-- > deleteInStreamGenericBy (==) s1 (s1 `append` s2) === s2
429−-- (s1 `serial` s2) `differenceBy eq` s1 === s2
308+-- > deleteInStreamGenericBy (==) s1 (s1 `interleave` s2) === s2
430−-- (s1 `wSerial` s2) `differenceBy eq` s1 === s2
310+-- Same as the list 'Data.List.//' operation but with argument order flipped.
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.
435-- Space: O(m) where @m@ is the number of elements in the first stream.
315-- Space: O(m) where @m@ is the number of elements in the first stream.
@@ -438,10 +318,10 @@ intersectBySorted eq s1 =
438-- @n@ is the number of elements in the second stream.
318-- @n@ is the number of elements in the second stream.
440-- /Pre-release/
320-- /Pre-release/
441−{-# INLINE differenceBy #-}
321+{-# INLINE deleteInStreamGenericBy #-}
442−differenceBy :: (Monad m) =>
322+deleteInStreamGenericBy :: Monad m =>
443 (a -> a -> Bool) -> Stream m a -> Stream m a -> Stream m a
323 (a -> a -> Bool) -> Stream m a -> Stream m a -> Stream m a
444−differenceBy eq s1 s2 =
324+deleteInStreamGenericBy eq s1 s2 =
445 Stream.concatEffect
325 Stream.concatEffect
447 -- This may work well if s1 is small
327 -- This may work well if s1 is small
@@ -449,47 +329,48 @@ differenceBy eq s1 s2 =
449 -- not emitting an element if it was successfully deleted from s2.
329 -- not emitting an element if it was successfully deleted from s2.
450 -- we will need a deleteBy that can return whether the element was
330 -- we will need a deleteBy that can return whether the element was
451 -- deleted or not.
331 -- deleted or not.
452− xs <- Stream.fold Fold.toList s1
332+ xs <- Stream.fold Fold.toList s2
453 let f = Fold.foldl' (flip (List.deleteBy eq)) xs
333 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
456−-- | Like 'differenceBy' but works only on sorted streams.
336+-- | A more efficient 'deleteInStreamGenericBy' for streams sorted in ascending order.
458-- Space: O(1)
338-- Space: O(1)
460-- /Unimplemented/
340-- /Unimplemented/
461−{-# INLINE mergeDifferenceBy #-}
341+{-# INLINE deleteInStreamAscBy #-}
462−mergeDifferenceBy :: -- (Monad m) =>
342+deleteInStreamAscBy :: -- (Monad m) =>
463 (a -> a -> Ordering) -> Stream m a -> Stream m a -> Stream m a
343 (a -> a -> Ordering) -> Stream m a -> Stream m a -> Stream m a
464−mergeDifferenceBy _eq _s1 _s2 = undefined
344+deleteInStreamAscBy _eq _s1 _s2 = undefined
346+-- XXX Remove the MonadIO constraint. We can just cache one stream and then
347+-- implement using differenceEqBy.
466−-- | This is essentially an append operation that appends all the extra
349+-- | This essentially appends to the second stream all the occurrences of
467−-- occurrences of elements from the second stream that are not already present
350+-- elements in the first stream that are not already present in the second
468−-- in the first stream.
351+-- stream.
470−-- >>> Stream.fold Fold.toList $ Stream.unionBy (==) (Stream.fromList [1,2,2,4]) (Stream.fromList [1,1,2,3])
353+-- Equivalent to the following except that @s2@ is evaluated only once:
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)
476−-- unionBy eq s1 s2 = s1 \`serial` (s2 `differenceBy eq` s1)
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])
481-- Space: O(n)
362-- Space: O(n)
483-- Time: O(m x n)
364-- Time: O(m x n)
485-- /Pre-release/
366-- /Pre-release/
486−{-# INLINE unionBy #-}
367+{-# INLINE unionWithStreamGenericBy #-}
487−unionBy :: MonadIO m =>
368+unionWithStreamGenericBy :: MonadIO m =>
488 (a -> a -> Bool) -> Stream m a -> Stream m a -> Stream m a
369 (a -> a -> Bool) -> Stream m a -> Stream m a -> Stream m a
489−unionBy eq s1 s2 =
370+unionWithStreamGenericBy eq s1 s2 =
490 Stream.concatEffect
371 Stream.concatEffect
492− xs <- Stream.fold Fold.toList s2
373+ xs <- Stream.fold Fold.toList s1
493 -- XXX we can use postscanlMAfter' instead of IORef
374 -- XXX we can use postscanlMAfter' instead of IORef
494 ref <- liftIO $ newIORef $! List.nubBy eq xs
375 ref <- liftIO $ newIORef $! List.nubBy eq xs
495 let f x = do
376 let f x = do
@@ -499,14 +380,14 @@ unionBy eq s1 s2 =
500 xs1 <- liftIO $ readIORef ref
381 xs1 <- liftIO $ readIORef ref
501 return $ Stream.fromList xs1
382 return $ Stream.fromList xs1
502− return $ Stream.mapM f s1 <> s3
383+ return $ Stream.mapM f s2 <> s3
504−-- | Like 'unionBy' but works only on sorted streams.
385+-- | A more efficient 'unionWithStreamGenericBy' for sorted streams.
506-- Space: O(1)
387-- Space: O(1)
508-- /Unimplemented/
389-- /Unimplemented/
509−{-# INLINE mergeUnionBy #-}
390+{-# INLINE unionWithStreamAscBy #-}
510−mergeUnionBy :: -- (Monad m) =>
391+unionWithStreamAscBy :: -- (Monad m) =>
511 (a -> a -> Ordering) -> Stream m a -> Stream m a -> Stream m a
392 (a -> a -> Ordering) -> Stream m a -> Stream m a -> Stream m a
512−mergeUnionBy _eq _s1 _s2 = undefined
393+unionWithStreamAscBy _eq _s1 _s2 = undefined