序列

序列是同类型元素的逻辑系列。 当你拥有一个有序的大型数据集合但不一定希望使用所有元素时,序列特别有用。 系统仅根据需要计算单个序列元素,因此,在不使用所有元素的情况下,序列可以提供比列表更好的性能。 序列由 seq<'T> 类型(IEnumerable<T> 的别名)表示。 因此,任何实现 IEnumerable<T> 接口的 .NET 类型都可以用作序列。 Seq 模块支持涉及序列的操作。

序列表达式

序列表达式是一种计算结果为序列的表达式。 序列表达式可以采用多种形式。 最简单的形式是指定一个范围。 例如,seq { 1 .. 5 } 会创建一个包含五个元素的序列,包括末端的 1 和 5。 你还可以在两个双句点之间指定一个增量(或减量)。 例如,以下代码会创建 10 的倍数序列。

// Sequence that has an increment.
seq { 0..10..100 }

序列表达式由生成序列值的 F# 表达式组成。 你还可以通过编程方式生成值:

seq { for i in 1..10 -> i * i }

前面的示例使用了 -> 运算符,该运算符允许你指定一个表达式,其值将成为序列的一部分。 仅当 -> 后面的代码的每个部分都返回值时,才能使用该运算符。

或者,你可以指定 do 关键字,后跟可选的 yield

seq {
    for i in 1..10 do
        yield i * i
}

// The 'yield' is implicit and doesn't need to be specified in most cases.
seq {
    for i in 1..10 do
        i * i
}

以下代码将生成坐标对列表以及表示网格的数组的索引。 请注意,第一个 for 表达式需要指定 do

let (height, width) = (10, 10)

seq {
    for row in 0 .. width - 1 do
        for col in 0 .. height - 1 -> (row, col, row * width + col)
}

序列中使用的 if 表达式是一个筛选器。 例如,若要生成一个只有素数的序列,假设你有一个 int -> bool 类型的函数 isprime,请按下面所示构造序列。

seq {
    for n in 1..100 do
        if isprime n then
            n
}

如前所述,此处需要 do,因为没有与 if 一起使用的 else 分支。 如果尝试使用 ->,你将收到一条错误消息,指出并非所有分支都返回值。

yield! 关键字

有时,你可能希望将一个元素序列包含到另一个序列中。 若要将一个序列包含到另一个序列中,你需要使用 yield! 关键字:

// Repeats '1 2 3 4 5' ten times
seq {
    for _ in 1..10 do
        yield! seq { 1; 2; 3; 4; 5}
}

我们可以换种方式理解 yield!:它会平展内部序列,然后将其包含在包含序列中。

在表达式中使用 yield! 时,所有其他单个值都必须使用 yield 关键字:

// Combine repeated values with their values
seq {
    for x in 1..10 do
        yield x
        yield! seq { for i in 1..x -> i}
}

对于每个 x,除了从 1x 的所有值之外,前面的示例还将生成 x 的值。

示例

第一个示例使用包含迭代、筛选器和 yield 的序列表达式来生成数组。 此代码将 1 到 100 之间的素数序列打印到控制台。

// Recursive isprime function.
let isprime n =
    let rec check i =
        i > n / 2 || (n % i <> 0 && check (i + 1))

    check 2

let aSequence =
    seq {
        for n in 1..100 do
            if isprime n then
                n
    }

for x in aSequence do
    printfn "%d" x

以下示例创建一个乘法表,其中包含三个元素的元组,每个元组由两个因子和乘积组成:

let multiplicationTable =
    seq {
        for i in 1..9 do
            for j in 1..9 -> (i, j, i * j)
    }

以下示例演示如何使用 yield! 将各个序列组合成一个最终序列。 在本例中,二叉树中每个子树的序列在递归函数中连接,以生成最终序列。

// Yield the values of a binary tree in a sequence.
type Tree<'a> =
    | Tree of 'a * Tree<'a> * Tree<'a>
    | Leaf of 'a

// inorder : Tree<'a> -> seq<'a>
let rec inorder tree =
    seq {
        match tree with
        | Tree(x, left, right) ->
            yield! inorder left
            yield x
            yield! inorder right
        | Leaf x -> yield x
    }

let mytree = Tree(6, Tree(2, Leaf(1), Leaf(3)), Leaf(9))
let seq1 = inorder mytree
printfn "%A" seq1

使用序列

序列支持许多与列表相同的函数。 序列还支持使用键生成函数进行分组和计数等操作。 序列还支持更多样化的子序列提取函数。

许多数据类型(例如列表、数组、集和映射)都是隐式序列,因为它们是可枚举的集合。 除了所有实现 System.Collections.Generic.IEnumerable<'T> 的 .NET 数据类型外,将序列作为参数的函数还适用于所有常见的 F# 数据类型。 这种函数与将列表作为参数的函数相反,后者只能接受列表。 类型 seq<'T>IEnumerable<'T> 的类型缩写。 这意味着,任何实现泛型 System.Collections.Generic.IEnumerable<'T> 的类型(包括 F# 中的数组、列表、集和映射,以及大多数 .NET 集合类型)都与 seq 类型兼容,并且可以在需要序列的任何位置使用。

模块函数

FSharp.Collections 命名空间中的 Seq 模块包含用于处理序列的函数。 这些函数也适用于列表、数组、映射和集,因为所有这些类型都是可枚举的,因此可以视为序列。

创建序列

你可以使用序列表达式(如前所述)或使用某些函数来创建序列。

你可以使用 Seq.empty 创建一个空序列,也可以使用 Seq.singleton 创建一个仅包含一个指定元素的序列。

let seqEmpty = Seq.empty
let seqOne = Seq.singleton 10

你可以使用 Seq.init 创建一个序列,其元素由你提供的函数创建。 同时提供该序列的大小。 此函数与 List.init 类似,只不过它在循环访问序列之前不会创建元素。 以下代码阐释了 Seq.init 的用法。

let seqFirst5MultiplesOf10 = Seq.init 5 (fun n -> n * 10)
Seq.iter (fun elem -> printf "%d " elem) seqFirst5MultiplesOf10

输出为

0 10 20 30 40

通过使用 Seq.ofArraySeq.ofList<'T> Function,可以根据数组和列表创建序列。 但是,你也可以使用强制转换运算符将数组和列表转换为序列。 以下代码展示了这两种方法。

// Convert an array to a sequence by using a cast.
let seqFromArray1 = [| 1 .. 10 |] :> seq<int>

// Convert an array to a sequence by using Seq.ofArray.
let seqFromArray2 = [| 1 .. 10 |] |> Seq.ofArray

通过使用 Seq.cast,可以根据弱类型集合(例如 System.Collections 中定义的集合)创建序列。 这种弱类型集合具有元素类型 System.Object,并使用非泛型 System.Collections.Generic.IEnumerable&#96;1 类型进行枚举。 以下代码阐释了如何使用 Seq.castSystem.Collections.ArrayList 转换为序列。

open System

let arr = ResizeArray<int>(10)

for i in 1 .. 10 do
    arr.Add(10)

let seqCast = Seq.cast arr

可以使用 Seq.initInfinite 函数定义无限序列。 对于这种序列,需要提供一个根据元素索引生成每个元素的函数。 由于延迟计算,可能会出现无限序列;系统会通过调用指定的函数按需创建元素。 以下代码示例将生成一个浮点数无限序列,在本例中,此序列是连续整数平方倒数的交替序列。

let seqInfinite =
    Seq.initInfinite (fun index ->
        let n = float (index + 1)
        1.0 / (n * n * (if ((index + 1) % 2 = 0) then 1.0 else -1.0)))

printfn "%A" seqInfinite

Seq.unfold 根据计算函数生成序列,该函数采用状态并对其进行转换,以生成序列中的每个后续元素。 状态只是用于计算每个元素的值,可以在计算每个元素时改变。 Seq.unfold 的第二个参数是用于启动序列的初始值。 Seq.unfold 对状态使用选项类型,这使你能够通过返回 None 值来终止序列。 以下代码显示了由 unfold 操作生成的两个序列示例 seq1fib。 第一个序列 (seq1) 就是一个最多包含 20 个数字的简单序列。 第二个序列 (fib) 使用 unfold 计算斐波纳契序列。 因为斐波那契序列中的每个元素都是前两个斐波纳契数之和,所以状态值为包含序列中前两个数的元组。 初始值为 (0,1),即序列中的前两个数字。

let seq1 =
    0 // Initial state
    |> Seq.unfold (fun state ->
        if (state > 20) then
            None
        else
            Some(state, state + 1))

printfn "The sequence seq1 contains numbers from 0 to 20."

for x in seq1 do
    printf "%d " x

let fib =
    (0, 1)
    |> Seq.unfold (fun state ->
        let cur, next = state
        if cur < 0 then  // overflow
            None
        else
            let next' = cur + next
            let state' = next, next'
            Some (cur, state') )

printfn "\nThe sequence fib contains Fibonacci numbers."
for x in fib do printf "%d " x

输出如下所示:

The sequence seq1 contains numbers from 0 to 20.

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

The sequence fib contains Fibonacci numbers.

0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 

以下代码是一个示例,它使用本文描述的许多序列模块函数来生成和计算无限序列的值。 该代码可能会运行几分钟。

// generateInfiniteSequence generates sequences of floating point
// numbers. The sequences generated are computed from the fDenominator
// function, which has the type (int -> float) and computes the
// denominator of each term in the sequence from the index of that
// term. The isAlternating parameter is true if the sequence has
// alternating signs.
let generateInfiniteSequence fDenominator isAlternating =
    if (isAlternating) then
        Seq.initInfinite (fun index ->
            1.0 /(fDenominator index) * (if (index % 2 = 0) then -1.0 else 1.0))
    else
        Seq.initInfinite (fun index -> 1.0 /(fDenominator index))

// The harmonic alternating series is like the harmonic series
// except that it has alternating signs.
let harmonicAlternatingSeries = generateInfiniteSequence (fun index -> float index) true

// This is the series of reciprocals of the odd numbers.
let oddNumberSeries = generateInfiniteSequence (fun index -> float (2 * index - 1)) true

// This is the series of recipocals of the squares.
let squaresSeries = generateInfiniteSequence (fun index -> float (index * index)) false

// This function sums a sequence, up to the specified number of terms.
let sumSeq length sequence =
    (0, 0.0)
    |>
    Seq.unfold (fun state ->
        let subtotal = snd state + Seq.item (fst state + 1) sequence
        if (fst state >= length) then
            None
        else
            Some(subtotal, (fst state + 1, subtotal)))

// This function sums an infinite sequence up to a given value
// for the difference (epsilon) between subsequent terms,
// up to a maximum number of terms, whichever is reached first.
let infiniteSum infiniteSeq epsilon maxIteration =
    infiniteSeq
    |> sumSeq maxIteration
    |> Seq.pairwise
    |> Seq.takeWhile (fun elem -> abs (snd elem - fst elem) > epsilon)
    |> List.ofSeq
    |> List.rev
    |> List.head
    |> snd

// Compute the sums for three sequences that converge, and compare
// the sums to the expected theoretical values.
let result1 = infiniteSum harmonicAlternatingSeries 0.00001 100000
printfn "Result: %f  ln2: %f" result1 (log 2.0)

let pi = Math.PI
let result2 = infiniteSum oddNumberSeries 0.00001 10000
printfn "Result: %f pi/4: %f" result2 (pi/4.0)

// Because this is not an alternating series, a much smaller epsilon
// value and more terms are needed to obtain an accurate result.
let result3 = infiniteSum squaresSeries 0.0000001 1000000
printfn "Result: %f pi*pi/6: %f" result3 (pi*pi/6.0)

搜索和查找元素

序列支持可用于列表的功能:Seq.existsSeq.exists2Seq.findSeq.findIndexSeq.pickSeq.tryFindSeq.tryFindIndex。 可用于序列的这些函数的版本仅对序列中所搜索的元素进行计算。 有关示例,请参阅列表

获取子序列

Seq.filterSeq.choose 与可用于列表的相应函数类似,只是在计算序列元素之前不会进行筛选和选择。

Seq.truncate 根据另一个序列创建序列,但将序列限制为指定数量的元素。 Seq.take 创建新序列,该序列仅包含从序列开头起指定数量的元素。 如果序列中的元素比你指定的要少,Seq.take 会引发 System.InvalidOperationExceptionSeq.takeSeq.truncate 之间的区别在于,如果元素的数量少于你指定的数量,Seq.truncate 不会生成错误。

以下代码展示了 Seq.truncateSeq.take 的行为和区别。

let mySeq = seq { for i in 1 .. 10 -> i*i }
let truncatedSeq = Seq.truncate 5 mySeq
let takenSeq = Seq.take 5 mySeq

let truncatedSeq2 = Seq.truncate 20 mySeq
let takenSeq2 = Seq.take 20 mySeq

let printSeq seq1 = Seq.iter (printf "%A ") seq1; printfn ""

// Up to this point, the sequences are not evaluated.
// The following code causes the sequences to be evaluated.
truncatedSeq |> printSeq
truncatedSeq2 |> printSeq
takenSeq |> printSeq
// The following line produces a run-time error (in printSeq):
takenSeq2 |> printSeq

出现错误之前的输出如下所示。

1 4 9 16 25
1 4 9 16 25 36 49 64 81 100
1 4 9 16 25
1 4 9 16 25 36 49 64 81 100

通过使用 Seq.takeWhile,可以指定谓词函数(布尔函数)并根据另一个序列(由原始序列中谓词为 true 的元素组成)创建序列,但在谓词返回 false 的第一个元素前停止。 Seq.skip 返回一个序列,该序列跳过另一个序列中指定数量的第一个元素并返回其余元素。 Seq.skipWhile 返回一个序列,该序列跳过另一个序列中谓词返回 true 的第一个元素,然后返回其余元素(从谓词返回 false 的第一个元素开始)。

以下代码示例阐释了 Seq.takeWhileSeq.skipSeq.skipWhile 的行为和区别。

// takeWhile
let mySeqLessThan10 = Seq.takeWhile (fun elem -> elem < 10) mySeq
mySeqLessThan10 |> printSeq

// skip
let mySeqSkipFirst5 = Seq.skip 5 mySeq
mySeqSkipFirst5 |> printSeq

// skipWhile
let mySeqSkipWhileLessThan10 = Seq.skipWhile (fun elem -> elem < 10) mySeq
mySeqSkipWhileLessThan10 |> printSeq

输出如下所示。

1 4 9
36 49 64 81 100
16 25 36 49 64 81 100

转换序列

Seq.pairwise 创建一个新序列,其中输入序列的连续元素分组为元组。

let printSeq seq1 = Seq.iter (printf "%A ") seq1; printfn ""
let seqPairwise = Seq.pairwise (seq { for i in 1 .. 10 -> i*i })
printSeq seqPairwise

printfn ""
let seqDelta = Seq.map (fun elem -> snd elem - fst elem) seqPairwise
printSeq seqDelta

Seq.windowedSeq.pairwise 类似,只不过它不生成元组序列,而是生成包含序列中相邻元素(窗口)副本的数组序列。 你可以在每个数组中指定所需的相邻元素数量。

以下代码示例演示了 Seq.windowed 的用法。 在本例中,窗口中的元素数为 3。 该示例使用前面代码示例中定义的 printSeq

let seqNumbers = [ 1.0; 1.5; 2.0; 1.5; 1.0; 1.5 ] :> seq<float>
let seqWindows = Seq.windowed 3 seqNumbers
let seqMovingAverage = Seq.map Array.average seqWindows
printfn "Initial sequence: "
printSeq seqNumbers
printfn "\nWindows of length 3: "
printSeq seqWindows
printfn "\nMoving average: "
printSeq seqMovingAverage

输出如下所示。

初始序列:

1.0 1.5 2.0 1.5 1.0 1.5

Windows of length 3:
[|1.0; 1.5; 2.0|] [|1.5; 2.0; 1.5|] [|2.0; 1.5; 1.0|] [|1.5; 1.0; 1.5|]

Moving average:
1.5 1.666666667 1.5 1.333333333

对多个序列的操作

Seq.zipSeq.zip3 采用两个或三个序列并生成一个元组序列。 这些函数类似于可用于列表的相应函数。 没有相应的功能可将一个序列分成两个或更多个序列。 如果需要为序列使用此功能,请将序列转换为列表并使用 List.unzip

排序、比较和分组

列表支持的排序函数也适用于序列。 其中包括 Seq.sortSeq.sortBy。 这些函数循环访问整个序列。

你可以使用 Seq.compareWith 函数比较两个序列。 该函数依次比较连续元素,并在遇到第一个不等对时停止。 所有其他元素都不参与比较。

以下代码显示了 Seq.compareWith 的用法。

let sequence1 = seq { 1 .. 10 }
let sequence2 = seq { 10 .. -1 .. 1 }

// Compare two sequences element by element.
let compareSequences =
    Seq.compareWith (fun elem1 elem2 ->
        if elem1 > elem2 then 1
        elif elem1 < elem2 then -1
        else 0)

let compareResult1 = compareSequences sequence1 sequence2
match compareResult1 with
| 1 -> printfn "Sequence1 is greater than sequence2."
| -1 -> printfn "Sequence1 is less than sequence2."
| 0 -> printfn "Sequence1 is equal to sequence2."
| _ -> failwith("Invalid comparison result.")

在前面的代码中,仅计算和检查第一个元素,结果为 -1。

Seq.countBy 采用一个函数,该函数为每个元素生成一个称为键的值。 通过对每个元素调用此函数,为每个元素生成一个键。 然后,Seq.countBy 返回一个序列,其中包含键值以及生成每个键值的元素的计数。

let mySeq1 = seq { 1.. 100 }

let printSeq seq1 = Seq.iter (printf "%A ") seq1

let seqResult =
    mySeq1
    |> Seq.countBy (fun elem ->
        if elem % 3 = 0 then 0
        elif elem % 3 = 1 then 1
        else 2)

printSeq seqResult

输出如下所示。

(1, 34) (2, 33) (0, 33)

前面的输出显示,原始序列中有 34 个元素生成了键 1,33 个值生成了键 2,33 个值生成了键 0。

你可以通过调用 Seq.groupBy 对序列的元素进行分组。 Seq.groupBy 采用一个序列和一个根据元素生成键的函数。 将针对序列的每个元素执行该函数。 Seq.groupBy 返回一个元组序列,其中每个元组的第一个元素是键,第二个元素是生成该键的元素序列。

以下代码示例演示如何使用 Seq.groupBy 将从 1 到 100 的数字序列划分为三个组,这些组具有不同的键值 0、1 和 2。

let sequence = seq { 1 .. 100 }

let printSeq seq1 = Seq.iter (printf "%A ") seq1

let sequences3 =
    sequences
    |> Seq.groupBy (fun index ->
        if (index % 3 = 0) then 0
        elif (index % 3 = 1) then 1
        else 2)

sequences3 |> printSeq

输出如下所示。

(1, seq [1; 4; 7; 10; ...]) (2, seq [2; 5; 8; 11; ...]) (0, seq [3; 6; 9; 12; ...])

可以通过调用 Seq.distinct 创建一个消除重复元素的序列。 或者,可以使用 Seq.distinctBy,它采用要对每个元素调用的键生成函数。 生成的序列包含原始序列中具有唯一键的元素;后面的元素如果生成与前面元素重复的键,则会被放弃。

以下代码示例阐释了 Seq.distinct 的用法。 Seq.distinct 演示如下:生成表示二进制数字的序列,然后显示唯一不同的元素是 0 和 1。

let binary n =
    let rec generateBinary n =
        if (n / 2 = 0) then [n]
        else (n % 2) :: generateBinary (n / 2)

    generateBinary n
    |> List.rev
    |> Seq.ofList

printfn "%A" (binary 1024)

let resultSequence = Seq.distinct (binary 1024)
printfn "%A" resultSequence

以下代码演示了 Seq.distinctBy,该函数从包含负数和正数的序列开始,并使用绝对值函数作为键生成函数。 生成的序列缺少与序列中的负数相对应的所有正数,因为负数出现在序列的前面,因此选择了负数,而不是具有相同绝对值或键的正数。

let inputSequence = { -5 .. 10 }
let printSeq seq1 = Seq.iter (printf "%A ") seq1

printfn "Original sequence: "
printSeq inputSequence

printfn "\nSequence with distinct absolute values: "
let seqDistinctAbsoluteValue = Seq.distinctBy (fun elem -> abs elem) inputSequence
printSeq seqDistinctAbsoluteValue

只读和缓存序列

Seq.readonly 创建序列的只读副本。 当你有一个读写集合(例如数组)并且不想修改原始集合时,Seq.readonly 很有用。 该函数可用于保存数据封装。 以下代码示例创建了一个包含数组的类型。 属性公开数组,但不返回数组,而是返回使用 Seq.readonly 根据数组创建的序列。

type ArrayContainer(start, finish) =
    let internalArray = [| start .. finish |]
    member this.RangeSeq = Seq.readonly internalArray
    member this.RangeArray = internalArray

let newArray = new ArrayContainer(1, 10)
let rangeSeq = newArray.RangeSeq
let rangeArray = newArray.RangeArray

// These lines produce an error:
//let myArray = rangeSeq :> int array
//myArray[0] <- 0

// The following line does not produce an error.
// It does not preserve encapsulation.
rangeArray[0] <- 0

Seq.cache 创建序列的存储版本。 使用 Seq.cache 可避免重新计算序列;当你有多个线程使用序列,但必须确保每个元素仅被处理一次时,也可以使用该函数。 当某个序列被多个线程使用时,可以让一个线程枚举和计算原始序列的值,而其余线程可以使用缓存的序列。

对序列执行计算

简单的算术运算与列表的算术运算类似,例如 Seq.averageSeq.sumSeq.averageBySeq.sumBy,等等。

Seq.foldSeq.reduceSeq.scan 与可用于列表的相应函数类似。 序列支持的函数是列表支持的这些函数的完整变体子集。 有关详细信息和示例,请参阅列表

另请参阅