on 3387346 help sign in
740 lines 25 kB history raw
1-- |
2-- Module : Streamly.Internal.Data.Stream.IsStream.Top
3-- Copyright : (c) 2020 Composewell Technologies
4-- License : BSD-3-Clause
5-- Maintainer : streamly@composewell.com
6-- Stability : experimental
7-- Portability : GHC
8--
9-- Top level IsStream module that can use all other lower level IsStream
10-- modules.
12module Streamly.Internal.Data.Stream.IsStream.Top
13 (
14 -- * Transformation
15 -- ** Sampling
16 -- | Value agnostic filtering.
17 sampleFromThen
18 , sampleIntervalStart
19 , sampleIntervalEnd
20 , sampleBurstStart
21 , sampleBurstEnd
23 -- ** Reordering
24 , sortBy
26 -- * Nesting
27 -- ** Set like operations
28 -- | These are not exactly set operations because streams are not
29 -- necessarily sets, they may have duplicated elements.
30 , intersectBy
31 , intersectBySorted
32 , differenceBy
33 , mergeDifferenceBy
34 , unionBy
35 , mergeUnionBy
37 -- ** Join operations
38 , crossJoin
39 , joinInner
40 , joinInnerMap
41 , joinInnerMerge
42 , joinLeft
43 , mergeLeftJoin
44 , joinLeftMap
45 , joinOuter
46 , mergeOuterJoin
47 , joinOuterMap
48 )
49where
51#include "inline.hs"
53import Control.Monad.Catch (MonadCatch)
54import Control.Monad.IO.Class (MonadIO(..))
55import Control.Monad.Trans.Class (lift)
56import Control.Monad.Trans.State.Strict (get, put)
57import Data.Function ((&))
58import Data.IORef (newIORef, readIORef, modifyIORef')
59import Data.Kind (Type)
60import Data.Maybe (isJust)
61import Streamly.Internal.Control.Concurrent (MonadAsync)
62import Streamly.Internal.Data.Stream.IsStream.Common (concatM)
63import Streamly.Internal.Data.Stream.IsStream.Type
64 (IsStream(..), adapt, foldl', fromList)
65import Streamly.Internal.Data.Stream.Serial (SerialT)
66import Streamly.Internal.Data.Time.Units (NanoSecond64(..), toRelTime64)
68import qualified Data.List as List
69import qualified Data.Map.Strict as Map
70import qualified Streamly.Internal.Data.Array as Array
71import qualified Streamly.Internal.Data.Array.Foreign.Mut.Type as MA
72import qualified Streamly.Internal.Data.Fold as Fold
73import qualified Streamly.Internal.Data.Parser as Parser
74import qualified Streamly.Internal.Data.Stream.IsStream.Lift as Stream
75import qualified Streamly.Internal.Data.Stream.IsStream.Eliminate as Stream
76import qualified Streamly.Internal.Data.Stream.IsStream.Generate as Stream
77import qualified Streamly.Internal.Data.Stream.IsStream.Expand as Stream
78import qualified Streamly.Internal.Data.Stream.IsStream.Reduce as Stream
79import qualified Streamly.Internal.Data.Stream.IsStream.Transform as Stream
80import qualified Streamly.Internal.Data.Stream.IsStream.Type as IsStream
81import qualified Streamly.Internal.Data.Stream.StreamD as StreamD
83import Prelude hiding (filter, zipWith, concatMap, concat)
85-- $setup
86-- >>> :m
87-- >>> import Prelude hiding (filter, zipWith, concatMap, concat)
88-- >>> import qualified Streamly.Prelude as Stream
89-- >>> import qualified Streamly.Internal.Data.Stream.IsStream as Stream
91------------------------------------------------------------------------------
92-- Sampling
93------------------------------------------------------------------------------
95-- XXX We can implement this using addition instead of "mod" to make it more
96-- efficient.
97--
98-- | @sampleFromthen offset stride@ samples the element at @offset@ index and
99-- then every element at strides of @stride@.
100--
101-- >>> Stream.toList $ Stream.sampleFromThen 2 3 $ Stream.enumerateFromTo 0 10
102-- [2,5,8]
103--
104-- /Pre-release/
105--
106{-# INLINE sampleFromThen #-}
107sampleFromThen :: (IsStream t, Monad m, Functor (t m)) =>
108 Int -> Int -> t m a -> t m a
109sampleFromThen offset stride =
110 Stream.with Stream.indexed Stream.filter
111 (\(i, _) -> i >= offset && (i - offset) `mod` stride == 0)
113-- | Continuously evaluate the input stream and sample the last event in time
114-- window of @n@ seconds.
115--
116-- This is also known as @throttle@ in some libraries.
117--
118-- @
119-- sampleIntervalEnd n = Stream.catMaybes . Stream.intervalsOf n Fold.last
120-- @
121--
122-- /Pre-release/
123--
124{-# INLINE sampleIntervalEnd #-}
125sampleIntervalEnd :: (IsStream t, MonadAsync m, Functor (t m)) =>
126 Double -> t m a -> t m a
127sampleIntervalEnd n = Stream.catMaybes . Stream.intervalsOf n Fold.last
129-- | Like 'sampleInterval' but samples at the beginning of the time window.
130--
131-- @
132-- sampleIntervalStart n = Stream.catMaybes . Stream.intervalsOf n Fold.head
133-- @
134--
135-- /Pre-release/
136--
137{-# INLINE sampleIntervalStart #-}
138sampleIntervalStart :: (IsStream t, MonadAsync m, Functor (t m)) =>
139 Double -> t m a -> t m a
140sampleIntervalStart n = Stream.catMaybes . Stream.intervalsOf n Fold.head
142data BurstState t x =
143 BurstNone
144 | BurstWait !t !x
145 | BurstDone !x
146 | BurstDoneNext !x !t !x
148{-# INLINE sampleBurst #-}
149sampleBurst :: (IsStream t, MonadAsync m, Functor (t m)) =>
150 Bool -> Double -> t m a -> t m a
151sampleBurst sampleAtEnd gap xs =
152 -- XXX Ideally we should schedule a timer event exactly after gap time,
153 -- but the tick stream should work well as long as the timer
154 -- granularity is small enough compared to the gap.
155 Stream.mapMaybe extract
156 $ Stream.scanl' step BurstNone
157 $ Stream.timeIndexed
158 $ Stream.interjectSuffix 0.01 (return Nothing) (Stream.map Just xs)
160 where
162 gap1 = toRelTime64 (NanoSecond64 (round (gap * 10^(9::Int))))
164 {-# INLINE step #-}
165 step BurstNone (t1, Just x1) = BurstWait t1 x1
166 step BurstNone _ = BurstNone
168 step (BurstDone _) (t1, Just x1) = BurstWait t1 x1
169 step (BurstDone _) _ = BurstNone
171 step old@(BurstWait t0 x0) (t1, Nothing)
172 | t1 - t0 >= gap1 = BurstDone x0
173 | otherwise = old
174 -- This can happen due to scheduling delays, if we received back to
175 -- back events spaced by more than the timeout without an
176 -- intervening timeout event then we emit the old event instead of
177 -- replacing it by the new.
178 step (BurstWait t0 x0) (t1, Just x1)
179 | t1 - t0 >= gap1 = BurstDoneNext x0 t1 x1
180 | sampleAtEnd = BurstWait t1 x1
181 | otherwise = BurstWait t1 x0
183 step (BurstDoneNext _ t0 x0) (t1, Nothing)
184 | t1 - t0 >= gap1 = BurstDone x0
185 | otherwise = BurstWait t0 x0
186 step (BurstDoneNext _ t0 x0) (t1, Just x1)
187 | t1 - t0 >= gap1 = BurstDoneNext x0 t1 x1
188 | sampleAtEnd = BurstWait t1 x1
189 | otherwise = BurstWait t1 x0
191 {-# INLINE extract #-}
192 extract (BurstDoneNext x _ _) = Just x
193 extract (BurstDone x) = Just x
194 extract _ = Nothing
196-- | Sample one event at the end of each burst of events. A burst is a group
197-- of events close together in time, it ends when an event is spaced by more
198-- than the specified time interval (in seconds) from the previous event.
199--
200-- This is known as @debounce@ in some libraries.
201--
202-- The clock granularity is 10 ms.
203--
204-- /Pre-release/
205--
206{-# INLINE sampleBurstEnd #-}
207sampleBurstEnd :: (IsStream t, MonadAsync m, Functor (t m)) =>
208 Double -> t m a -> t m a
209sampleBurstEnd = sampleBurst True
211-- | Like 'sampleBurstEnd' but samples the event at the beginning of the burst
212-- instead of at the end of it.
213--
214-- /Pre-release/
215--
216{-# INLINE sampleBurstStart #-}
217sampleBurstStart :: (IsStream t, MonadAsync m, Functor (t m)) =>
218 Double -> t m a -> t m a
219sampleBurstStart = sampleBurst False
221------------------------------------------------------------------------------
222-- Reordering
223------------------------------------------------------------------------------
224--
225-- We could possibly choose different algorithms depending on whether the
226-- input stream is almost sorted (ascending/descending) or random. We could
227-- serialize the stream to an array and use quicksort.
228--
229-- | Sort the input stream using a supplied comparison function.
230--
231-- /O(n) space/
232--
233-- Note: this is not the fastest possible implementation as of now.
234--
235-- /Pre-release/
236--
237{-# INLINE sortBy #-}
238sortBy :: MonadCatch m => (a -> a -> Ordering) -> SerialT m a -> SerialT m a
239-- sortBy f = Stream.concatPairsWith (Stream.mergeBy f) Stream.fromPure
240sortBy cmp =
241 let p =
242 Parser.groupByRollingEither
243 (\x -> (< GT) . cmp x)
244 Fold.toStreamRev
245 Fold.toStream
246 in Stream.concatPairsWith (Stream.mergeBy cmp) id
247 . Stream.parseMany (fmap (either id id) p)
249------------------------------------------------------------------------------
250-- SQL Joins
251------------------------------------------------------------------------------
252--
253-- Some references:
254-- * https://en.wikipedia.org/wiki/Relational_algebra
255-- * https://en.wikipedia.org/wiki/Join_(SQL)
257-- TODO: OrdSet/IntSet/hashmap based versions of these. With Eq only
258-- constraint, the best would be to use an Array with linear search. If the
259-- second stream is sorted we can also use a binary search, using Ord
260-- constraint or an ordering function.
261--
262-- For Storables we can cache the second stream into an unboxed array for
263-- possibly faster access/compact representation?
264--
265-- If we do not want to keep the stream in memory but always read it from the
266-- source (disk/network) every time we iterate through it then we can do that
267-- too by reading the stream every time, the stream must have immutable state
268-- in that case and the user is responsible for the behavior if the stream
269-- source changes during iterations. We can also use an Unfold instead of
270-- stream. We probably need a way to distinguish streams that can be read
271-- mutliple times without any interference (e.g. unfolding a stream using an
272-- immutable handle would work i.e. using pread/pwrite instead of maintianing
273-- an offset in the handle).
275-- XXX We can do this concurrently.
276--
277-- | This is the same as 'Streamly.Internal.Data.Unfold.outerProduct' but less
278-- efficient.
279--
280-- The second stream is evaluated multiple times. If the second stream is
281-- consume-once stream then it can be cached in an 'Data.Array.Array' before
282-- calling this function. Caching may also improve performance if the stream is
283-- expensive to evaluate.
284--
285-- Time: O(m x n)
286--
287-- /Pre-release/
288{-# INLINE crossJoin #-}
289crossJoin :: Monad (t m) => t m a -> t m b -> t m (a, b)
290crossJoin s1 s2 = do
291 -- XXX use concatMap instead?
292 a <- s1
293 b <- s2
294 return (a, b)
296-- XXX We can do this concurrently.
297-- XXX If the second stream is sorted and passed as an Array we could use
298-- binary search if we have an Ord instance or Ordering returning function. The
299-- time complexity would then become (m x log n).
300--
301-- | For all elements in @t m a@, for all elements in @t m b@ if @a@ and @b@
302-- are equal by the given equality pedicate then return the tuple (a, b).
303--
304-- The second stream is evaluated multiple times. If the stream is a
305-- consume-once stream then the caller should cache it (e.g. in a
306-- 'Data.Array.Array') before calling this function. Caching may also improve
307-- performance if the stream is expensive to evaluate.
308--
309-- For space efficiency use the smaller stream as the second stream.
310--
311-- You should almost always use joinInnerMap instead of joinInner. joinInnerMap
312-- is an order of magnitude faster. joinInner may be used when the second
313-- stream is generated from a seed, therefore, need not be stored in memory and
314-- the amount of memory it takes is a concern.
315--
316-- Space: O(n) assuming the second stream is cached in memory.
317--
318-- Time: O(m x n)
319--
320-- /Pre-release/
321{-# INLINE joinInner #-}
322joinInner ::
323 forall (t :: (Type -> Type) -> Type -> Type) m a b.
324 (IsStream t, Monad m) =>
325 (a -> b -> Bool) -> t m a -> t m b -> t m (a, b)
326joinInner eq s1 s2 = do
327 -- ConcatMap works faster than bind
328 Stream.concatMap (\a ->
329 Stream.concatMap (\b ->
330 if a `eq` b
331 then Stream.fromPure (a, b)
332 else Stream.nil
333 ) s2
334 ) s1
336-- XXX Generate error if a duplicate insertion is attempted?
337toMap :: (Monad m, Ord k) => IsStream.SerialT m (k, v) -> m (Map.Map k v)
338toMap = Stream.foldl' (\kv (k, b) -> Map.insert k b kv) Map.empty
340-- If the second stream is too big it can be partitioned based on hashes and
341-- then we can process one parition at a time.
342--
343-- XXX An IntMap may be faster when the keys are Int.
344-- XXX Use hashmap instead of map?
345--
346-- | Like 'joinInner' but uses a 'Map' for efficiency.
347--
348-- If the input streams have duplicate keys, the behavior is undefined.
349--
350-- For space efficiency use the smaller stream as the second stream.
351--
352-- Space: O(n)
353--
354-- Time: O(m + n)
355--
356-- /Pre-release/
357{-# INLINE joinInnerMap #-}
358joinInnerMap :: (IsStream t, Monad m, Ord k) =>
359 t m (k, a) -> t m (k, b) -> t m (k, a, b)
360joinInnerMap s1 s2 =
361 Stream.concatM $ do
362 km <- toMap $ IsStream.adapt s2
363 pure $ Stream.mapMaybe (joinAB km) s1
365 where
367 joinAB kvm (k, a) =
368 case k `Map.lookup` kvm of
369 Just b -> Just (k, a, b)
370 Nothing -> Nothing
372-- | Like 'joinInner' but works only on sorted streams.
373--
374-- Space: O(1)
375--
376-- Time: O(m + n)
377--
378-- /Unimplemented/
379{-# INLINE joinInnerMerge #-}
380joinInnerMerge :: (a -> b -> Ordering) -> t m a -> t m b -> t m (a, b)
381joinInnerMerge = undefined
383-- XXX We can do this concurrently.
384-- XXX If the second stream is sorted and passed as an Array or a seek capable
385-- stream then we could use binary search if we have an Ord instance or
386-- Ordering returning function. The time complexity would then become (m x log
387-- n).
388--
389-- | For all elements in @t m a@, for all elements in @t m b@ if @a@ and @b@
390-- are equal then return the tuple @(a, Just b)@. If @a@ is not present in @t
391-- m b@ then return @(a, Nothing)@.
392--
393-- The second stream is evaluated multiple times. If the stream is a
394-- consume-once stream then the caller should cache it in an 'Data.Array.Array'
395-- before calling this function. Caching may also improve performance if the
396-- stream is expensive to evaluate.
397--
398-- @
399-- rightJoin = flip joinLeft
400-- @
401--
402-- Space: O(n) assuming the second stream is cached in memory.
403--
404-- Time: O(m x n)
405--
406-- /Unimplemented/
407{-# INLINE joinLeft #-}
408joinLeft :: Monad m =>
409 (a -> b -> Bool) -> SerialT m a -> SerialT m b -> SerialT m (a, Maybe b)
410joinLeft eq s1 s2 = Stream.evalStateT (return False) $ do
411 a <- Stream.liftInner s1
412 -- XXX should we use StreamD monad here?
413 -- XXX Is there a better way to perform some action at the end of a loop
414 -- iteration?
415 lift $ put False
416 let final = do
417 r <- lift get
418 if r
419 then Stream.nil
420 else Stream.fromPure Nothing
421 b <- fmap Just (Stream.liftInner s2) <> final
422 case b of
423 Just b1 ->
424 if a `eq` b1
425 then do
426 lift $ put True
427 return (a, Just b1)
428 else Stream.nil
429 Nothing -> return (a, Nothing)
431-- | Like 'joinLeft' but uses a hashmap for efficiency.
432--
433-- Space: O(n)
434--
435-- Time: O(m + n)
436--
437-- /Pre-release/
438{-# INLINE joinLeftMap #-}
439joinLeftMap :: (IsStream t, Ord k, Monad m) =>
440 t m (k, a) -> t m (k, b) -> t m (k, a, Maybe b)
441joinLeftMap s1 s2 =
442 Stream.concatM $ do
443 km <- toMap $ IsStream.adapt s2
444 return $ Stream.map (joinAB km) s1
446 where
448 joinAB km (k, a) =
449 case k `Map.lookup` km of
450 Just b -> (k, a, Just b)
451 Nothing -> (k, a, Nothing)
453-- | Like 'joinLeft' but works only on sorted streams.
454--
455-- Space: O(1)
456--
457-- Time: O(m + n)
458--
459-- /Unimplemented/
460{-# INLINE mergeLeftJoin #-}
461mergeLeftJoin :: -- Monad m =>
462 (a -> b -> Ordering) -> t m a -> t m b -> t m (a, Maybe b)
463mergeLeftJoin _eq _s1 _s2 = undefined
465-- XXX We can do this concurrently.
466--
467-- | For all elements in @t m a@, for all elements in @t m b@ if @a@ and @b@
468-- are equal by the given equality pedicate then return the tuple (Just a, Just
469-- b). If @a@ is not found in @t m b@ then return (a, Nothing), return
470-- (Nothing, b) for vice-versa.
471--
472-- For space efficiency use the smaller stream as the second stream.
473--
474-- Space: O(n)
475--
476-- Time: O(m x n)
477--
478-- /Unimplemented/
479{-# INLINE joinOuter #-}
480joinOuter :: MonadIO m =>
481 (a -> b -> Bool)
482 -> SerialT m a
483 -> SerialT m b
484 -> SerialT m (Maybe a, Maybe b)
485joinOuter eq s1 s =
486 Stream.concatM $ do
487 inputArr <- Array.fromStream s
488 let len = length inputArr
489 foundArr <-
490 Stream.fold
491 (MA.writeN len)
492 (Stream.fromList (Prelude.replicate len False))
493 return $ go inputArr foundArr <> leftOver inputArr foundArr
495 where
497 leftOver inputArr foundArr =
498 let stream1 = IsStream.fromSerial $ Array.toStream inputArr
499 stream2 = Stream.unfold MA.read foundArr
500 in Stream.filter
501 isJust
502 ( Stream.zipWith (\x y ->
503 if y
504 then Nothing
505 else Just (Nothing, Just x)
506 ) stream1 stream2
507 ) & Stream.catMaybes
509 go inputArr foundArr = Stream.evalStateT (return False) $ do
510 a <- Stream.liftInner s1
511 -- XXX should we use StreamD monad here?
512 -- XXX Is there a better way to perform some action at the end of a loop
513 -- iteration?
514 lift $ put False
515 let final = do
516 r <- lift get
517 if r
518 then Stream.nil
519 else Stream.fromPure Nothing
520 (i, b) <-
521 let stream = IsStream.fromSerial $ Array.toStream inputArr
522 in Stream.indexed $ fmap Just (Stream.liftInner stream) <> final
524 case b of
525 Just b1 ->
526 if a `eq` b1
527 then do
528 lift $ put True
529 MA.putIndex i True foundArr
530 return (Just a, Just b1)
531 else Stream.nil
532 Nothing -> return (Just a, Nothing)
534-- Put the b's that have been paired, in another hash or mutate the hash to set
535-- a flag. At the end go through @t m b@ and find those that are not in that
536-- hash to return (Nothing, b).
537--
538-- | Like 'joinOuter' but uses a 'Map' for efficiency.
539--
540-- Space: O(m + n)
541--
542-- Time: O(m + n)
543--
544-- /Pre-release/
545{-# INLINE joinOuterMap #-}
546joinOuterMap ::
547 (IsStream t, Ord k, MonadIO m) =>
548 t m (k, a) -> t m (k, b) -> t m (k, Maybe a, Maybe b)
549joinOuterMap s1 s2 =
550 Stream.concatM $ do
551 km1 <- kvFold $ IsStream.adapt s1
552 km2 <- kvFold $ IsStream.adapt s2
554 -- XXX Not sure if toList/fromList would fuse optimally. We may have to
555 -- create a fused Map.toStream function.
556 let res1 = Stream.map (joinAB km2) $ Stream.fromList $ Map.toList km1
557 where
558 joinAB km (k, a) =
559 case k `Map.lookup` km of
560 Just b -> (k, Just a, Just b)
561 Nothing -> (k, Just a, Nothing)
563 -- XXX We can take advantage of the lookups in the first pass above to
564 -- reduce the number of lookups in this pass. If we keep mutable cells
565 -- in the second Map, we can flag it in the first pass and not do any
566 -- lookup in the second pass if it is flagged.
567 let res2 = Stream.mapMaybe (joinAB km1) $ Stream.fromList $ Map.toList km2
568 where
569 joinAB km (k, b) =
570 case k `Map.lookup` km of
571 Just _ -> Nothing
572 Nothing -> Just (k, Nothing, Just b)
574 return $ Stream.serial res1 res2
576 where
578 -- XXX Generate error if a duplicate insertion is attempted?
579 kvFold = Stream.foldl' (\kv (k, b) -> Map.insert k b kv) Map.empty
581-- | Like 'joinOuter' but works only on sorted streams.
582--
583-- Space: O(1)
584--
585-- Time: O(m + n)
586--
587-- /Unimplemented/
588{-# INLINE mergeOuterJoin #-}
589mergeOuterJoin :: -- Monad m =>
590 (a -> b -> Ordering) -> t m a -> t m b -> t m (Maybe a, Maybe b)
591mergeOuterJoin _eq _s1 _s2 = undefined
593------------------------------------------------------------------------------
594-- Set operations (special joins)
595------------------------------------------------------------------------------
596--
597-- TODO: OrdSet/IntSet/hashmap based versions of these. With Eq only constraint
598-- the best would be to use an Array with linear search. If the second stream
599-- is sorted we can also use a binary search, using Ord constraint.
601-- | 'intersectBy' is essentially a filtering operation that retains only those
602-- elements in the first stream that are present in the second stream.
603--
604-- >>> Stream.toList $ Stream.intersectBy (==) (Stream.fromList [1,2,2,4]) (Stream.fromList [2,1,1,3])
605-- [1,2,2]
606--
607-- >>> Stream.toList $ Stream.intersectBy (==) (Stream.fromList [2,1,1,3]) (Stream.fromList [1,2,2,4])
608-- [2,1,1]
609--
610-- 'intersectBy' is similar to but not the same as 'joinInner':
611--
612-- >>> Stream.toList $ fmap fst $ Stream.joinInner (==) (Stream.fromList [1,2,2,4]) (Stream.fromList [2,1,1,3])
613-- [1,1,2,2]
614--
615-- Space: O(n) where @n@ is the number of elements in the second stream.
616--
617-- Time: O(m x n) where @m@ is the number of elements in the first stream and
618-- @n@ is the number of elements in the second stream.
619--
620-- /Pre-release/
621{-# INLINE intersectBy #-}
622intersectBy :: (IsStream t, Monad m) =>
623 (a -> a -> Bool) -> t m a -> t m a -> t m a
624intersectBy eq s1 s2 =
625 concatM
626 $ do
627 -- This may work well when s2 is small
628 xs <- Stream.toListRev $ Stream.uniqBy eq $ adapt s2
629 return $ Stream.filter (\x -> List.any (eq x) xs) s1
631-- | Like 'intersectBy' but works only on streams sorted in ascending order.
632--
633-- Space: O(1)
634--
635-- Time: O(m+n)
636--
637-- /Pre-release/
638{-# INLINE intersectBySorted #-}
639intersectBySorted :: (IsStream t, Monad m) =>
640 (a -> a -> Ordering) -> t m a -> t m a -> t m a
641intersectBySorted eq s1 =
642 IsStream.fromStreamD
643 . StreamD.intersectBySorted eq (IsStream.toStreamD s1)
644 . IsStream.toStreamD
646-- Roughly joinLeft s1 s2 = s1 `difference` s2 + s1 `intersection` s2
648-- | Delete first occurrences of those elements from the first stream that are
649-- present in the second stream. If an element occurs multiple times in the
650-- second stream as many occurrences of it are deleted from the first stream.
651--
652-- >>> Stream.toList $ Stream.differenceBy (==) (Stream.fromList [1,2,2]) (Stream.fromList [1,2,3])
653-- [2]
654--
655-- The following laws hold:
656--
657-- @
658-- (s1 `serial` s2) `differenceBy eq` s1 === s2
659-- (s1 `wSerial` s2) `differenceBy eq` s1 === s2
660-- @
661--
662-- Same as the list 'Data.List.//' operation.
663--
664-- Space: O(m) where @m@ is the number of elements in the first stream.
665--
666-- Time: O(m x n) where @m@ is the number of elements in the first stream and
667-- @n@ is the number of elements in the second stream.
668--
669-- /Pre-release/
670{-# INLINE differenceBy #-}
671differenceBy :: (IsStream t, Monad m) =>
672 (a -> a -> Bool) -> t m a -> t m a -> t m a
673differenceBy eq s1 s2 =
674 concatM
675 $ do
676 -- This may work well if s1 is small
677 -- If s1 is big we can go through s1, deleting elements from s2 and
678 -- not emitting an element if it was successfully deleted from s2.
679 -- we will need a deleteBy that can return whether the element was
680 -- deleted or not.
681 xs <- Stream.toList $ adapt s1
682 fmap fromList $ foldl' (flip (List.deleteBy eq)) xs s2
684-- | Like 'differenceBy' but works only on sorted streams.
685--
686-- Space: O(1)
687--
688-- /Unimplemented/
689{-# INLINE mergeDifferenceBy #-}
690mergeDifferenceBy :: -- (IsStream t, Monad m) =>
691 (a -> a -> Ordering) -> t m a -> t m a -> t m a
692mergeDifferenceBy _eq _s1 _s2 = undefined
694-- | This is essentially an append operation that appends all the extra
695-- occurrences of elements from the second stream that are not already present
696-- in the first stream.
697--
698-- >>> Stream.toList $ Stream.unionBy (==) (Stream.fromList [1,2,2,4]) (Stream.fromList [1,1,2,3])
699-- [1,2,2,4,3]
700--
701-- Equivalent to the following except that @s1@ is evaluated only once:
702--
703-- @
704-- unionBy eq s1 s2 = s1 \`serial` (s2 `differenceBy eq` s1)
705-- @
706--
707-- Similar to 'joinOuter' but not the same.
708--
709-- Space: O(n)
710--
711-- Time: O(m x n)
712--
713-- /Pre-release/
714{-# INLINE unionBy #-}
715unionBy :: (IsStream t, MonadAsync m, Semigroup (t m a)) =>
716 (a -> a -> Bool) -> t m a -> t m a -> t m a
717unionBy eq s1 s2 =
718 concatM
719 $ do
720 xs <- Stream.toList $ adapt s2
721 -- XXX we can use postscanlMAfter' instead of IORef
722 ref <- liftIO $ newIORef $! List.nubBy eq xs
723 let f x = do
724 liftIO $ modifyIORef' ref (List.deleteBy eq x)
725 return x
726 s3 = concatM
727 $ do
728 xs1 <- liftIO $ readIORef ref
729 return $ fromList xs1
730 return $ Stream.mapM f s1 <> s3
732-- | Like 'unionBy' but works only on sorted streams.
733--
734-- Space: O(1)
735--
736-- /Unimplemented/
737{-# INLINE mergeUnionBy #-}
738mergeUnionBy :: -- (IsStream t, Monad m) =>
739 (a -> a -> Ordering) -> t m a -> t m a -> t m a
740mergeUnionBy _eq _s1 _s2 = undefined