Programming in Scala 17장
17장부터는 음슴체를 사용하지 않으려 합니다.
17 Collections
17.1 Sequences
시퀀스는 순서대로 나열된 데이터 그룹에 대해 동작한다. 정돈된 요소들이기 때문에 첫 번째 요소, 두 번째 요소, 103번째 요소 등을 요구할 수 있다.
List
리스트는 리스트의 시작 부분에 빠른 삽입과 삭제를 지원한다. 그러나 임의의 인덱스에 대한 빠른 접근은 제공하지 않는데, 그 구현이 리스트를 통해 연속적으로 반복되어야 하기 때문이다. (※. 뭔소리야? )
이런 특징들의 조합이 이상하게 들리겠지만, 이들은 많은 알고리즘에 잘 동작하는 sweet spot을 찾아낸다. 시작 요소의 빠른 삽입과 삭제는 패턴매칭에 잘 동작하고, 리스트의 불변성은 복사본을 만들 필요가 없기 때문에 정확하고 효율적인 알고리즘을 개발하는데 좋다.
다음은 리스트를 어떻게 초기화하고 어떻게 리스트의 머리와 꼬리에 접근하는지 보여주는 예제이다.
1 2 3 |
|
리스트 소개는 3.8장, 리스트의 사용은 16장, 리스트의 구현은 22장을 참조한다.
Arrays
배열은 0부터 시작하는 인덱스로 요소를 획득하고 갱신하는 모든 임의 요소에 대해 접근성이 좋다. 다음은 크기는 알고 있지만, 아직 요소의 값은 모를 때 배열을 생성하는 예제이다.
1
|
|
다음은 요소의 값을 알고 있을 때 배열을 초기화하는 예제이다.
1
|
|
Scala에서 배열은 Java처럼 꺽쇠괄호가 아닌 둥근괄호로 억세스한다. 다음은 배열 요소를 접근하고 갱신하는 예제이다.
1
|
|
Scala 배열은 Java 배열처럼 같은 방식으로 표현된다. Java 매소드를 사용해서 배열을 반환할 수 있다. Scala 배열과 Java 배열의 차이점은 19.3장에서 다룬다.
List Buffers
클래스 List
는 리스트 꼬리가 아닌 머리에 대한 접근이 빠르다. 따라서, 리스트 마지막에서부터 append해서 리스트를 만들어 갈 경우, 리스트 앞에 prepend한 후 reverse
를 호출하는 게 좋다.
reverse
를 회피하는 대안으로는 ListBuffer
를 사용하는 것이다. ListBuffer
는 변경 가능한 객체로, 상수 시간(constant time)이 소요되는 append와 prepend 연산을 제공한다.
append는 +=
연산자를, prepend는 +=:
연산자를 사용한다. 다 만들어지면 toList
를 호출하여 List
를 획득한다.
1 2 3 4 5 6 7 8 |
|
ListBuffer
를 사용하는 다른 이유는 잠재적인 스택 오버플로우(stack overflow)를 방지하기 위함이다. prepend할 때 요구되는 알고리즘이 tail recursion이 아닐 경우에 for
나 while
의 반복문 그리고 ListBuffer
를 사용할 수 있다. (※. 8.9 Tail recursion(p. 159)을 참고. 기타 라 스칼라 코딩단 Tail Recursion 논의)
ArrayBuffer
ArrayBuffer
는 시쿼스의 처음과 끝에서 요소를 삽입/삭제할 수 있다는 것을 제외하면 배열과 비슷하다. 모든 Array
오퍼레이션을 사용할 수 있으나 약간 느리다. 새로운 삽입과 삭제는 평균적으로 상수 시간(constant time)이 소요되나 버퍼의 내용이 담긴 새로운 배열을 할당하기 때문에 가끔 선형적이다.
ArrayBuffer
를 사용하려면, 먼저 가변 콜렉션 패키지에서 이를 임포트한다.
1
|
|
ArrayBuffer
를 생성할 때는 반드시 타입 매개변수를 기술해야 한다. 그러나 길이는 기술하지 않아도 된다. ArrayBuffer
는 필요에 따라 할당된 공간이 자동으로 조정된다.
1
|
|
+=
메소드로 append할 수 있다.
1 2 3 |
|
일반적인 배열 메소드를 모두 사용할 수 있다. 예를 들면, 다음과 같다.
1 2 |
|
String (via StringOps)
많은 시퀀스 메소드가 구현된 StringOps
시퀀스를 주목해 보자. Predef
가 String
에서 StringOps
로 암묵적인 변환을 수행하기 때문에 다음 예제처럼 어떤 문자열도 시퀀스처럼 다룰 수 있다.
1 2 3 4 |
|
hasUpperCase
메소드 바디에 있는 문자열 s
에 호출된 exists
메소드는 문자열 객체에 존재하지 않는다. Scala 컴파일러가 암묵적으로 s
를 이 메소드를 가진 StringOps
로 변환하고, exists
메소드는 문자열을 캐릭터 시퀀스로 다룬다.
17.2 Sets and maps
기본적인 set과 map은 3.10장에서 살펴보았다.
Scala에서는 가변/불변 set과 map을 제공한다. set의 계층 구조는 48쪽 그림 3.2에, map의 계증 구조는 50쪽의 그림 3.3에 나타나 있다. 그림에서 보다시피 Set
과 Map
이라는 이름은 각각 다른 패키지에 존재하며 3개의 trait에 의해 사용된다.
Set
과 Map
이라고 쓰면 기본적으로 불변 객체를 얻게 된다. 가변 객체는 명시적으로 임포트해야 한다. Scala에서는 모든 소스에 암묵적으로 임포트되는 Predef
객체로 다음과 같이 불변 객체에 쉽게 접근할 수 있다.
1 2 3 4 5 6 7 |
|
type
키워드는 긴 FQN(fully qualified name)에 대한 별칭으로 Set
과 Map
을 정의하기 위해 Predef
에서 사용되고 있다. Map
은 Predef.Map
과 같고, Predef.Map
은 collection.immtuable.Map
과 같다. 이는 Map
타입과 객체에 대해 동일하다.
한 소스 파일에서 가변/불변 set 또는 map을 같이 사용하려면 가변형(mutable variant)를 지닌 패키지 이름을 임포트한다.
1
|
|
이제 불변 set은 Set
으로 참조, 가변 set은 mutable.Set
으로 참조된다.
1
|
|
Using sets
set의 키(key)는 ==
에 의해 결정되고 많아봐야 (set 내에서) 하나뿐인 객체임을 보장하는 특성이 있다.
예제로 한 문자열 내에 다른 단어들의 개수를 세기 위해 set을 사용해보자.
단어 구분자로 공백과 구두점을 기술할 경우, 문자열에서 split
메소드는 한 문자열을 여러 단어로 분리할 수 있다. 구분자 정규표현식은 [!,.]+
로 충분하다.
1 2 |
|
고유한 단어의 개수를 세기 위해, 이들을 같은 케이스로 변환한 다음, 이들을 set에 추가한다. set은 중복된 값들을 배제하기 때문에 각각 구분된 단어는 set에서 정확하게 한번 나타난다.
먼저, empty
메소드를 사용해서 빈 set을 생성해보자.
1
|
|
이후 for
문으로 단어에 대해 반복해가며 각 단어를 소문자로 변환하고 이를 +=
연산자로 가변 set을 추가한다.
1 2 3 |
|
따라서, 이 텍스트는 정확하게 3개의 구분된 단어 (spot, run, see)를 포함한다는 것을 알 수 있다.
가변/불변 set에서 가장 일반적으로 사용되는 메소드가 표 17.1에 제시되어 있다. (p. 346)
Using maps
map은 콜렉션의 각 요소와 값을 연결 짓는다. map은 0부터 시작하는 정수가 아닌 어떤 종류의 key를 쓸 수 있는 것을 제외하면 배열을 사용하는 것과 비슷하다.
예제로 scala.collection.mutable
패키지를 임포트해서, 빈 가변 map을 생성해보자.
1
|
|
map을 생성할 때는 두 개의 타입을 기술해야 한다. 첫째 타입은 map의 key에 대해 두 번째 타입은 value에 대한 것이다.
map의 엔트리를 세팅하는 것은 배열의 엔트리를 세팅하는 것과 비슷하다.
1 2 3 |
|
map을 읽는 것도 배열을 읽는 것과 비슷하다.
1
|
|
모두 조립하면 문자열에서 각 단어가 나타나는 회수를 세는 메소드는 다음과 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Defaults sets and maps
대개의 경우는 `Set(), scala.collection.mutable.Map()‘ 등의 팩토리 메소드에 의해 제공되는 가변/불변 set과 map의 구현체들로 충분하다. 이런 팩토리 메소드에 의해 제공된 구현체들은 해쉬 테이블을 포함, 빠른 룩업 알고리즘을 사용하여 콜렉션에 객체가 있는지 없는지 재빨리 결정한다.
예를 들어, scala.collection.mutable.Set()
팩토리 메소드는 해쉬테이블을 내부적으로 사용하는 scala.collection.mutable.HashSet
를 반환하고, 비슷하게 scala.collection.mutable.Map()
팩토리 메소드는 scala.collection.mutable.HashMap
을 반환한다.
불변 set과 map은 좀 더 많은 팩토리 메소드가 있다. 예를 들어, scala.collection.immutable.Set()
팩토리 메소드에 의해 반환되는 클래스는 표 17.3 (p. 350) 처럼 얼마나 많은 요소를 전달하는지에 따라 달라진다.
5개의 요소보다 작은 set의 경우, 성능을 최대화하기 위한 특별한 클래스가 각 특정 크기의 set에 대해 전적으로 할당된다. 5개나 혹은 그 이상의 요소를 지닌 set을 요청할 경우, 팩토리 메소드는 해싱을 사용하는 구현체(HashSet)를 반환한다.
비슷하게 scala.collection.immutable.Map()
팩토리 메소드는 표 17.4 (p. 350)처럼 전달하는 key-value 쌍이 얼마나 많은 지에 따라 다른 클래스를 반환한다. map도 성능을 최대화하기 위해 set과 마찬가지로 5개의 요소보다 작은 map의 경우 각 특정 크기의 map이 전적으로 할당된다. 5개 이상의 요소를 지난 map을 요청할 경우, 팩토리 메소드는 해싱을 사용하는 구현체(HashMap)를 반환한다.
EmtpySet
에 요소를 하나 추가하면, Set1
를 반환한다. Set1
에 요소를 추가하면 Set2
를 반환한다. Set2
에서 요소를 하나 삭제하면, 다른 Set1
을 얻게 된다.
Sorted sets and maps
가끔 set과 map의 반복자가 특정한 순서대로 요소를 반환하는 필요가 있다. 이를 위해 Scala 콜렉션 라이브러리는 SortedSet
과 SortedMap
을 제공한다. 이런 trait
는 TreeSet
과 TreeMap
클래스에 의해 구현된다. 이들은 순서대로 요소나 키를 유지하는 레드-블랙(red-black) 트리를 사용한다.
순서는 Ordered
트레이트에 의해 결정되는데, 이들은 set의 요소 타입 혹은 map의 키 타입이며 반드시 섞여지거나 암묵적으로 변경 가능하는 것들이어야 한다. 이런 클래스는 가변형만 제공하고 있다.
TreeSet 예제
1 2 3 |
|
TreeMap 예제
1 2 3 4 |
|
17.3 Selecting mutable versus immutable collections
불변 콜렉션을 가변 콜렉션으로 또는 엮으로 스위칭하는 쉬운 방법은 Scala의 문법적 도구(syntactic sugar)를 이용하는 것이다. 가변 set과 map이 += 메소드를 제공하지 않지만, Scala는 +=에 대해 선택적인 해석을 한다. a += b
를 쓰면, a
는 +=
라는 이름의 메소드를 제공하지 않으므로 Scala는 a = a + b
처럼 해석하려고 한다.
예를 들면, 불변 set은 +=
연산자를 제공하지 않는다.
1 2 |
|
그러나 val
대신에 var
로 선언하면, 불변 콜렉션이라고 해도 +=
연산자에 의해 갱신될 수 있다. 새 콜렉션이 생성되면, people
은 새로운 콜렉션을 참조하기 위해 재할당된다.
1 2 3 |
|
이후 people
변수는 추가된 문자열 “Bob”을 포함하는 새로운 불변 set을 참조한다.
동일한 방법을 +=
메소드가 아닌 =
로 끝나는 모든 메소드에 적용할 수 있다. 다음은 set의 요소를 삭제하는 -=
연산자와 set에 콜렉션을 추가하는 ++=
연산자를 사용한 같은 문법이다.
1 2 3 |
|
얼마나 유용한 지 보기 위해, 1.1장의 Map 예제를 보자.
1 2 3 |
|
이 코드는 불변 콜렉션을 사용한다. 가변 콜렉션을 사용하고 싶다면, Map
의 가변 버전을 임포트하면 되는데, 이는 불변 Map
의 기본 임포트를 오버라이딩한다.
1 2 3 4 |
|
그런데 이러한 문법적인 조치는 콜렉션이 아닌 모든 종류의 값에 동작한다. 예를 들면, 다음은 부동소수점 숫자에 사용하였다.
1 2 3 4 |
|
이 확장의 효과는 Java의 할당 연산자인 +=
, -=
, *=
와 비슷하나 좀 더 제너럴한데, 왜냐하면 =로 끝나는 모든 연산자들이 변경될 수 있기 때문이다.
17.4 Initializing collections
컴패니언 객체(Companion Object) 이름 뒤의 중괄호안에 요소를 적으면, Scala 컴파일러는 이를 그 동료 객체에 apply
메소드를 호출하도록 변환한다. (※. 참고 Companion Object)
1 2 3 4 5 6 7 |
|
대부분 Scala 컴파일러가 자신의 팩토리 메소드로 전달되는 요소로 콜렉션의 요소 타입을 추론하나, 콜렉션을 만든 후, 컴파일러가 선택하는 다른 타입을 기술하고 싶을 때도 있다.
이는 가변 객체에 대해선 다음과 같은 이슈를 제기한다.
1 2 3 |
|
여기서 문제는 stuff
에 Int
타입이 주어진 것이다. Any
타입을 가지고 싶으면, 아래처럼 꺽쇠 괄호에 요소의 타입을 넣어줌으로써 명시적으로 말할 필요가 있다.
1
|
|
다른 특별한 경우는, 다른 콜렉션으로 콜력션을 초기화하고 싶을 경우이다. 예를 들면, 리스트가 있고 이 리스트의 요소를 지니는 TreeSet
을 원할 경우이다.
1
|
|
TreeSet
에 팩토리 메소드로 colors
리스트를 전달할 수 없다.
1 2 |
|
대신에, 빈 TreeSet[String]
을 만들고 여기에 TreeSet
의 ++
연산자로 리스트 요소를 추가할 수 있다.
1
|
|
Converting to array or list
콜렉션으로 리스트나 배열을 초기화하는 것은 간단하다. 콜렉션의 toList
나 toArray
를 호출하면 된다.
1 2 3 |
|
원래 colors
리스트는 정렬되지 않았지만, TreeSet
의 toList
를 호출하여 생성된 리스트의 요소들은 알파벳 순서대로 정렬된다.
콜렉션에 toList
나 toArray
를 호출하면 결과 리스트나 배열 요소의 순서는 콜렉션의 elements
의 호출하여 얻어지는 반복자에 의해 생성된 요소의 순서와 동일하다. 왜냐하면 TreeSet[String]
의 반복자가 알파벳 순서로 문자열들을 생성하기 때문이다.
리스트나 배열로의 변환은 콜렉션의 모든 요소를 복사해야 하므로 큰 콜렉션에 대해서 느릴 수 있다는 것을 기억하자.
Converting between mutable and immutable sets and maps
가변 set과 map을 불변의 것으로 또는 그 엮으로 변경할 필요가 있다. 이를 위해 리스트의 요소를 TreeSet
을 초기화하기 위해 이전의 테크닉을 사용할 수 있다.
empty
메소드를 사용해서 새로운 타입의 콜렉션을 생성하고, ++
혹은 ++=
를 사용해서 타겟 콜렉션 타입에 대해 적절한 새로운 요소를 추가한다.
1 2 3 4 |
|
가변과 불변 map에도 같은 변경 테크닉을 사용할 수 있다.
1 2 |
|
17.5 Tuples
3.9장에 설명된 것처럼, 튜플은 고정된 개수의 아이템을 조합하여 전체를 싸잡아 전달할 수 있다. 다음은 정수, 문자열, 그리고 console을 가지는 튜플이다.
1
|
|
단순하기 그지없는 육중한 데이터(data-heavy) 클래스를 정의하는 지루한 작업은 튜플로 피할 수 있다. 튜플은 클래스 이름을 고른다든지, 클래스가 정의될 범위를 고른다든지, 클래스 멤버의 이름을 고르는 노력을 덜어준다.
다른 타입의 객체를 조합할 수 있기 때문에, 튜플은 Traversable
을 상속하지 않는다. 하나의 정수와 정확히 하나의 문자열의 그룹을 만들 때는 List
나 Array
가 아닌 튜플이면 된다.
튜플의 일반적인 응용은 메소드가 여러 개의 값들을 반환할 때이다. 예를 들면, 다음은 콜렉션에서 가장 긴 단어를 찾고 그 인덱스를 반환하는 메소드이다.
1 2 3 4 5 6 7 8 9 10 |
|
이 메소드는 다음과 같이 사용한다.
1
|
|
longestWord
함수는 두 개의 아이템을 계산한다. 배열에서 가장 긴 단어인 word
와 그 단어의 인덱스인 idx
가 그것이다.
문제를 단순화하기 위해 함수는 리스트에 적어도 하나의 단어가 있다고 가정하고 리스트에서 빨리 나오는 단어를 선택하는 방법으로 끈(tie)을 끊는다. 함수가 반환할 단어와 인덱스를 선택하면, 튜플 문법 (word, idx)
를 사용해서 이들 모두를 함께 반환한다.
튜플 요소에 억세스하기 위해 첫번째 요소는 _1
메소드로, 두번째 요소는 _2
메소드 등등을 사용한다.
1 2 |
|
또한, 튜풀의 각 요소를 고유의 변수에 할당할 수 있다.
1 2 |
|
그러나 괄호를 없애면 다른 결과를 얻게 된다.
1
|
|
이 문법은 같은 표현을 중복해서 정의한다. 각 변수는 오른쪽 표현식을 각각 평가하여 초기화된다. 두 변수 모두 완전한 형태로써 튜플에 대해 초기화된다.
튜플은 “A와 B” 그 이상의 의미가 없는 데이터를 조합하는데 좋다. 그러나 조합이 어떤 의미를 가지게 되어 메소드가 추가될 때는 좀 더 나아가 클래스를 생성하는 것이 좋다. 예를 들면 년, 월, 일의 조합하는데 3개짜리 튜플을 사용하지 말고 사람과 컴파일러 모두 가독성과 실수를 잡기에 좋도록 좀 더 명확한 의도를 지닌 Date
클래스를 만들자.
17.6 Conclusion
Scala 콜렉션에 대해 좀 더 자세한 정보는 24장과 25장을 살펴보길 바란다. 이제부터 다음장에서는 Scala 라이브러리에서 돌아와 언어 (자체)에 주의를 집중하여 불변 객체에 대한 Scala의 지원을 논의할 것이다.