DEV Community

CloudHolic
CloudHolic

Posted on

F# Tutorial (4) - Collection

이번 글에선 F#에서 제공하는 collection에 대해 알아보겠습니다.
F#에서 지원하는 collection은 크게 List, Array, Sequence의 3종류가 있습니다.

List

List는 순서가 있고 immutable한 같은 type의 원소에 대한 collection입니다. 내부적으로 Singly linked list로 구현되어 있으며, 따라서 iteration은 효율적이지만 random access에는 그리 적합하지 않습니다.

F#에서 list는 'T list라는 type을 가지며 []를 사용해서 정의합니다.

let list1 = []          // empty list
let list2 = [ 1; 2; 3; ]    // 1, 2, 3을 원소로 갖는 list
let list3 = [           // 여러 줄에 걸쳐 정의할 수도 있습니다.
    1
    2
    3
]
let list4 = [ 1 .. 1000 ]       // 1~1000까지의 정수를 원소로 갖는 리스트
Enter fullscreen mode Exit fullscreen mode

List는 ::(cons) 연산자를 통해 원소를 list에 연결할 수 있습니다.
List를 list에 연결하고 싶으면 @연산자를 대신 사용합니다.

let list1 = [ 1; 2; 3; ]
let list2 = 100 :: list1    // list2 = [ 100; 1; 2; 3; ]
let list3 = list1 @ list2   // list3 = [ 1; 2; 3; 100; 1; 2; 3; ]
Enter fullscreen mode Exit fullscreen mode

주의할 점은, list는 immutable한 collection이기 때문에 list에 대한 수정은 반드시 새 list를 만든다는 점입니다. 절대 기존 list를 수정해주지 않습니다.

List는 Head와 Tail이라는 개념이 있습니다. Head는 list의 첫 번째 원소를 뜻하며, Tail은 그 나머지 전부를 모은 list를 뜻합니다. F#에서는 Head와 Tail이라는 property로 이를 지원합니다. 그 외에도 지원하는 property는 다음과 같습니다.

Property Type 설명
Head 'T 첫 번째 원소
Tail 'T list 첫 번째 원소를 제외한 나머지 원소들의 list
Empty 'T list 동일한 type의 원소를 갖는 빈 list
IsEmpty bool list가 비어있으면 true
Item 'T 특정 index의 원소 (zero-based)
Length int list의 원소의 개수

이미 한 번 언급했듯이, F#의 list에선 random access에 O(N)의 시간이 필요합니다. 따라서 List.Item(n)은 그다지 빠르지 않습니다.

배열(Array)

배열은 고정된 크기의 mutable한 같은 type의 원소에 대한 collection입니다. 타 언어에서도 흔히 볼 수 있는 배열과 동일하며, 따라서 빠른 random access를 지원합니다.

F#에서 배열은 'T array라는 type을 가지며, [| |]를 사용해서 정의합니다.

let array1 = Array.empty    // empty array
let array2 = [| 1; 2; 3; |] // 1, 2, 3을 원소로 갖는 배열
let array3 = [|         // 배열도 여러 줄에 걸쳐 정의할 수 있습니다.
    1
    2
    3
|]
let array4 : int array = Array.zeroCreate 10    // 0을 10개 갖는 array
Enter fullscreen mode Exit fullscreen mode

배열은 []를 통해 특정 원소에 접근할 수 있습니다. 늘 그렇듯 zero-base입니다.

let array1 = [| 1; 2; 3; 4; 5; |]
let a = array1[1]       // a = 2
let array2 = array1[1..3]   // array2 = [| 2; 3; 4; |]
let array3 = array1[..2]    // array3 = [| 1; 2; 3; |]
let array4 = array1[4..]    // array4 = [| 5; |]
Enter fullscreen mode Exit fullscreen mode

여기에서도 ..를 사용하여 범위를 지정할 수 있으며, 이를 slicing이라고 부릅니다. 주의할 점은 slicing을 사용할 경우, 실질적으로 해당되는 원소가 단 1개뿐이더라도 새 배열로 복사되어서 리턴한다는 점입니다. 이 경우에는 원본 배열을 변형시키지 않습니다.
단일 index를 사용하여 배열에 접근한 경우, <-로 해당 원소의 값을 수정할 수 있습니다.
배열의 접근/수정의 경우 Array.get, Array.set 함수로도 동일한 작업을 수행 할 수 있으며, Array.fill 함수를 사용해 여러 원소의 값을 일괄적으로 바꿀 수도 있습니다.

let array1 = [| 1..25 |]
Array.fill array1 2 20 0    // array1의 2~20번째 원소의 값을 0으로 변경
Array.get array1 2      // array1의 2번째 원소의 값 = 0
Array.set array1 1 10       // array1의 1번째 원소의 값을 10으로 변경
Enter fullscreen mode Exit fullscreen mode

배열은 mutable하지만 fixed-size임에 주의해야 합니다. 이미 있는 원소의 값을 바꾸는건 자유롭지만 크기를 늘리는건 불가능합니다. append를 수행할 경우 다른 collection과 마찬가지로 원본의 변경 없이 새 배열을 만들어 리턴합니다.

다른 언어와 마찬가지로 F#에서는 다차원 배열을 지원합니다. 하지만 [| |]를 사용하는 것이 아니라 array2D, array3D, array4D를 사용해야만 합니다. 5차원 이상의 배열이 필요할 경우 직접 만들어야 합니다.

let array2d1 = array2D [ [ 1; 0; ]; [0; 1; ] ]
Enter fullscreen mode Exit fullscreen mode

물론 [| |]를 사용하여 배열을 원소로 갖는 배열을 만들 수 있지만, index의 접근방법에서 다소 차이가 납니다.

let array1 = [| [| 1; 0; |]; [| 0; 1; |]; |]
let array2 = array2D [ [ 1; 0; ]; [ 0; 1; ] ]
array1[0][0]        // 1
array2[0, 0]        // 1
Enter fullscreen mode Exit fullscreen mode

2차원 배열의 type은 'T[,]라고 정의되며, 3차원 배열은 'T[,,]로 정의됩니다. 그 이상의 차원에 대해서도 동일한 규칙이 적용됩니다.
각각의 차원에 대해 slicing을 적용할 수 있으며, 추가적으로 *을 통해 해당 차원의 모든 index에 대응시킬 수 있습니다. 그 외에는 1차원 배열과 동일한 규칙으로 적용됩니다.

matrix1[1.., *]
matrix1[1..3, *]
matrix1[*, 1..3]
matrix1[1..3, 1..3]
Enter fullscreen mode Exit fullscreen mode

Sequence

Sequence는 동일한 type의 원소에 대한 논리적인 collection입니다. 다른 collection과는 다르게, sequence는 lazy evaluation을 지원합니다. 즉, 각각의 원소는 필요할 때 계산되며, 이 때문에 list나 array에 비해 요구하는 총 원소의 개수는 많지만 그들 전부가 필요하지는 않은 경우에 유용하게 사용할 수 있습니다.

F#에서 sequence는 seq<T'>라는 type을 가지며, seq { }를 사용해서 정의합니다.

let seq1 = Seq.empty                // empty sequence
let seq2 = seq { 1 .. 5 }           // 1 ~ 5를 원소로 갖는 sequence
let seq3 = seq { yield "Hello"; yield "World"; }// "Hello", "World" 원소로 갖는 sequence
Enter fullscreen mode Exit fullscreen mode

List나 배열과는 다르게 원소를 저장할 때 yeild를 사용합니다. 이에 대한 내용은 나중에 다루겠습니다. 지금은 yield를 사용해서 선언한다고만 알아두시면 됩니다.

Sequence의 경우 lazy evaluation이 기본적으로 지원되는 collection이기 때문에 '무한'을 다루는 데 적합합니다.

let seq1 = Seq.singleton 10 // 모든 원소가 10인 sequence
let seq2 = Seq.take 5 seq1  // seq { 10; 10; 10; 10; 10; }
Enter fullscreen mode Exit fullscreen mode

위 코드에서 seq1은 길이가 딱히 정해져 있지 않습니다. 계산을 요청할 때마다 10을 리턴할 뿐이죠. Seq.take 혹은 Seq.truncate 함수를 사용하여 원소 몇 개를 가져오거나 Seq.skip 함수를 사용하여 몇 개를 건너뛸 수 있습니다.

내장 함수들

각각의 collection에는 다양한 내장 함수들이 존재합니다. 각 collection에서 지원하는 모든 함수들의 목록은 List, Array, Sequence를 참고하시면 됩니다. 여기서는 그 중 간단하면서도 자주 쓰이는 일부만 다루며, 나머지 중 일부는 이후의 글에서 다루겠습니다. Collection의 종류에 따라 지원하는 함수의 종류가 조금씩 다르지만, 아래에서 다루는 함수들은 list, 배열, sequence에서 전부 지원하며, 함수의 형태는 각각 List.func, Array.func, Seq.func의 형태입니다.

1) exists / forall
같이 제시된 함수의 조건을 만족하는 원소가 있는지를 체크하며 해당하는 원소가 하나라도 있다면 그 결과는 true가 됩니다.

let list1 = [ 0..3 ]
let result = List.exists (fun e -> e = 4) list1 // false
Enter fullscreen mode Exit fullscreen mode

위의 예시에서는 list1에서 4를 가진 원소가 있는지를 체크하며, 그러한 원소가 없으니 결과는 false입니다.

forall의 경우 exists와는 다르게 모든 원소에 대해 주어진 조건을 통과하는지 체크합니다. 하나라도 실패한 원소가 있다면 그 결과는 false가 됩니다.

2) sort / sortBy
Collection의 원소를 정렬합니다.

let list1 = List.sort [1; -4; 8; 2; 5]              // [-4; 1; 2; 5; 8]
let list2 = List.sortBy (fun e -> abs e) [1; -4; 8; 2; 5]   // [1; 2; -4; 5; 8]
Enter fullscreen mode Exit fullscreen mode

기본적인 sort 함수는 오름차순으로 정렬합니다. 만일 크기 비교가 불가능한 type이거나 별도의 기준이 필요할 경우, sortBy 함수를 통해 크기 비교의 기준이 될 함수를 같이 제공할 수 있습니다.

3) find / tryFind
Collection에서 주어진 기준에 만족하는 첫 번째 원소를 찾습니다. 만일 해당하는 원소가 하나도 없다면 System.Collections.Generic.KeyNotFoundException 예외를 발생시킵니다.

let result = List.find (fun e -> e % 5 = 0) [1..100]    // 5
Enter fullscreen mode Exit fullscreen mode

위의 코드는 [1..100]에서 가장 먼저 등장하는 5의 배수를 찾으며, 그 결과는 5입니다.

tryFind함수를 사용하게 되면 예외를 발생시키지 않을 수 있으며, 검색에 실패할 경우 None을, 성공할 경우 Some(x)를 리턴하게 됩니다.

4) sum / sumBy / average / averageBy
Collection의 원소의 합(sum), 평균(average)를 계산합니다.

let sum1 = List.sum [1..10] // 55
let avr1 = List.average [1..3]  // 2.0
Enter fullscreen mode Exit fullscreen mode

sortsortBy의 관계와 같이, 만일 별도의 계산 함수가 필요하다면 sumByaverageBy를 사용하면 됩니다.

Infinite sequence의 경우 에러가 나지는 않지만 의도하지 않은 결과가 나올 확률이 매우 높습니다. 무한으로 존재하는데 그걸 전부 더하라고 하면 무한루프가 일어나겠죠. 따라서 take 등을 사용해 적절히 잘라준 후 사용해야 합니다.

5) zip / zip3 / unzip / unzip3
F#에서 zip이란 2개의 collection을 각 원소가 기존 collection들을 묶은 tuple이 되는 하나의 collection으로 묶는 함수를, unzip은 반대로 각 원소가 tuple인 collection을 tuple의 개수만큼 분리하는 함수를 뜻합니다.

let list1 = [1; 2; 3]
let list2 = [-1; -2; -3]
let zipList = List.zip list1 list2  // [(1, -1); (2, -2); (3, -3)]  
let listTuple = List.unzip zipList  // ([1; 2; 3], [-1; -2; -3])
Enter fullscreen mode Exit fullscreen mode

만일 3개의 collection에 대해 이 작업을 수행하고 싶다면 zip3, unzip3 함수를 사용하면 됩니다.

6) append / concat
append 함수는 list에서 다룬 @과 동일한 역할을 합니다. 즉, 같은 type의 두 collection을 합쳐 새 collection으로 리턴합니다. 만일 세 개 이상의 collection에 대해 같은 일을 하고 싶다면, concat 함수를 사용하면 됩니다.

let list1 = List.append [1; 2; 3] [4; 5; 6] // [1; 2; 3; 4; 5; 6]
let list2 = List.concat [[1; 2] [3; 4] [5; 6]]  // [1; 2; 3; 4; 5; 6]
Enter fullscreen mode Exit fullscreen mode

concat함수는 인자의 개수가 정해지지 않았기 때문에 연결을 원하고자 하는 collection들의 list를 인자로 받습니다.

7) 다른 collection으로의 변환
지금까지 알아본 collection들은 상호 변환이 가능합니다.
List.toSeq는 list를 sequence로 바꾸며, List.ofSeq는 반대로 sequence를 list로 바꿔줍니다.
배열에 대해서도 List.toArray, List.ofArray 함수가 존재하며, 다른 collection들도 이에 대응하는 함수들이 전부 존재합니다.
to*함수와 of*함수는 성능상의 차이점은 전혀 없으니(즉, List.ofSeqSeq.toList는 내부적으로도 완벽하게 동일한 함수입니다.) 편하신 대로 사용하시면 됩니다.


지금까지 기본적인 collection에 대해 간략하게 알아보았습니다. 다음 글에서는 F#을 비롯한 FP 언어의 근본이라고 할 수 있는 함수에 대해 더 자세히 알아보겠습니다.

Top comments (0)