Conduit Streaming Framework

repository·master·Indexed 21 days ago

https://github.com/snoyberg/conduit

A high-performance Haskell streaming framework for data processing with constant memory usage and deterministic resource management. It provides a composable interface for producing, transforming, and consuming data streams, serving as a predictable alternative to lazy I/O. The ecosystem includes integration libraries such as cereal-conduit for Data.Serialize and network-conduit-tls for secure TLS-aware network applications. Compatible with version 1.3 and available in LTS Haskell version 11 and up.

Tokens
11.1K
Snippets
33
Records
40
Agent score
73%

What's inside conduit

  1. Overview of the conduit streaming framework

    master
    conduit is a framework designed for the production, transformation, and consumption of data streams in constant memory. It provides a deterministic alternative to lazy I/O, ensuring that resources (like file handles or network sockets) are handled predictably and released promptly. It is suitable for processing large datasets that would otherwise exceed available memory.
  2. Overview of Conduit streaming framework

    master

    Conduit is a framework for processing streaming data (e.g., reading bytes from files, parsing CSVs from HTTP responses, or traversing directory trees) with several key advantages:

    • Constant memory usage: Efficiently handles large datasets without loading them entirely into memory.
    • Deterministic resource usage: Ensures resources like file handles are closed promptly.
    • Composable interfaces: Easily combine diverse data sources (HTTP, files) with various consumers (XML/CSV processors).

    Conduit is compatible with version 1.3 and is available in LTS Haskell version 11 and up.

  3. Use cereal-conduit to bridge Data.Serialize and Conduit

    master
    The cereal-conduit library provides integration between the cereal serialization library and the conduit streaming framework. It allows you to transform Data.Serialize Get and Put operations into Conduit Source, Sink, and Conduit components, enabling streaming serialization and deserialization of data.
  4. Interacting with external processes using Data.Conduit.Process

    master

    The Data.Conduit.Process module provides a high-level, type-safe interface for interacting with external processes via streaming. It addresses common issues in System.Process such as lazy I/O and race conditions when checking exit codes.

    Key features:

    • Provides Sources and Sinks for standard input, output, and error.
    • Uses a StreamingProcessHandle to avoid deadlocks when checking exit codes.
    • Designed to work seamlessly with the async library for concurrent stream handling.

    Important Requirements:

    • Use the multithreaded runtime (-threaded): The underlying waitForProcess function can block all threads on a single-threaded runtime.
    • Process Configuration: For general parameters like working directory or environment variables, use standard System.Process facilities (e.g., the proc or shell functions).
    import Data.Conduit.Process (proc, streamingProcess, waitForStreamingProcess)
  5. What is ResourceT and how to use it

    master

    ResourceT is a monad transformer that creates a region of code where you can safely allocate and release resources. Unlike the standard bracket pattern, which is designed for nested, statically known resource lifecycles, ResourceT allows for interleaved allocation and deallocation. This is particularly useful when the number or order of resources depends on runtime data (e.g., reading a stream and opening a new file for every chunk of data).

    To use ResourceT, you typically use runResourceT to execute a block of code and allocate to register a resource with a cleanup action. You can also manually trigger the release of a resource using the releaseKey returned by allocate.

    import Control.Monad.Trans.Resource
    import Control.Monad.IO.Class
    
    main :: IO ()
    main = runResourceT $ do
        -- allocate returns a (ReleaseKey, resource)
        (releaseKey, resource) <- allocate
            (putStrLn "Allocating resource" >> return 42)
            (\i -> putStrLn $ "Freeing resource: " ++ show i)
        
        -- Use the resource
        liftIO $ print resource
        
        -- Manually release the resource early if needed
        release releaseKey
  6. Produce multiple streams in parallel using ZipSource

    master

    Similar to ZipSink, ZipSource allows you to produce multiple streams of data in parallel from a single source. It is useful for zipping together different data sources into a single stream of tuples or combined values.

    import Conduit
    
    -- Create a stream of (index, fibonacci_number) pairs
    indexedFibs :: ConduitT () (Int, Int) IO ()
    indexedFibs = getZipSource
        $ (,) 
      <$> ZipSource (yieldMany [1..])
      <*> ZipSource (yieldMany fibs)
      where fibs = 0 : 1 : zipWith (+) fibs (drop 1 fibs)
    
    main :: IO ()
    main = runConduit $ indexedFibs .| takeC 10 .| mapM_C print
  7. Understand the Conduit evaluation strategy: Downstream drives

    master

    The most critical concept in Conduit is that everything is driven by downstream. A component in the pipeline only executes if a downstream component requests data via await.

    • If the downstream component is return (), it never calls await, so the entire pipeline terminates immediately without running upstream components.
    • If a component in the middle of the pipeline is return (), it will exit immediately without yielding anything, causing any downstream components to receive Nothing and also exit.
    • To consume all values from a pipeline and discard them, use sinkNull instead of return ().
    import Conduit
    
    -- This will produce no output because return () exits immediately
    main :: IO ()
    main = runConduit $ yieldMany [1..10] .| iterMC print .| return ()
    
    -- This will print all values because sinkNull drives the pipeline
    main2 :: IO ()
    main2 = runConduit $ yieldMany [1..10] .| iterMC print .| sinkNull
  8. How interleaved effects work in Conduit

    master

    Unlike standard lists where side-effecting operations (like mapM) often require breaking the pipeline or processing entire chunks at once, Conduit allows you to interleave effects directly within the pipeline. This results in:

    1. Constant Memory Usage: Data flows through the pipeline one element at a time, avoiding the need to build intermediate lists in memory.
    2. Efficient Execution: Because Conduit is consumer-driven, components downstream can stop the upstream from performing unnecessary work. For example, if a takeWhileC condition fails, the upstream components will not be called again.

    To perform side-effecting operations on elements in a pipeline, use mapMC instead of mapC.

    magicalConduit :: IO ()
    magicalConduit = runConduit
         $ yieldMany [1..]
        .| takeC 10
        .| mapMC magic
        .| takeWhileC (< 18)
        .| mapM_C print
  9. Use ZipConduit to combine multiple transformers

    master

    ZipConduit is a newtype wrapper that allows you to combine multiple transformers. It works by draining all yielded values from all internal ZipConduits until they are all awaiting, then grabbing the next value from upstream and feeding it to all of them simultaneously. This is particularly useful for splitting a single stream into multiple specialized sub-streams and then recombining the results into a single transformer.

    To use it, wrap your individual conduits in ZipConduit and combine them using the *> operator, then use getZipConduit to extract the resulting transformer.

    import Conduit
    
    tagger :: Monad m => ConduitT Int (Either Int Int) m ()
    tagger = mapC $ \i -> if even i then Left i else Right i
    
    evens, odds :: Monad m => ConduitT Int String m ()
    evens  = mapC $ \i -> "Even number: " ++ show i
    odds   = mapC $ \i -> "Odd  number: " ++ show i
    
    left :: Either l r -> Maybe l
    left = either Just (const Nothing)
    
    right :: Either l r -> Maybe r
    right = either (const Nothing) Just
    
    inside :: Monad m => ConduitT (Either Int Int) String m ()
    inside = getZipConduit
        $ ZipConduit (concatMapC left  .| evens)
       *> ZipConduit (concatMapC right .| odds)
    
    main :: IO ()
    main = runConduit $ enumFromToC 1 10 .| tagger .| inside .| mapM_C putStrLn
  10. Work with chunked data using CE functions

    master

    When working with efficient containers like Text, ByteString, or Vector, standard stream functions (suffixed with C) operate on the container itself. To operate on the elements inside the chunks, use functions suffixed with CE (Chunked Element). This avoids the overhead of manually mapping over containers.

    Common patterns include:

    • omapCE: Monomorphic mapping over elements (use the o prefix if the container is monomorphic, like Text).
    • takeCE: Take a specific number of elements from the chunks.
    • takeWhileCE: Take elements while a predicate holds.
    import Conduit
    import Data.Char (toUpper)
    
    -- Efficiently converting a file to uppercase using chunked operations
    main :: IO ()
    main = runConduitRes
         $ sourceFile "input.txt"
        .| decodeUtf8C
        .| omapCE toUpper
        .| encodeUtf8C
        .| stdoutC
  11. When to use ResourceT vs bracket

    master

    Choosing between the bracket pattern and ResourceT depends on the complexity of your resource management needs:

    • Use bracket if your resource management needs are simple (e.g., a single allocation and release tied to a single scope).
    • Use ResourceT if you need to interleave allocations or manage resources in a more complex, non-linear fashion. ResourceT provides a flexible way to allocate resources in an exception-safe manner, allowing for more complicated programs to be created efficiently.