F# Guide — Sequences: Lazy Data Streams
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# sequences (seq) are lazy, potentially infinite collections that compute elements on-demand, making them ideal for processing large datasets, generating infinite series, and composing Data Pipelines efficiently.
What You'll Learn
- Creating sequences with seq expressions
- Lazy evaluation behavior
- Sequence transformations
- Working with infinite sequences
- Sequences vs lists vs arrays
Why It Matters
Sequences enable processing datasets larger than memory, generating infinite mathematical series, and composing efficient data pipelines. Durga Antivirus Pro uses sequences for processing log files of any size.
Real-World Use
Reading large files line-by-line, processing database result sets, generating mathematical sequences for scientific computing, and streaming data from network sources.
flowchart LR
A["Sequences"] --> B["seq Expressions"]
B --> C["Lazy Evaluation"]
C --> D["Transformations"]
D --> E["Consumption"]
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
Creating Sequences
// Using seq expression
let numbers = seq { 1; 2; 3; 4; 5 }
// Range
let range = seq { 1..10 }
// Yield with for
let squares = seq {
for i in 1..10 do
yield i * i
}
// Yield many
let combined = seq {
yield 1
yield! [2; 3; 4]
yield 5
}
Lazy Evaluation
// Elements are computed only when needed
let traceSeq = seq {
for i in 1..5 do
printfn "Computing %d" i
yield i * 2
}
// Nothing printed yet - lazy!
// Take first 3
let first3 = traceSeq |> Seq.take 3
// Only "Computing 1", "Computing 2", "Computing 3" printed
// Force evaluation with List.ofSeq
let list = traceSeq |> List.ofSeq
// All 5 computed
Infinite Sequences
// Infinite natural numbers
let naturals = Seq.initInfinite id
// Fibonacci sequence
let fibonacci = Seq.unfold (fun (a, b) -> Some(a, (b, a + b))) (0, 1)
// Take what you need
let first10Fib = fibonacci |> Seq.take 10 |> List.ofSeq
// [0; 1; 1; 2; 3; 5; 8; 13; 21; 34]
Sequence Transformations
let seq1 = seq { 1..10 }
let doubled = seq1 |> Seq.map (fun x -> x * 2)
let evens = seq1 |> Seq.filter (fun x -> x % 2 = 0)
let first5 = seq1 |> Seq.take 5
let skip3 = seq1 |> Seq.skip 3
// Chunking
let chunks = seq1 |> Seq.chunkBySize 3
// [[|1; 2; 3|]; [|4; 5; 6|]; [|7; 8; 9|]; [|10|]]
// Windows (sliding window)
let windows = seq1 |> Seq.windowed 3
// [[|1; 2; 3|]; [|2; 3; 4|]; ...]
File I/O with Sequences
// Process large files line-by-line
let processLargeFile filePath =
seq {
use reader = System.IO.StreamReader(filePath)
while not reader.EndOfStream do
yield reader.ReadLine()
}
// Use as regular sequence
let errorLines =
processLargeFile "app.log"
|> Seq.filter (fun line -> line.Contains("ERROR"))
|> Seq.truncate 100
Caching Sequences
// Avoid recomputing expensive sequences
let expensiveSeq = seq {
for i in 1..1000 do
System.Threading.Thread.Sleep(1) // Simulate work
yield i
}
// Cache after first enumeration
let cached = expensiveSeq |> Seq.cache
// First iteration computes
let first = cached |> Seq.take 10 |> List.ofSeq
// Second iteration uses cache
let second = cached |> Seq.take 20 |> List.ofSeq
Sequencing and Comprehensions
// Nested loops in seq expression
let pairs = seq {
for x in 1..3 do
for y in 1..3 do
yield (x, y)
}
// Conditional yields
let fizzbuzz = seq {
for i in 1..100 do
if i % 15 = 0 then yield "FizzBuzz"
elif i % 3 = 0 then yield "Fizz"
elif i % 5 = 0 then yield "Buzz"
else yield string i
}
Common Mistakes
1. Multiple enumeration
Iterating a sequence multiple times recomputes it. Use Seq.cache or List.ofSeq for reuse.
2. Side effects in lazy code
Side effects execute when the sequence is consumed, not when it's defined. This can cause timing surprises.
3. Holding resources too long
File handles stay open until the sequence is fully consumed. Use use bindings and consume promptly.
4. Forcing the entire sequence
Seq.toList forces full evaluation. For large or infinite sequences, use Seq.truncate or Seq.take.
5. Stack overflow from recursive sequences
Non-tail-recursive sequence generators can overflow. Use Seq.unfold for recursive patterns.
Practice Questions
1. What is lazy evaluation? Elements are computed only when they are accessed, not when the sequence is defined.
2. How do you create an infinite sequence?
Use Seq.initInfinite, Seq.unfold, or a recursive seq { } expression with yield.
3. What does Seq.cache do? It stores computed elements so subsequent iterations don't recompute them.
Challenge: Create an infinite sequence of prime numbers using the Sieve of Eratosthenes.
FAQ
{{< faq question="Are sequences the same as IEnumerable?" >}}
Yes. F# seq is an alias for System.Collections.Generic.IEnumerable<T>. Any .NET collection implementing IEnumerable works as a seq.
{{< /faq >}}
{{< faq question="When should I use seq over list?" >}} Use seq for large data, file streams, lazy computation, and infinite series. Use list for small to medium in-memory collections. {{< /faq >}}
{{< faq question="Can sequences be iterated multiple times?" >}}
By default, each iteration recomputes the sequence. Use Seq.cache to enable efficient multiple iterations.
{{< /faq >}}
{{< faq question="How do I convert between seq and list?" >}}
Use List.ofSeq and List.toSeq. Or Array.ofSeq and Array.toSeq.
{{< /faq >}}
{{< faq question="What is Seq.unfold?" >}} It generates a sequence from a seed value using a function that returns Some (next value, next seed) or None to terminate. {{< /faq >}}
Mini Project
Build a log analysis tool using sequences:
type LogEntry = { Timestamp: System.DateTime; Level: string; Message: string }
let parseLog filePath =
seq {
use reader = System.IO.StreamReader(filePath)
while not reader.EndOfStream do
let line = reader.ReadLine()
let parts = line.Split('|')
yield { Timestamp = System.DateTime.Parse(parts.[0])
Level = parts.[1]
Message = parts.[2] }
}
let getErrors filePath =
parseLog filePath
|> Seq.filter (fun entry -> entry.Level = "ERROR")
|> Seq.truncate 100
|> List.ofSeq
What's Next
Now that you understand sequences, explore records for structured data types.
| Topic | Description | Link |
|---|---|---|
| F# Records | Record types | {{< ref "10-records" >}} |
| F# Discriminated Unions | Union types | {{< ref "11-discriminated-unions" >}} |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro