Skip to content

F# Guide — Pipelines: Composable Data Processing

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about F# Guide. We cover key concepts, practical examples, and best practices to help you master this topic.

F# pipelines use the forward pipe operator (|>) to chain data transformations, creating readable, step-by-step sequences where data flows from one function to the next.

What You'll Learn

  • Forward pipe operator (|>)
  • Building data transformation pipelines
  • Debugging pipelines
  • Performance considerations
  • Custom pipeline operators

Why It Matters

Pipelines make code read in the direction of data flow (left-to-right, top-to-bottom), matching how humans think about transformations. Durga Antivirus Pro uses pipelines for scan result processing.

Real-World Use

Etl Pipelines, data analysis, request processing middleware, and any sequence of data transformations.

flowchart LR
    A["Pipelines"] --> B["Pipe Operator"]
    B --> C["Transformations"]
    C --> D["Debugging"]
    D --> E["Custom Operators"]
    A:::current --> B
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b

Forward Pipe Operator

// Without pipe (nested, reads right-to-left)
let result = List.sum (List.map (fun x -> x * 2) (List.filter (fun x -> x > 0) [-1; 0; 1; 2]))

// With pipe (reads top-to-bottom)
let result =
    [-1; 0; 1; 2]
    |> List.filter (fun x -> x > 0)
    |> List.map (fun x -> x * 2)
    |> List.sum

Building Pipelines

let processData data =
    data
    |> Seq.filter (fun x -> x > 0)
    |> Seq.map (fun x -> x * x)
    |> Seq.sortByDescending id
    |> Seq.take 10
    |> Seq.toList

// Multi-step data processing
let analyzeText (text: string) =
    text
    |> fun s -> s.Split(' ')
    |> Array.filter (fun w -> w.Length > 3)
    |> Array.map (fun w -> w.ToLower())
    |> Array.countBy id
    |> Array.sortByDescending snd
    |> Array.take 5

Debugging Pipelines

// Inject debug steps
let result =
    data
    |> List.map (fun x -> x * 2)
    |> List.map (fun x -> printfn "After double: %d" x; x)
    |> List.filter (fun x -> x > 5)
    |> List.map (fun x -> printfn "After filter: %d" x; x)
    |> List.sum

// Or use a tap function
let tap f x = f x; x

let result =
    data
    |> List.map (fun x -> x * 2)
    |> tap (List.iter (printfn "Value: %d"))
    |> List.filter (fun x -> x > 5)
    |> List.sum

Custom Pipe Operators

// Pipe into two-track (Result) processing
let (>>=) result f = Result.bind f result

let processWithResult input =
    Ok input
    >>= (fun x -> if x > 0 then Ok (x * 2) else Error "negative")
    >>= (fun x -> if x < 100 then Ok x else Error "too large")

// Double-pipe for applying two functions
let (<|) f x = f x  // Backward pipe
let result = List.sum <| [1; 2; 3]

Pipeline with Side Effects

let saveToFile path data =
    System.IO.File.WriteAllText(path, data)
    data

let pipeline =
    getData()
    |> transformData
    |> validateData
    |> saveToFile "output.txt"
    |> sendNotification

Async Pipelines

let asyncPipeline input = async {
    let! data = fetchDataAsync input
    let processed = data |> transform |> validate
    let! saved = saveAsync processed
    return saved
}

// Chaining async operations
let result =
    asyncPipeline "input"
    |> Async.RunSynchronously

Common Mistakes

1. Over-piping simple expressions

Don't pipe everything. Simple add 5 3 is clearer than 3 |> add 5.

2. Breaking pipeline for side effects

If you need to debug, extract a named function rather than breaking the chain.

3. Long unreadable pipelines

Break long pipelines into named intermediate values or extract steps into functions.

4. Ignoring error handling in pipelines

Pipelines hide intermediate errors. Use Result types or add validation steps.

5. Performance from many intermediate collections

Each step can create intermediate collections. Use Seq for lazy evaluation where possible.

Practice Questions

1. What does the |> operator do? Passes the left operand as the last argument to the function on the right.

2. Why use pipelines instead of nested calls? Pipelines read in the natural order of data flow (left-to-right), unlike nested calls which read inside-out.

3. How do you debug a pipeline? Inject a tap function that logs intermediate values, or use printfn within a map step.

Challenge: Build a pipeline that reads a file, filters lines, transforms them, and writes results.

FAQ

{{< faq question="Is pipe operator slow?" >}} No. The pipe operator is inlined by the compiler and has zero runtime overhead. It's purely a readability feature. {{< /faq >}}

{{< faq question="Can I pipe into methods?" >}} Yes, but you need to use dot notation or wrap in a function: "hello" |> fun s -> s.ToUpper() or "hello" |> (fun s -> s.ToUpper()). {{< /faq >}}

{{< faq question="What is the reverse pipe?" >}} <| passes the right operand to the left function. Useful for avoiding parentheses: printfn "%d" <| 5 + 3. {{< /faq >}}

{{< faq question="Can I create custom pipeline steps?" >}} Yes. Any function of type 'T -> 'U can be used as a pipeline step. Named functions make the best steps. {{< /faq >}}

{{< faq question="How do I handle errors in pipelines?" >} Use Result types in your pipeline: Ok data >>= Process >>= validate. The >>= operator short-circuits on Error. {{< /faq >}}

Mini Project

Build an ETL pipeline for processing CSV data:

let processCsv (csv: string) =
    csv.Split('\n')
    |> Array.skip 1  // Skip header
    |> Array.map (fun line -> line.Split(','))
    |> Array.filter (fun fields -> fields.Length >= 3)
    |> Array.map (fun fields ->
        {| Name = fields.[0]; Age = int fields.[1]; Score = float fields.[2] |})
    |> Array.filter (fun r -> r.Score > 80.0)
    |> Array.sortByDescending (fun r -> r.Score)
    |> Array.take 10

What's Next

Now that you understand pipelines, explore function composition for building new functions.

Topic Description Link
F# Composition Function composition {{< ref "17-composition" >}}
F# Async Async programming {{< ref "18-async" >}}

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro