F# 시퀀스
F# 시퀀스 (Sequences)
시퀀스(sequence)는 모두 같은 타입의 요소들이 논리적으로 이어진 연속된 값이에요. 특히 데이터가 많고 순서가 있지만, 그 요소를 전부 사용할 필요는 없을 때 아주 유용한데요. 시퀀스의 개별 요소는 필요할 때만 계산되기 때문에, 모든 요소를 실제로 쓰지 않는 상황에서는 리스트(list)보다 더 나은 성능을 보여줘요. 시퀀스는 seq<'T> 타입으로 표현되며, 이 타입은 IEnumerable<T>의 별칭(alias)이에요. 그래서 IEnumerable<T> 인터페이스를 구현하는 어떤 .NET 타입이든 시퀀스로 쓸 수 있고, Seq 모듈이 시퀀스를 다루는 여러 기능을 제공해요.
출처: https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/sequences
본문
시퀀스 식 (Sequence expressions)
시퀀스 식(sequence expression)은 시퀀스로 평가되는 표현식이에요. 시퀀스 식에는 여러 형태가 있는데, 가장 간단한 형태는 범위(range)를 지정하는 거예요. 예를 들어 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
}
다음 코드는 배열에 들어 있는 좌표 쌍(grid)의 목록을, 배열의 인덱스와 함께 생성해요. 첫 번째 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 식은 필터(filter) 역할을 해요. 예를 들어 int -> bool 타입의 isprime 함수가 있다고 가정하고, 소수(prime number)만으로 이루어진 시퀀스를 만들려면 다음과 같이 구성하면 돼요.
seq {
for n in 1..100 do
if isprime n then
n
}
앞서 말했듯이 여기에서는 if에 짝을 이루는 else 분기가 없기 때문에 do가 필요해요. ->를 쓰려고 하면 모든 분기가 값을 반환하지 않는다는 오류가 나요.
yield! 키워드
때로는 시퀀스의 요소들을 다른 시퀀스 안에 포함시키고 싶을 때가 있어요. 시퀀스 안에 다른 시퀀스를 포함하려면 yield! 키워드를 사용해야 해요.
// Repeats '1 2 3 4 5' ten times
seq {
for _ in 1..10 do
yield! seq { 1; 2; 3; 4; 5}
}
yield!의 다른 해석으로는, 안쪽 시퀀스를 평탄화(flatten)한 다음 그 내용을 포함하는 시퀀스에 포함시킨다고 볼 수 있어요.
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에 대해 x라는 값과 함께 1부터 x까지의 모든 값을 만들어내요.
예시 (Examples)
첫 번째 예시는 반복(iteration), 필터(filter), 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
다음 예시는 두 개의 인수(factor)와 그 곱(product)으로 이루어진 세 요소 튜플의 곱셈표를 만들어요.
let multiplicationTable =
seq {
for i in 1..9 do
for j in 1..9 -> (i, j, i * j)
}
다음 예시는 yield!를 사용해 개별 시퀀스들을 하나의 최종 시퀀스로 결합하는 방법을 보여줘요. 이 경우 이진 트리(binary tree)의 각 하위 트리(subtree)에 대한 시퀀스를 재귀 함수에서 이어 붙여 최종 시퀀스를 만들어내요.
// 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
시퀀스 사용하기 (Using Sequences)
시퀀스는 리스트가 지원하는 많은 함수들을 그대로 지원해요. 시퀀스는 또한 키(key)를 생성하는 함수를 이용해 그룹화(grouping)나 개수 세기(counting) 같은 연산도 지원해요. 그리고 부분 시퀀스(subsequence)를 추출하는 더 다양한 함수들도 지원해요.
리스트, 배열, 집합(set), 맵(map) 같은 많은 데이터 타입은 열거 가능한 컬렉션(enumerable collection)이기 때문에 사실상 시퀀스예요. 시퀀스를 인자로 받는 함수는 System.Collections.Generic.IEnumerable<'T>를 구현하는 어떤 .NET 데이터 타입뿐만 아니라 일반적인 F# 데이터 타입과도 함께 동작해요. 반면 리스트를 인자로 받는 함수는 오직 리스트만 받을 수 있다는 점과 대조돼요. seq<'T> 타입은 IEnumerable<'T>의 타입 약어(type abbreviation)예요. 즉 F#의 배열, 리스트, 집합, 맵과 대부분의 .NET 컬렉션 타입을 포함해 제네릭 System.Collections.Generic.IEnumerable<'T>를 구현하는 어떤 타입이든 seq 타입과 호환되며, 시퀀스가 기대되는 자리 어디든 사용할 수 있어요.
모듈 함수 (Module Functions)
FSharp.Collections 네임스페이스의 Seq 모듈에는 시퀀스를 다루는 함수들이 들어 있어요. 이 함수들은 리스트, 배열, 맵, 집합에서도 그대로 동작하는데, 이 타입들이 모두 열거 가능하기 때문에 시퀀스로 취급될 수 있기 때문이에요.
시퀀스 만들기 (Creating Sequences)
앞에서 설명한 시퀀스 식을 사용하거나, 특정 함수를 사용해 시퀀스를 만들 수 있어요.
Seq.empty로 빈 시퀀스를 만들 수 있고, Seq.singleton으로 지정한 요소 하나만 가진 시퀀스를 만들 수 있어요.
let seqEmpty = Seq.empty
let seqOne = Seq.singleton 10
Seq.init는 여러분이 제공한 함수로 요소를 만들어내는 시퀀스를 만들 때 사용해요. 시퀀스의 크기(size)도 함께 지정해요. 이 함수는 List.init와 비슷하지만, 차이점은 시퀀스를 반복(iterate)할 때까지 요소가 만들어지지 않는다는 거예요. 다음 코드는 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.ofArray와 Seq.ofList<'T> 함수를 사용하면 배열과 리스트에서 시퀀스를 만들 수 있어요. 그런데 캐스트 연산자(cast operator)를 사용해서 배열과 리스트를 시퀀스로 변환할 수도 있어요. 다음 코드에서 두 가지 방법을 모두 보여줘요.
// 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에 정의된 것 같은 약한 타입(weakly typed) 컬렉션에서 시퀀스를 만들 수 있어요. 이런 약하게 타입된 컬렉션은 요소 타입이 System.Object이고, 제네릭이 아닌 System.Collections.Generic.IEnumerable`1 타입으로 열거돼요. 다음 코드는 Seq.cast로 System.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 함수로 무한 시퀀스(infinite sequence)를 정의할 수 있어요. 이 경우 요소의 인덱스로부터 각 요소를 만들어내는 함수를 제공해요. 무한 시퀀스는 지연 평가(lazy evaluation) 덕분에 가능한데, 요소들은 여러분이 지정한 함수가 호출되면서 필요할 때 만들어져요. 다음 코드 예시는 부동 소수점 숫자의 무한 시퀀스를 만들어내는데, 이 경우는 연속된 정수들의 제곱의 역수의 교대 급수(alternating series)예요.
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는 상태(state)를 받아 각 후속 요소를 만들어내도록 변환하는 계산 함수로부터 시퀀스를 생성해요. 여기서 상태는 각 요소를 계산하는 데 사용되는 값일 뿐이며, 각 요소가 계산될 때마다 바뀔 수 있어요. Seq.unfold의 두 번째 인자는 시퀀스를 시작하는 데 사용되는 초기 값이에요. Seq.unfold는 상태에 옵션 타입(option type)을 사용하는데, 이 덕분에 None 값을 반환해 시퀀스를 종료할 수 있어요. 다음 코드는 unfold 연산으로 생성된 두 시퀀스 seq1과 fib를 보여줘요. 첫 번째 seq1은 20까지의 숫자를 가진 간단한 시퀀스고, 두 번째 fib는 unfold로 피보나치(Fibonacci) 수열을 계산해요. 피보나치 수열의 각 요소는 앞선 두 피보나치 수의 합이기 때문에, 상태 값은 수열의 앞선 두 숫자로 이루어진 튜플이에요. 초기 값은 수열의 첫 두 숫자인 (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 =
sequence
|> Seq.skip 1 // skip first item (matching the original behavior)
|> Seq.truncate length // don't take more than length items
|> Seq.scan (+) 0.0 // generate running sums
|> Seq.skip 1 // skip the initial 0.0 from sequence of running sums
// 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)
요소 검색하기 (Searching and Finding Elements)
시퀀스는 리스트에서 제공하는 기능을 지원해요: Seq.exists, Seq.exists2, Seq.find, Seq.findIndex, Seq.pick, Seq.tryFind, Seq.tryFindIndex가 그것이에요. 시퀀스에서 사용 가능한 이 함수들의 버전은 찾고 있는 요소까지만 시퀀스를 평가해요. 예시는 Lists(리스트)를 참고해요.
부분 시퀀스 얻기 (Obtaining Subsequences)
Seq.filter와 Seq.choose는 리스트에서 사용 가능한 대응 함수들과 비슷하지만, 필터링과 선택(choosing)이 시퀀스 요소가 평가될 때까지 일어나지 않는다는 차이가 있어요.
Seq.truncate는 다른 시퀀스에서 시퀀스를 만들되, 시퀀스를 지정한 개수의 요소로 제한해요. Seq.take는 시퀀스의 시작 부분에서 지정한 개수의 요소만 담긴 새 시퀀스를 만들어요. 시퀀스에 가져오려는 개수보다 요소가 적으면 Seq.take는 System.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을 사용하면 서술 함수(predicate function, 불리언 함수)를 지정해, 원본 시퀀스에서 그 서술이 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
시퀀스 변환하기 (Transforming Sequences)
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.windowed는 Seq.pairwise와 비슷하지만, 튜플의 시퀀스를 만드는 대신 시퀀스에서 인접한 요소들의 복사본(창, window)을 담은 배열들의 시퀀스를 만들어요. 각 배열에 몇 개의 인접 요소를 담을지 지정해요.
다음 코드 예시는 Seq.windowed의 사용을 보여줘요. 이 경우 창(window) 안의 요소 수는 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
출력 결과는 다음과 같아요.
Initial sequence:
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
여러 시퀀스 연산 (Operations with Multiple Sequences)
Seq.zip과 Seq.zip3은 두 개 또는 세 개의 시퀀스를 받아 튜플의 시퀀스를 만들어요. 이 함수들은 리스트에서 사용 가능한 대응 함수들과 비슷해요. 하나의 시퀀스를 두 개 이상의 시퀀스로 나누는 대응 기능은 없어요. 시퀀스에 이 기능이 필요하다면 시퀀스를 리스트로 변환하고 List.unzip을 사용해요.
정렬, 비교, 그룹화 (Sorting, Comparing, and Grouping)
리스트에서 지원되는 정렬 함수들은 시퀀스에서도 동작해요. 여기에는 Seq.sort와 Seq.sortBy가 포함돼요. 이 함수들은 시퀀스 전체를 반복(iterate)해요.
두 시퀀스를 비교할 때는 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
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의 사용을 보여줘요. 이진수(binary number)를 나타내는 시퀀스를 생성하고, 고유한 요소가 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 "Original sequence: "
printSeq inputSequence
printfn "\nSequence with distinct absolute values: "
let seqDistinctAbsoluteValue = Seq.distinctBy (fun elem -> abs elem) inputSequence
printSeq seqDistinctAbsoluteValue
읽기 전용 및 캐시된 시퀀스 (Readonly and Cached Sequences)
Seq.readonly는 시퀀스의 읽기 전용(read-only) 복사본을 만들어요. Seq.readonly는 배열 같은 읽기-쓰기(read-write) 컬렉션을 가지고 있고 원본 컬렉션을 수정하고 싶지 않을 때 유용해요. 이 함수는 데이터 캡슐화(encapsulation)를 지키는 데 사용할 수 있어요. 다음 코드 예시에서 배열을 담고 있는 타입이 만들어져요. 속성(property)이 배열을 노출하는데, 배열을 반환하는 대신 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는 시퀀스의 저장된(stored) 버전을 만들어요. 시퀀스의 재평가(reevaluation)를 피하고 싶을 때, 또는 여러 스레드가 시퀀스를 사용할 때 각 요소가 한 번만 처리되도록 해야 하는 경우에 Seq.cache를 사용해요. 여러 스레드가 사용하는 시퀀스가 있다면 한 스레드가 원본 시퀀스의 값을 열거하고 계산하고, 나머지 스레드는 캐시된 시퀀스를 사용하게 할 수 있어요.
시퀀스 계산 수행하기 (Performing Computations on Sequences)
Seq.average, Seq.sum, Seq.averageBy, Seq.sumBy 같은 간단한 산술 연산은 리스트의 것과 비슷해요.
Seq.fold, Seq.reduce, Seq.scan은 리스트에서 사용 가능한 대응 함수들과 비슷해요. 시퀀스는 리스트가 지원하는 이 함수들의 전체 변형 중 일부만 지원해요. 자세한 내용과 예시는 Lists(리스트)를 참고해요.