SqueakByExample:10.3
컬렉션에 대한 stream 처리
stream 은 컬렉션을 다루는데 매우 유용합니다. stream 은 컬렉션에서 요소들을 읽고 쓰는 작업을 위해 사용할 수 있습니다. 이제부터 컬렉션을 다룰때 필요한 stream 의 특징을 자세히 알아보도록 하겠습니다.
컬렉션 읽기
여기서는 컬렉션에 대한 읽기작업을 할 때 사용되는 기능을 소개하겠습니다. 컬렉션을 읽는 작업에 stream 을 사용한다는것은, 본질적으로는 컬렉션의 포인터(요소에 대한 Pointer)를 얻는다는것을 의미합니다. 이 포인터는 읽기를 하면 다음 방향으로 이동되며, 그 포인터를 사용자가 원할 때 원하는 위치로 이동시킬 수도 있습니다. ReadStream 클래스를 사용해서 컬렉션으로부터 요소들의 읽기작업을 할 수 있습니다.
next 메서드를 이용하면 컬렉션으로 부터 요소를 1 개 읽을 수 있으며, next: 메서드를 이용하면 원하는 개수만큼의 요소를 읽어낼 수 있습니다.
stream := ReadStream on: #(1 (a b c) false).
stream next. ⇒ 1
stream next. ⇒ #(#a #b #c)
stream next. ⇒ false
stream := ReadStream on: 'abcdef'.
stream next: 0. ⇒ ''
stream next: 1. ⇒ 'a'
stream next: 3. ⇒ 'bcd'
stream next: 2. ⇒ 'ef'
peek 메시지는 앞으로 진행하지 않은 채, stream 에서 다음 구성요소가 무엇인지 알고싶을때 사용합니다.
stream := ReadStream on: '-143'.
negative := (stream peek = $-). "look at the first element without reading it"
negative. ⇒ true
negative ifTrue: [stream next].
"ignores the minus character"
number := stream upToEnd.
number. ⇒ '143'
위의 코드는 stream 에 숫자에 따라 얻어진 결과값이 - 라면 boolen 으로 값을 반환하고 결과값을 무조건 양수로 반환합니다. upToEnd 메서드는 현재 위치부터 stream 의 끝까지 있는 모든것을 반환하며, 위치를 stream 의 마지막 지점으로 설정합니다. 위의 코드는 peekFor: 를 사용해서 단순화 할 수 있으며, peakFor: 메서드는 stream 의 다음차례의 요소와 인수가 같은경우 stream 의 위치를 그 다음으로 이동시키며 진행하고, 인수와 다를때는 stream 의 위치를 이동시키지 않습니다.
stream := '-143' readStream.
(stream peekFor: $-) ⇒ true
stream upToEnd ⇒ '143'
peekFor: 는 또한 인수가 요소와 동일한 경우, 그 결과를 Boolean 으로 반환합니다.
위의 예제에서 stream 을 만드는 새로운 방법을 눈치채셨나요: 순차 컬렉션에 대해 단지 readStream 을 보내는것 만으로도, 대상이 되는 컬렉션에 대한 read stream 을 만들 수 있습니다.
Positioning(위치선정).
stream 에 대한 포인터의 위치를 설정하기 위한 메서드가 있습니다. 만약 index 를 알고 있으면 position: 메서드를 사용해서 해당되는 위치로 직접 이동할 수 있죠. position 을 이용하면 현재의 위치를 알 수 있습니다. stream 의 position 은 요소 자체가 아니라 2 개의 요소 사이라는것을 꼭 기억해주시기 바랍니다. stream 의 초기 index 상 위치는 0 입니다.
사용자는 아레의 코드를 이용해서 그림 10.4 에 그려진 stream 의 상태를 얻을 수 있습니다.
stream := 'abcde' readStream.
stream position: 2.
stream peek ⇒ $c
stream 을 처음이나 맨 끝으로 이동시키려면, reset 또는 setToEnd 를 사용하면 됩니다. skip: 과 skipTo: 는 현재의 위치를 기준으로 다음위치로 이동하는 경우 사용합니다. skipTo: 는 건네받은 인수와 같은 요소를 찾을때까지 stream 에 있는 모든 요소를 건너뛰는 반면에, skip: 인 인수로 받은 숫자만큼 요소를 건너뜁니다. 매칭된 요소의 뒤쪽에 stream 의 포인터가 위치한다는걸 잊지 마시기 바랍니다.
stream := 'abcdef' readStream.
stream next. ⇒ $a "stream is now positioned just after the a"
stream skip: 3. "stream is now after the d"
stream position. ⇒ 4
stream skip: --2. "stream is after the b"
stream position. ⇒ 2
stream reset.
stream position. ⇒ 0
stream skipTo: $e. "stream is just after the e now"
stream next. ⇒ $f
stream contents. ⇒ 'abcdef'
보시는 것처럼 글자 e 를 건너뛰었습니다.
contents 메서드는 항상 전체 stream 의 복사본을 반환합니다.
Testing.
몇가지 메서드를 이용하면 현재의 stream 상태를 테스트 할 수 있습니다: isEmpty 는 컬렉션이 가지고있는 요소가 없는경우 true 를 반환하는 반면, atEnd 는 컬렉션의 더이상 읽을 수 있는요소가 없는 경우 true 를 반환합니다.
알고리즘에서 atEnd 를 사용하는 경우를 살펴보도록 하겠습니다. 아래의 코드에서는 2 개의 정렬이 끝난 콜렉션을 인자로 받아, 이것들을 병합해서 정렬이 끝난 새로운 콜렉션으로 만듭니다.
stream1 := #(1 4 9 11 12 13) readStream.
stream2 := #(1 2 3 4 5 10 13 14 15) readStream.
"The variable result will contain the sorted collection."
result := OrderedCollection new.
[stream1 atEnd not & stream2 atEnd not]
whileTrue: [stream1 peek < stream2 peek
"Remove the smallest element from either stream and add it to the result."
ifTrue: [result add: stream1 next]
ifFalse: [result add: stream2 next]].
"One of the two streams might not be at its end. Copy whatever remains."
result
addAll: stream1 upToEnd;
addAll: stream2 upToEnd.
result. ⇒ an OrderedCollection(1 1 2 3 4 4 5 9 10 11 12 13 13 14 15)
컬렉션 쓰기
ReadStream 을 사용해서, 컬렉션의 요소를 반복 진행을 통해 읽는 방법을 위에서 살펴봤습니다. 이제부터는 WriteStreams 를 사용해서 컬렉션을 만드는 방법을 배워보도록 하겠습니다.
WriteStreams 은 컬렉션 안의 다양한 위치에서 많은 데이터를 덧붙이는 작업에 유용합니다. 아래의 예제처럼, 정적이거나 동적인 문자열을 생성하는 경우에 주로 사용됩니다:
stream := String new writeStream.
stream
nextPutAll: 'This Smalltalk image contains: ';
print: Smalltalk allClasses size;
nextPutAll: ' classes.';
cr;
nextPutAll: 'This is really a lot.'.
stream contents. ⇒ 'This Smalltalk image contains: 2322 classes. This is really a lot.'
이런 기법은, 다양한 클래스의 printOn: 메서드의 구현에서 사용되고 있습니다. 단순히 String 을 생성해야하는 경우, stream 을 사용하게되면 조금 더 단순하고 효과적인 방법을 사용할 수 있습니다.
string := String streamContents:
[:stream |
stream
print: #(1 2 3);
space;
nextPutAll: 'size';
space;
nextPut: $=;
space;
print: 3. ].
string. ⇒ '#(1 2 3) size = 3'
메서드 streamContents: 는 컬렉션과 컬렉션을 이용한 stream 을 만듭니다. 이 메서드는 인자로 블록을 받아 실행하며, 인자로 주어질 블록에는 stream 이 들어가야 합니다. 블록이 종료되면, streamContents: 는 컬렉션의 내용을 반환합니다.
다음 WriteStream 메서드는 이런 컨텍스트에서 특별히 유용합니다:
nextPut: 인자로 받은 내용을 stream 에 추가합니다.
nextPutAll: 인자로 받은 컬렉션의 각 구성요소를 stream 에 추가합니다.
print: 인자의의 텍스트표현을 stream 에 추가합니다.
특수문자를 stream 에 표시하는데 편리한 space, tab 그리고 CRcarriage return 등의 메서드도 있습니다. 또 다른 유용한 메서드는, 마지막 문자가 공백이 아닐 경우 공백을 추가해서, stream 에서 마지막 문자가 공백이 되도록 보장하는 ensureASpace 입니다.
문자(열)의 결합에 대하여. WriteStream 에서 문자(열)을 연결하는 작업에 있어 가장 좋은 방법은 nextPut:, nextPutAll: 을 사용하는 것입니다. 콤마연산자(,) 를 이용한 문자(열)의 결합은 메서드를 사용하는것에 비교하면 상당히 비효율적인 방식입니다:
[| temp |
temp := String new.
(1 to: 100000)
do: [:i | temp := temp, i asString, ' ']] timeToRun ⇒ 115176 "(milliseconds)"
[| temp |
temp := WriteStream on: String new.
(1 to: 100000)
do: [:i | temp nextPutAll: i asString; space].
temp contents] timeToRun ⇒ 1262 "(milliseconds)"
stream 을 사용하는 것이 보다 효율적인 이유는, 콤마연산자는 수신자와 인수를 연결하는 새로운 문자열을 만들기때문에, 수신자와 인수의 양쪽 모두를 복사하는 작업을 진행하게 되는 상황은 비효율적이기 때문입니다.
만약 같은 수신자에 대해서 콤마연산자로 연결을 반복하면 연결할때마다 문자열이 길어지기때문에, 복사해야하는 문자의 개수가 기하급수적으로 증가하게 되겠죠.
그리고 이런식으로 콤마연산자를 이용한 문자열의 연결을 반복하게되면 메모리상에 쓰레기를 더 많이 남기게 되며, garbege collection 의 대상이 됩니다. 콤마연산자를 이용한 문자(열)의 연결 대신 stream 을 사용하는 방법은 유명한 최적화 방법입니다. 실제로 이런 작업을 위해서 streamContents[1] 사용하실 수 있습니다:
String streamContents: [ :tempStream |
(1 to: 100000)
do: [:i | tempStream nextPutAll: i asString; space]]
동시에 읽고 쓰기
콜렉션에 접근해서 동시에 읽고 쓰기를 하려는경우 stream 을 사용하면 이런 작업이 가능합니다. web browser 에서 앞 버전과 뒤 버튼을 관리하기 위해 History 클래스를 만든다고 가정해 보겠습니다. History 클래스는 그림 10.5 에서 그림 10.11 까지의 내용과 같은 동작을 하게 될겁니다.
위의 그림같은 동작은 ReadWriteStream 을 사용하여 구현할 수 있습니다.
Object subclass: #History
instanceVariableNames: 'stream'
classVariableNames: ''
poolDictionaries: ''
category: 'SBE--Streams'
History>>initialize
super initialize.
stream := ReadWriteStream on: Array new.
여기에서 딱히 어려운 부분은 없습니다. 이제 stream 을 포함하는 새로운 클래스를 정의하도록 하겠습니다. stream 은 initialize 메서드를 통해 만들어집니다.
그 다음 앞 과 뒤 양쪽으로 이동하기 위한 메서드가 필요하겠죠:
History>>goBackward
self canGoBackward ifFalse: [self error: 'Already on the first element'].
stream skip: --2.
↑ self next.
History>>goForward
self canGoForward ifFalse: [self error: 'Already on the last element'].
↑ stream next
지금까지의 코드는 꽤 간단합니다. 이제 유저가 링크를 클릭했을 때, 동작해야할 goTo: 메서드를 작업하도록 하겠습니다. 아래와 같은 방법도 가능합니다:
History>>goTo: aPage
stream nextPut: aPage.
하지만 지금까지의 작업이 완전한것은 아닙니다. 왜냐하면 유저가 링크를 클릭한 이후, 내부에서 앞 에 해당하는 페이지가 있어서는 안되기 때문입니다. 예를들어 앞 에 해당하는 버튼은 비활성화가 되어야 한다는거죠. 이런 작업에 대한 가장 단순한 해결책은 nil 을 사용해서 history 의 끝을 나타내면 됩니다.
History>>goTo: anObject
stream nextPut: anObject.
stream nextPut: nil.
stream back.
이제 canGoBackward 와 canGoForward 의 구현만 남았군요.
stream 은 항상 2 개의 요소 사이에 위치하게 됩니다. 뒤로 가는 작업을 하기위해서는 현재의 위치 앞쪽으로 2 개의 페이지가 있어야 합니다. 하나는 현재의 페이지고, 다른 하나는 이동을 원하는 페이지가 되겠죠.
History>>canGoBackward
↑ stream position > 1
History>>canGoForward
↑ stream atEnd not and: [stream peek notNil]
stream 의 내용을 확인하는 메서드를 추가해 보도록 하겠습니다:
History>>contents
↑ stream contents
History 는 예상대로 작동합니다:
History new
goTo: #page1;
goTo: #page2;
goTo: #page3;
goBackward;
goBackward;
goTo: #page4;
contents ⇒ #(#page1 #page4 nil nil)
Notes
- ↑ 223페이지에서 확인가능