SqueakByExample:9.5
컬렉션의 반복자(Collection iterators)
스몰토크에서 반복문이나 조건문은, 컬렉션, 정수, 블록과같은 객체에 대한 단순한 메시지 전송입니다(3 장을 참고해주세요). 스몰토크의 콜렉션계층은, 초기값부터 마지막값까지 변화하는 수치를 인자로 블록을 평가하는 to:do: 같은 저수준 메시지를 포함한 여러가지 고수준의 반복자iterator[1]를 제공합니다. 이런 반복자의 사용은, 프로그램의 코드를 좀 더 튼튼하고 간결하게 만들어 줍니다.
Iterating (do:)
do: 메서드는 기본적인 컬렉션 반복자 메서드입니다. do: 메서드는 그 자체의 인자(단일 인자를 취하는 블록)를 수신자의 각 요소에 순서대로 적용합니다. 다음 예제는 수신자에 들어 있는 모든 문자열을 Transcript 에 print 합니다.
#('bob' 'joe' 'toto') do: [:each | Transcript show: each; cr].
변종(Variants)
do: 메서드에는 많은 변종이 있는데, 예를들면 do:without:, doWithIndex:, reverseDo: 등이 해당되겠죠. indexed 컬렉션(Array, OrderedCollection, SortedCollectino) 을 위한 doWithIndex: 메서드는 현재 index 에 대한 접근등을 제공합니다. 이 doWithIndex: 메서드는 Number 클래스에서 정의된 to:do: 와 관련이 있습니다.
#('bob' 'joe' 'toto') doWithIndex: [:each :i | (each = 'joe') ifTrue: [ ↑ i ] ] ⇒ 2
orderd collection 의 reverseDo:는 컬렉션의 정렬을 역순으로 처리합니다.
아래의 코드에서 do:separatedBy: 라는 재미있는 메서드를 볼 수 있습니다. 이 메시지는 두개의 요소 사이를 대상으로 두번째 인자로 주어진 블록을 실행합니다.
res := ''.
#('bob' 'joe' 'toto') do: [:e | res := res, e ] separatedBy: [res := res, '.'].
res ⇒ 'bob.joe.toto'
위의 방법은 효율적이라고 볼 수는 없으며, 결과를 buffer 에 쓰기 위해서는 stream 을 사용하는게 더 좋은 방법이라는걸 주의해주세요(10 장을 참고).
String streamContents: [:stream | #('bob' 'joe' 'toto') asStringOn: stream delimiter: '.' ] ⇒ 'bob.joe.toto'
Dictionaries.
메시지 do: 가 dictionary 로 전송될때 검사되는 요소는 value 가 되며 association연관은 되지 않습니다.이런 경우 keysDo:, valuesDo: associationsDo: 등을 사용하는것이 더 적당하며, 각 메서드들은 Keys, values 또는 associations 에 대해 반복 실행됩니다.
colors := Dictionary newFrom: { #yellow -> Color yellow. #blue -> Color blue. #red -> Color red }.
colors keysDo: [:key | Transcript show: key; cr]. "displays the keys"
colors valuesDo: [:value | Transcript show: value;cr]. "displays the values"
colors associationsDo: [:value | Transcript show: value;cr]. "displays the associations"
Collecting results (collect:)
콜렉션의 요소를 처리하고, 그 결과를 새로운 컬렉션으로 만들기를 원한다면, do: 를 사용하는것보다 collect: 또는 그 외의 반복자 메서드를 이용하는것이 보다 좋은 방법이 될겁니다. 이런 메서드의 대부분은, Collection 이나 서브클래스에서 열거enumerating 프로토콜쪽에서 찾을 수 있습니다.
예를들어 각 요소의 2 배로 하는 새로운 컬렉션을 만든다고 생각해 보겠습니다. do: 를 사용한다면 다음과 같은 코드가 될겁니다.
double := OrderedCollection new.
#(1 2 3 4 5 6) do: [:e | double add: 2 * e].
double ⇒ an OrderedCollection(2 4 6 8 10 12)
collect: 메서드는, 인자로 건네받은 블록을 각 요소에 대해 실행하고, 결과를 수용하는 새로운 콜렉션을 반환합니다. do: 대신에 collect: 를 사용하면 코드는 매우 간결해지게 됩니다:
#(1 2 3 4 5 6) collect: [:e | 2 * e] ⇒ #(2 4 6 8 10 12)
do: 와 비교되는 collect: 의 장점은 아래의 예제에서 더 극적으로 확인되는데, 예제는 정수의 콜렉션을 준비하고, 그 절대값을 결과로 받는경우가 되겠습니다.
aCol := #( 2 -3 4 -35 4 -11).
result := aCol species new: aCol size.
1 to: aCol size do: [ :each | result at: each put: (aCol at: each) abs].
result ⇒ #(2 3 4 35 4 11)
위의 표현식과 대비되는 아래의 간결한 표현식을 비교해 보시기 바랍니다.
#( 2 -3 4 -35 4 -11) collect: [:each | each abs ] ⇒ #(2 3 4 35 4 11)
두 번째 방법이 더 좋은 이유는, 이런 방법이 Set 이나 Bag 에서도 동일하게 작동할거라는 점입니다.
일반적으로 컬렉션의 각 요소들에 메시지를 발송하는것을 원하지 않는다면, do: 사용을 피하는 것이 바람직합니다.
메시지 collect: 를 송신하면 수신자와 동일한 종류의 컬렉션을 반환한다는 사실에 주의해주세요. 이러한 이유 때문에, 다음 코드는 실패하게 됩니다. (String 은 Integer 값들을 가지고 있지 않습니다.)
'abc' collect: [:ea | ea asciiValue ] "error!"
그대신 String 을 Array 또는 OrderedCollection 으로 변환해서 처리할 수 있습니다.
'abc' asArray collect: [:ea | ea asciiValue ] ⇒ #(97 98 99)
실제로 collect: 는 수신자와 정확히 동일한 클래스에 대한 컬렉션 반환을 보장하지 않으며 오직 동일한 "종(species)" 만을 반환합니다. Interval 의 경우 유사한 종류는 사실 Array 입니다.
(1 to: 5) collect: [ :ea | ea * 2 ] ⇒ #(2 4 6 8 10)
요소들을 선택하고 거부하기
select: 메서드는 특별한 조건을 만족시키는 수신자의 요소들을 반환합니다:
(2 to: 20) select: [:each | each isPrime] ⇒ #(2 3 5 7 11 13 17 19)
reject: 메서드는 select: 와는 반대되는 작업을 수행합니다:
(2 to: 20) reject: [:each | each isPrime] ⇒ #(4 6 8 9 10 12 14 15 16 18 20)
detect: 로 요소를 식별하기
detect: 메서드는 수신자의 요소중에서 주어진 블록 인자를 만족하는 첫번째 요소를 반환합니다.
'through' detect: [:each | each isVowel] ⇒ $o
메서드 detect:ifNone 은 메서드 detect: 의 변종입니다. 이 메서드의 두 번째 인자(블록)은, 첫번째 블록에서 일치되는 요소가 없을 때, 실행됩니다.
Smalltalk allClasses detect: [:each | '*java*' match: each asString] ifNone: [ nil ] ⇒ nil
inject:into:로 결과들을 모으기
함수형 프로그래밍 언어들은, 자주 몇 가지 이항 연산자를 컬렉션의 모든 구성요소에 반복적으로 적용해서 결과를 받기 위해 fold 또는 reduce 라고 지칭되는 좀 더 고수준의 정렬 기능을 제공합니다. 스퀵에서 이런 기능은 Collection>>inject:into: 를 사용하면 됩니다.
이 메서드의 첫 번째 인자는 초기 값이며, 두 번째 인자는 지금까지의 결과와 각각의 구성 요소에 차례대로 적용된 2 개의 인자 블록이 됩니다.
inject:into: 의 간단한 예로서 숫자들의 컬렉션에 대한 총합을 계산해 보겠습니다. 가우스(Gauss) 처럼, 스퀵에서는 1 에서 부터 시작한 100 까지의 정수Integer들의 총합을 구하기 위해 아래와 같은 표현식을 작성하면 됩니다:
(1 to: 100) inject: 0 into: [:sum :each | sum + each ] ⇒ 5050
또다른 예로서, 분수들을 계산할때 1 개의 인자 블록을 쓴다면 다음과 같이 작성할 수 있겠죠:
factorial := [:n | (1 to: n) inject: 1 into: [:product :each | product * each ] ].
factorial value: 10 ⇒ 3628800
그 외의 메시지들
count: 메시지 count: 는 조건식을 만족시키는 요소의 개수를 반환합니다. 조건식은 Boolean 블록으로 작성합니다.
Smalltalk allClasses count: [:each | 'Collection*' match: each asString ] ⇒ 2
includes: 메시지 includes: 는 인자가 컬렉션에 포함되었는지의 여부를 점검합니다.
colors := {Color white . Color yellow. Color red . Color blue . Color orange}.
colors includes: Color blue. ⇒ true
anySatisfy: 메시지 anySatisfy: 는 만약 최소 1 개의 컬렉션 요소가 인자에 의해 제시된 조건식을 만족시킬 경우 true 로 답변합니다.
colors anySatisfy: [:c | c red > 0.5] ⇒ true
Notes
- ↑ Iterator 는 한국어로 대부분 반복자라고 부릅니다만, wikipedia 의 경우에는 객체지향 프로그래밍에서 container 에 제공되는것이라고 언급합니다. 이런 컨테이너는 사실 smalltalk 에서는 컬렉션이라고 할만하죠. 여기서는 일반적으로 한국에서 사용하듯이 반복자 라는 용어를 사용하도록 하겠습니다.