2-- Module : Streamly.Internal.Data.Fold.Extra
3-- Copyright : (c) 2019 Composewell Technologies
5-- Maintainer : streamly@composewell.com
6-- Stability : experimental
10module Streamly.Internal.Data.Fold.Extra
23 -- | Direct values in the input stream to different folds using an n-ary
24 -- fold selector. 'demux' is a generalization of 'classify' (and
25 -- 'partition') where each key of the classifier can use a different fold.
32 -- TODO: These can be implemented using the above operations
33 -- , demuxWithSel -- Stop when the fold for the specified key stops
34 -- , demuxWithMin -- Stop when any of the folds stop
35 -- , demuxWithAll -- Stop when all the folds stop (run once)
38 -- | In an input stream of key value pairs fold values for different keys
39 -- in individual output buckets using the given fold. 'classify' is a
40 -- special case of 'demux' where all the branches of the demultiplexer use
43 -- Different types of maps can be used with these combinators via the IsMap
44 -- type class. Hashmap performs better when there are more collisions, trie
45 -- Map performs better otherwise. Trie has an advantage of sorting the keys
46 -- at the same time. For example if we want to store a dictionary of words
47 -- and their meanings then trie Map would be better if we also want to
48 -- display them in sorted order.
60#include "ArrayMacros.h"
62import Control.Monad.IO.Class (MonadIO(..))
63import Data.IORef (newIORef, readIORef, writeIORef)
64import Data.Map.Strict (Map)
65import Streamly.Internal.Data.IsMap (IsMap(..))
66import Streamly.Internal.Data.Tuple.Strict (Tuple'(..), Tuple3'(..))
68import qualified Data.IntSet as IntSet
69import qualified Data.Set as Set
70import qualified Prelude
71import qualified Streamly.Internal.Data.IsMap as IsMap
74 ( filter, foldl1, drop, dropWhile, take, takeWhile, zipWith
75 , foldl, foldr, map, mapM_, sequence, all, any, sum, product, elem
76 , notElem, maximum, minimum, head, last, tail, length, null
77 , reverse, iterate, init, and, or, lookup, (!!)
78 , scanl, scanl1, replicate, concatMap, mconcat, foldMap, unzip
79 , span, splitAt, break, mapM, zip, maybe)
80import Streamly.Internal.Data.Fold
84-- >>> :set -XFlexibleContexts
85-- >>> import qualified Streamly.Data.Fold as Fold
86-- >>> import qualified Streamly.Data.Stream as Stream
87-- >>> import qualified Streamly.Internal.Data.Fold.Extra as Fold
89-- XXX Name as nubOrd? Or write a nubGeneric
91-- | Used as a scan. Returns 'Just' for the first occurrence of an element,
92-- returns 'Nothing' for any other occurrences.
96-- >>> stream = Stream.fromList [1::Int,1,2,3,4,4,5,1,5,7]
97-- >>> Stream.fold Fold.toList $ Stream.scanMaybe Fold.nub stream
102nub :: (Monad m, Ord a) => Fold m a (Maybe a)
103nub = fmap (\(Tuple' _ x) -> x) $ foldl' step initial
107 initial = Tuple' Set.empty Nothing
109 step (Tuple' set _) x =
111 then Tuple' set Nothing
112 else Tuple' (Set.insert x set) (Just x)
114-- | Like 'nub' but specialized to a stream of 'Int', for better performance.
118-- >>> nubInt = Fold.nub
122nubInt :: Monad m => Fold m Int (Maybe Int)
123nubInt = fmap (\(Tuple' _ x) -> x) $ foldl' step initial
127 initial = Tuple' IntSet.empty Nothing
129 step (Tuple' set _) x =
130 if IntSet.member x set
131 then Tuple' set Nothing
132 else Tuple' (IntSet.insert x set) (Just x)
135-- XXX Add a countDistinct window fold
136-- XXX Add a bloom filter fold
138-- | Count non-duplicate elements in the stream.
142-- >>> countDistinct = Fold.postscan Fold.nub $ Fold.catMaybes $ Fold.length
144-- The memory used is proportional to the number of distinct elements in the
145-- stream, to guard against using too much memory use it as a scan and
146-- terminate if the count reaches more than a threshold.
148-- /Space/: \(\mathcal{O}(n)\)
152{-# INLINE countDistinct #-}
153countDistinct :: (Monad m, Ord a) => Fold m a Int
154countDistinct = postscan nub $ catMaybes length
156countDistinct = fmap (\(Tuple' _ n) -> n) $ foldl' step initial
160 initial = Tuple' Set.empty 0
162 step (Tuple' set n) x = do
168 in Tuple' (Set.insert x set) cnt
171-- | Like 'countDistinct' but specialized to a stream of 'Int', for better
176-- >>> countDistinctInt = Fold.postscan Fold.nubInt $ Fold.catMaybes $ Fold.length
179{-# INLINE countDistinctInt #-}
180countDistinctInt :: Monad m => Fold m Int Int
181countDistinctInt = postscan nubInt $ catMaybes length
183countDistinctInt = fmap (\(Tuple' _ n) -> n) $ foldl' step initial
187 initial = Tuple' IntSet.empty 0
189 step (Tuple' set n) x = do
190 if IntSet.member x set
195 in Tuple' (IntSet.insert x set) cnt
198------------------------------------------------------------------------------
199-- demux: in a key value stream fold each key sub-stream with a different fold
200------------------------------------------------------------------------------
202-- TODO Demultiplex an input element into a number of typed variants. We want
203-- to statically restrict the target values within a set of predefined types,
204-- an enumeration of a GADT.
206-- This is the consumer side dual of the producer side 'mux' operation (XXX to
209-- XXX If we use Refold in it, it can perhaps fuse/be more efficient. For
210-- example we can store just the result rather than storing the whole fold in
213-- Note: There are separate functions to determine Key and Fold from the input
214-- because key is to be determined on each input whereas fold is to be
215-- determined only once for a key.
217-- | In a key value stream, fold values corresponding to each key with a key
218-- specific fold. The fold returns the fold result as the second component of
219-- the output tuple whenever a fold terminates. The first component of the
220-- tuple is a container of in-progress folds. If a fold terminates, another
221-- instance of the fold is started upon receiving an input with that key.
223-- This can be used to scan a stream and collect the results from the scan
228{-# INLINE demuxScanWith #-}
229demuxScanWith :: (Monad m, IsMap f, Traversable f) =>
231 -> (a -> m (Fold m a b))
232 -> Fold m a (m (f b), Maybe (Key f, b))
233demuxScanWith getKey getFold = fmap extract $ foldlM' step initial
237 initial = return $ Tuple' IsMap.mapEmpty Nothing
239 {-# INLINE runFold #-}
240 runFold kv (Fold step1 initial1 extract1) (k, a) = do
248 let fld = Fold step1 (return res1) extract1
249 in Tuple' (IsMap.mapInsert k fld kv) Nothing
250 Done b -> Tuple' (IsMap.mapDelete k kv) (Just (k, b))
251 Done b -> return $ Tuple' kv (Just (k, b))
253 step (Tuple' kv _) a = do
255 case IsMap.mapLookup k kv of
258 runFold kv fld (k, a)
259 Just f -> runFold kv f (k, a)
261 extract (Tuple' kv x) = (Prelude.mapM f kv, x)
271-- | This is specialized version of 'demuxScanWith' that uses mutable cells as
272-- fold accumulators for better performance.
276-- >>> demuxScanMutWith = Fold.demuxScanWith
278{-# INLINE demuxScanMutWith #-}
279demuxScanMutWith :: (MonadIO m, IsMap f, Traversable f) =>
281 -> (a -> m (Fold m a b))
282 -> Fold m a (m (f b), Maybe (Key f, b))
283demuxScanMutWith getKey getFold = fmap extract $ foldlM' step initial
287 initial = return $ Tuple' IsMap.mapEmpty Nothing
289 {-# INLINE initFold #-}
290 initFold kv (Fold step1 initial1 extract1) (k, a) = do
297 let fld = Fold step1 (return res1) extract1
298 ref <- liftIO $ newIORef fld
299 return $ Tuple' (IsMap.mapInsert k ref kv) Nothing
300 Done b -> return $ Tuple' kv (Just (k, b))
301 Done b -> return $ Tuple' kv (Just (k, b))
303 {-# INLINE runFold #-}
304 runFold kv ref (Fold step1 initial1 extract1) (k, a) = do
311 let fld = Fold step1 (return res1) extract1
312 liftIO $ writeIORef ref fld
313 return $ Tuple' kv Nothing
315 let kv1 = IsMap.mapDelete k kv
316 in return $ Tuple' kv1 (Just (k, b))
317 Done _ -> error "demuxScanMutWith: unreachable"
319 step (Tuple' kv _) a = do
321 case IsMap.mapLookup k kv of
326 f <- liftIO $ readIORef ref
327 runFold kv ref f (k, a)
329 extract (Tuple' kv x) = (Prelude.mapM f kv, x)
334 (Fold _ i e) <- liftIO $ readIORef ref
340-- | This collects all the results of 'demuxScanWith' in a container.
342{-# INLINE demuxWith #-}
343demuxWith :: (Monad m, IsMap f, Traversable f) =>
344 (a -> Key f) -> (a -> m (Fold m a b)) -> Fold m a (f b)
345demuxWith getKey getFold =
347 classifier = demuxScanWith getKey getFold
348 getMap Nothing = pure IsMap.mapEmpty
349 getMap (Just action) = action
351 teeWith IsMap.mapUnion
352 (rmapM getMap $ lmap fst latest)
353 (lmap snd $ catMaybes toMap)
354 in postscan classifier aggregator
356-- | Same as 'demuxWith' but uses 'demuxScanMutWith' for better performance.
360-- >>> demuxMutWith = Fold.demuxWith
362{-# INLINE demuxMutWith #-}
363demuxMutWith :: (MonadIO m, IsMap f, Traversable f) =>
364 (a -> Key f) -> (a -> m (Fold m a b)) -> Fold m a (f b)
365demuxMutWith getKey getFold =
367 classifier = demuxScanMutWith getKey getFold
368 getMap Nothing = pure IsMap.mapEmpty
369 getMap (Just action) = action
371 teeWith IsMap.mapUnion
372 (rmapM getMap $ lmap fst latest)
373 (lmap snd $ catMaybes toMap)
374 in postscan classifier aggregator
376-- | Fold a stream of key value pairs using a function that maps keys to folds.
380-- >>> demux f = Fold.demuxWith fst (Fold.lmap snd . f)
384-- >>> import Data.Map (Map)
386-- let f "SUM" = return Fold.sum
387-- f _ = return Fold.product
388-- input = Stream.fromList [("SUM",1),("PRODUCT",2),("SUM",3),("PRODUCT",4)]
389-- in Stream.fold (Fold.demux f) input :: IO (Map String Int)
391-- fromList [("PRODUCT",8),("SUM",4)]
395demux :: (Monad m, IsMap f, Traversable f) =>
396 (Key f -> m (Fold m a b)) -> Fold m (Key f, a) (f b)
397demux f = demuxWith fst (\(k, _) -> fmap (lmap snd) (f k))
399------------------------------------------------------------------------------
400-- Classify: Like demux but uses the same fold for all keys.
401------------------------------------------------------------------------------
403-- XXX Change these to make the behavior similar to demux* variants. We can
404-- implement this using classifyScanManyWith. Maintain a set of done folds in
405-- the underlying monad, and when initial is called look it up, if the fold is
406-- done then initial would set a flag in the state to ignore the input or
409-- | Folds the values for each key using the supplied fold. When scanning, as
410-- soon as the fold is complete, its result is available in the second
411-- component of the tuple. The first component of the tuple is a snapshot of
412-- the in-progress folds.
414-- Once the fold for a key is done, any future values of the key are ignored.
418-- >>> classifyScanWith f fld = Fold.demuxScanWith f (const fld)
420{-# INLINE classifyScanWith #-}
421classifyScanWith :: (Monad m, IsMap f, Traversable f, Ord (Key f)) =>
422 -- Note: we need to return the Map itself to display the in-progress values
423 -- e.g. to implement top. We could possibly create a separate abstraction
424 -- for that use case. We return an action because we want it to be lazy so
425 -- that the downstream consumers can choose to process or discard it.
426 (a -> Key f) -> Fold m a b -> Fold m a (m (f b), Maybe (Key f, b))
427classifyScanWith f (Fold step1 initial1 extract1) =
428 fmap extract $ foldlM' step initial
432 initial = return $ Tuple3' IsMap.mapEmpty Set.empty Nothing
434 {-# INLINE initFold #-}
435 initFold kv set k a = do
443 Tuple3' (IsMap.mapInsert k s1 kv) set Nothing
445 Tuple3' kv set (Just (k, b))
446 Done b -> return (Tuple3' kv (Set.insert k set) (Just (k, b)))
448 step (Tuple3' kv set _) a = do
450 case IsMap.mapLookup k kv of
453 then return (Tuple3' kv set Nothing)
454 else initFold kv set k a
460 Tuple3' (IsMap.mapInsert k s1 kv) set Nothing
462 let kv1 = IsMap.mapDelete k kv
463 in Tuple3' kv1 (Set.insert k set) (Just (k, b))
465 extract (Tuple3' kv _ x) = (Prelude.mapM extract1 kv, x)
467-- XXX we can use a Prim IORef if we can constrain the state "s" to be Prim
469-- The code is almost the same as classifyScanWith except the IORef operations.
471-- | Same as classifyScanWith except that it uses mutable IORef cells in the
472-- Map providing better performance. Be aware that if this is used as a scan,
473-- the values in the intermediate Maps would be mutable.
477-- >>> classifyScanMutWith = Fold.classifyScanWith
478-- >>> classifyScanMutWith f fld = Fold.demuxScanMutWith f (const fld)
480{-# INLINE classifyScanMutWith #-}
481classifyScanMutWith :: (MonadIO m, IsMap f, Traversable f, Ord (Key f)) =>
482 (a -> Key f) -> Fold m a b -> Fold m a (m (f b), Maybe (Key f, b))
483classifyScanMutWith f (Fold step1 initial1 extract1) =
484 fmap extract $ foldlM' step initial
488 initial = return $ Tuple3' IsMap.mapEmpty Set.empty Nothing
490 {-# INLINE initFold #-}
491 initFold kv set k a = do
498 ref <- liftIO $ newIORef s1
499 return $ Tuple3' (IsMap.mapInsert k ref kv) set Nothing
501 return $ Tuple3' kv set (Just (k, b))
502 Done b -> return (Tuple3' kv (Set.insert k set) (Just (k, b)))
504 step (Tuple3' kv set _) a = do
506 case IsMap.mapLookup k kv of
509 then return (Tuple3' kv set Nothing)
510 else initFold kv set k a
512 s <- liftIO $ readIORef ref
516 liftIO $ writeIORef ref s1
517 return $ Tuple3' kv set Nothing
519 let kv1 = IsMap.mapDelete k kv
521 $ Tuple3' kv1 (Set.insert k set) (Just (k, b))
523 extract (Tuple3' kv _ x) =
524 (Prelude.mapM (\ref -> liftIO (readIORef ref) >>= extract1) kv, x)
526-- | Fold a key value stream to a key-value container. If the same key appears
527-- multiple times, only the last value is retained.
529toMap :: (Monad m, IsMap f) => Fold m (Key f, a) (f a)
530toMap = foldl' (\kv (k, v) -> IsMap.mapInsert k v kv) IsMap.mapEmpty
532-- | Split the input stream based on a key field and fold each split using the
533-- given fold. Useful for map/reduce, bucketizing the input in different bins
534-- or for generating histograms.
538-- >>> import Data.Map.Strict (Map)
540-- let input = Stream.fromList [("ONE",1),("ONE",1.1),("TWO",2), ("TWO",2.2)]
541-- classify = Fold.classifyWith fst (Fold.lmap snd Fold.toList)
542-- in Stream.fold classify input :: IO (Map String [Double])
544-- fromList [("ONE",[1.0,1.1]),("TWO",[2.0,2.2])]
546-- Once the classifier fold terminates for a particular key any further inputs
547-- in that bucket are ignored.
549-- Space used is proportional to the number of keys seen till now and
550-- monotonically increases because it stores whether a key has been seen or
557{-# INLINE classifyWith #-}
558classifyWith :: (Monad m, IsMap f, Traversable f, Ord (Key f)) =>
559 (a -> Key f) -> Fold m a b -> Fold m a (f b)
562 classifier = classifyScanWith f fld
563 getMap Nothing = pure IsMap.mapEmpty
564 getMap (Just action) = action
566 teeWith IsMap.mapUnion
567 (rmapM getMap $ lmap fst latest)
568 (lmap snd $ catMaybes toMap)
569 in postscan classifier aggregator
571-- | Same as 'classifyWith' but maybe faster because it uses mutable cells as
572-- fold accumulators in the Map.
576-- >>> classifyMutWith = Fold.classifyWith
578{-# INLINE classifyMutWith #-}
579classifyMutWith :: (MonadIO m, IsMap f, Traversable f, Ord (Key f)) =>
580 (a -> Key f) -> Fold m a b -> Fold m a (f b)
581classifyMutWith f fld =
583 classifier = classifyScanMutWith f fld
584 getMap Nothing = pure IsMap.mapEmpty
585 getMap (Just action) = action
587 teeWith IsMap.mapUnion
588 (rmapM getMap $ lmap fst latest)
589 (lmap snd $ catMaybes toMap)
590 in postscan classifier aggregator
592-- | Given an input stream of key value pairs and a fold for values, fold all
593-- the values belonging to each key. Useful for map/reduce, bucketizing the
594-- input in different bins or for generating histograms.
598-- >>> classify = Fold.classifyWith fst . Fold.lmap snd
603-- let input = Stream.fromList [("ONE",1),("ONE",1.1),("TWO",2), ("TWO",2.2)]
604-- in Stream.fold (Fold.classify Fold.toList) input
606-- fromList [("ONE",[1.0,1.1]),("TWO",[2.0,2.2])]
609{-# INLINE classify #-}
610classify :: (Monad m, Ord k) => Fold m a b -> Fold m (k, a) (Map k b)
611classify = classifyWith fst . lmap snd
613-- | Determine the frequency of each element in the stream.
615-- You can just collect the keys of the resulting map to get the unique
616-- elements in the stream.
620-- >>> frequency = Fold.classifyWith id Fold.length
622{-# INLINE frequency #-}
623frequency :: (Monad m, Ord a) => Fold m a (Map a Int)
624frequency = classifyWith id length
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list