序列 (F#)

更新:2010 年 5 月

「序列」(Sequence) 是一連串的邏輯項目,所有項目都是相同型別。 如果您有大型且排序過的資料集合,但不需要使用所有項目時,序列就特別有用。 個別序列項目只在需要時才會進行計算,因此在不使用所有項目的情況下,序列提供的效能優於清單。 序列是透過 seq<'T> 型別所表示,這是 IEnumerable<T> 的別名。 因此,可以將實作 System.IEnumerable 的任何 .NET Framework 型別當成序列使用。 序列模組支援與序列相關的操作。

序列運算式

「序列運算式」(Sequence Expression) 是評估為序列的運算式。 序列運算式可以採用數種形式。 最簡單的形式會指定範圍。 例如,seq { 1 .. 5 } 建立含有五個項目的序列 (包含端點 1 和 5)。 您可以指定兩個雙句點之間的增量 (或減量)。 例如,下列程式碼會建立 10 的倍數的序列。

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

序列運算式是由產生序列之值的 F# 運算式所構成。 它們可以使用 yield 關鍵字產生會變成系列一部分的值。

範例如下。

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

您可以使用 -> 運算子,而非 yield,在此情況下,您可以省略 do 關鍵字 (如下列範例所示)。

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

下列程式碼會將座標組清單及索引產生為表示格線的陣列。

let (height, width) = (10, 10)
seq { for row in 0 .. width - 1 do
         for col in 0 .. height - 1 do
           yield (row, col, row*width + col)
    }

序列中使用的 if 運算式是一種篩選器。 例如,若要產生只有質數的序列,且假設您有型別為 int -> bool 的函式 isprime,則請如下建構序列。

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

當您在反覆項目中使用 yield 或 -> 時,每個反覆項目都必須產生序列的一個項目。 如果每個反覆項目都產生由多個元素所組成的序列,請使用 yield!。 在此情況下,會串連每個反覆項目上產生的項目,然後產生最終序列。

您可以使用序列運算式來合併多個運算式。 每個運算式所產生的項目都會串連在一起。 如需範例,請參閱本主題的<範例>章節。

範例

第一個範例使用含有反覆項目、篩選器和 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 yield n }
for x in aSequence do
    printfn "%d" x

下列程式碼使用 yield 來建立九九乘法表,其中包含由三個項目所組成的 Tuple,這三個項目各包含兩個因數和一個乘積。

let multiplicationTable =
  seq { for i in 1..9 do
            for j in 1..9 do
               yield (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

使用序列

序列支援許多與清單相同的函式。 序列也使用索引鍵產生函式,支援分組和計算這類作業。 序列也支援較多種函式來擷取子序列。

多種資料類型 (例如清單、陣列、集合 (Set) 和對應) 都是隱含的序列,原因是它們都是可列舉的集合 (Collection)。 除了實作 IEnumerable<T> 的任何 .NET Framework 資料類型之外,採用序列做為引數的函式也可以與任何通用 F# 資料類型搭配運作。 與此相反的是採用清單做為引數的函式,它只可採用清單。 型別 seq<'a> 是 IEnumerable<'a> 的型別縮寫。 這表示任何實作泛型 IEnumerable<T> 的型別 (包含 F# 中的陣列、清單、集合 (Set) 和對應,也包含大部分 .NET Framework 集合 (Collection) 型別) 都與 seq 型別相容,而且可以用於需要序列的位置。

模組函式

Microsoft.FSharp.Collections 命名空間中的序列模組包含與序列搭配使用的函式。 這些函式也可以與清單、陣列、對應和集合 (Set) 搭配使用,原因是這些型別都是可列舉的,因此可以視為序列。

建立序列

您可以使用序列運算式 (如前所述) 或使用特定函式,來建立序列。

您可以使用 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> 函式 (F#),您可以透過陣列和清單建立序列。 不過,您也可以使用轉型運算子,將陣列和清單轉換為序列。 下列程式碼同時顯示這兩種技術。

// 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 的項目)。 這類弱式型別集合的項目型別為 Object,而且使用非泛型 IEnumerable<T> 型別予以列舉。 下列程式碼說明如何使用 Seq.cast,將 ArrayList 轉換為序列。

open System
let mutable arrayList1 = new System.Collections.ArrayList(10)
for i in 1 .. 10 do arrayList1.Add(10) |> ignore
let seqCast : seq<int> = Seq.cast arrayList1

您可以使用 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 作業所產生的兩個序列範例 (seq1 和 fib)。 第一個序列 (seq1) 只是數目最多 100 個的簡單序列。 第二個序列 (fib) 則使用 unfold 計算 Fibonacci 序列。 因為 Fibonacci 序列的每個項目都是前兩個 Fibonacci 數字的總和,所以狀態值是由序列中前兩個數字所組成的 Tuple。 初始值是 (1,1),即序列中的前兩個數字。

let seq1 = Seq.unfold (fun state -> if (state > 20) then None else Some(state, state + 1)) 0
printfn "The sequence seq1 contains numbers from 0 to 20."
for x in seq1 do printf "%d " x
let fib = Seq.unfold (fun state ->
    if (snd state > 1000) then None
    else Some(fst state + snd state, (snd state, fst state + snd state))) (1,1)
printfn "\nThe sequence fib contains Fibonacci numbers."
for x in fib do printf "%d " x

其輸出如下:

序列 seq1 包含從 0 到 20 的數字。

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

序列 fib 包含 Fibonacci 的數字。

2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597

下列程式碼範例使用這裡所說明的多個序列模組函式,以產生和計算無限序列的值。 這個程式碼可能需要幾分鐘的時間執行。

// infiniteSequences.fs
// 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 series is the series of reciprocals of whole numbers.
let harmonicSeries = generateInfiniteSequence (fun index -> float index) false
// 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 =
    Seq.unfold (fun state ->
        let subtotal = snd state + Seq.nth (fst state + 1) sequence
        if (fst state >= length) then None
        else Some(subtotal,(fst state + 1, subtotal))) (0, 0.0)

// 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 會擲回 InvalidOperationException。 Seq.take 與 Seq.truncate 的差異在於如果項目數少於您指定的數目,Seq.truncate 並不會產生錯誤。

下列程式碼顯示 Seq.truncate 和 Seq.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.takeWhile、Seq.skip 和 Seq.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 會建立新的序列,這個序列會將輸入序列的連續項目分組為 Tuple。

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

Seq.windowed 和 Seq.pairwise 相同,差異在於它會產生內含序列中相鄰項目之複本的陣列序列 (「視窗」 (Window)),而不是產生 Tuple 序列。 您可以指定每個陣列中想要的相鄰項目數。

下列程式碼範例會示範 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 採用兩個或三個序列,並產生 Tuple 序列。 這些函式與適用於清單的對應函式類似。 並沒有對應的功能可以將一個序列分成兩個以上的序列。 如果您的序列需要此功能,請將該序列轉換為清單,並使用 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 採用的函式會為每個項目產生「索引鍵」(Key) 值。 在每個項目上呼叫此函式,就會產生每個項目的索引鍵。 Seq.countBy 接著會傳回內含索引鍵值的序列,以及已產生所有索引鍵值的項目數計數。

let mySeq1 = seq { 1.. 100 }
let printSeq seq1 = Seq.iter (printf "%A ") seq1; printfn ""
let seqResult = Seq.countBy (fun elem -> if elem % 3 = 0 then 0
                                         elif elem % 3 = 1 then 1
                                         else 2) mySeq1

printSeq seqResult

輸出如下。

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

前一個輸出顯示已產生索引鍵 1 的原始序列有 34 個項目、已產生索引鍵 2 的原始序列有 33 個值,而已產生索引鍵 0 的原始序列有 33 個值。

您可以呼叫 Seq.groupBy,將序列的項目進行分組。 Seq.groupBy 採用序列以及透過項目產生索引鍵的函式。 此函式會在序列的每個項目上執行。 Seq.groupBy 會傳回 Tuple 序列,而每個 Tuple 的第一個項目是索引鍵,第二個項目則是產生該索引鍵之項目的序列。

下列程式碼範例顯示如何使用 Seq.groupBy,將 1 到 100 個數目的序列分割為三個具有不同索引鍵值 0、1 和 2 的群組。

let sequence = seq { 1 .. 100 }
let printSeq seq1 = Seq.iter (printf "%A ") seq1; printfn ""
let sequences3 = Seq.groupBy (fun index ->
                                if (index % 3 = 0) then 0
                                  elif (index % 3 = 1) then 1
                                  else 2) sequence
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。 產生代表二進位數字的序列,然後顯示唯一的差異項目 0 和 1,以示範 Seq.distinct。

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 ""
printfn "Original sequence: "
printSeq inputSequence
printfn "\nSequence with distinct absolute values: "
let seqDistinctAbsoluteValue = Seq.distinctBy (fun elem -> abs elem) inputSequence
seqDistinctAbsoluteValue |> printSeq

唯讀和已快取序列

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 與適用於清單的對應函式類似。 序列支援清單所支援之函式的完整變化子集。 如需詳細資訊與範例,請參閱清單 (F#)

請參閱

參考

IEnumerable<T>

其他資源

F# 語言參考

F# 型別

變更記錄

日期

記錄

原因

2010 年 5 月

改善一些程式碼範例。

資訊加強。