SqueakByExample:10.3
컬렉션(collections) 스트리밍 작업하기
스트림은 구성요소들의 컬렉션들을 다룰 때 매우 유용합니다. 스트림은 컬렉션에서 구성요소들을 읽고 쓰기 위해 사용될 수 있습니다. 우리는 이제 컬렉션들을 위한 스트림의 특징들을 탐구해 볼 것입니다.
컬렉션 읽기
이 섹션은 컬렉션을 읽기 위해 사용된 특징들을 제시할 것입니다. 스트림(stream)을 사용하여 컬렉션을 읽는 작업은, 여러분에게 본질적으로 컬렉션(the collection)으로 들어가는 포인터(pointer)를 제공해 드릴 것입니다. 이 포인터는 읽기를 할 때 앞으로 이동될 것이며, 여러분은 그 포인터를 여러분이 원할 때마다 이동시킬 수 있습니다. 클래스 ReadStream은 컬렉션으로부터 구성요소들을 읽기 위해 사용되어야만 합니다.
메서드 next와 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 := 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'
이 코드는 스트림(the stream)에 있는 숫자의 신호(sign)에 따라 불리언 변수를 음수(negative)로, 그리고 숫자를 절대 값으로 설정합니다. 메서드 upToEnd는 현재 위치로부터 스트림의 끝부분에 모든 것을 리턴하고 스트림을 그것의 끝 부분에 설정합니다. 이 코드는 peekFor:를 사용하여 단순화될 수 있으며, 이것은 만약 다음 구성요소가 파라미터와 동등할 경우 앞으로 이동하며, 동등하지 않을 경우 이동하지 않습니다.
stream := '--143' readStream.
(stream peekFor: $--) ⇒ true
stream upToEnd ⇒ '143'
peekFor: 는 또한 파라미터가 구성요소와 동일할 경우를 가리키는 불리언(Boolean)을 리턴합니다.
여러분은 아마 위의 예시에서 스트림(stream)을 구축하는 새로운 방법을 알아내셨을 것입니다: 유저는 그 특정한 컬렉션에서 읽기 스트림(a reading stream)을 얻기 위해 readStream을 sequenceable 컬렉션에 보낼 수 있습니다.
Positioning(위치선정). 스트림 포인터(the stream pointer)의 위치를 선정하기 위한 메서드들이 있습니다. 만약 여러분이 색인(index)를 갖고 있으시다면, 위치(position)를 사용하여 그 색인(index)에 직접 이동할 수 있습니다. 여러분은 위치(position)을 사용하여 현재 위치(position)를 요청할 수 있습니다. 스트림이 구성요소에 위치해 있지 않고 2 개의 구성요소 사이에 위치한다는 것을 꼭 기억해 주십시오. 스트림의 시작시에 대응하는 색인(index)은 0입니다.
여러분은 다음 코드로 그림 10.4에 그려진 스트림의 상태를 얻을 수 있습니다.
stream := 'abcde' readStream.
stream position: 2.
stream peek ⇒ $c
시작과 끝에 스트림을 배치시키기 위해, 여러분은 reset 또는 seetToEnd를 사용할 수 있습니다. skip과 skipTo는 현재 위치에 비례한 위치로 나아가는 작업에 사용됩니다: skepTo:가 그 자체 파라미터와 동등한 구성요소를 찾을 때까지 스트림에 있는 모든 구성요소를 스킵(skip)하는 반면에, skip:은 인수로서 숫자를 수락하고 구성요소들의 숫자를 스킵 합니다. 매칭된 구성요소 뒤에, 스트림이 위치된다는 것을 염두해 주십시오.
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는 스킵되었습니다.
메서드 컨텐츠는 항상 전체 스트림의 복사본을 리턴합니다.
Testing. 몇 가지 메서드들은 여러분으로 하여금 현재 스트림의 상태 테스트를 할 수 있게 해드립니다: isEmpty는 컬렉션에 구성요소들이 없거나 없는 경우에 한정하여 true를 리턴하는 반면에, atEnd는 구성요소들이 더 이상 읽혀질 수 없거나 읽혀질 수 없는 경우에 한정하여 true를 리턴합니다.
여기에, 파라미터로서 2 개의 분류된 컬렉션들(sorted collections)를 취하거나 컬렉션들을 다른 분류된 컬렉션 속으로 병합시키는 atEnd를 사용하는, 가능한 알고리즘의 실행이 있습니다.
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을 사용하여, 컬렉션의 구성요소에서 반복적용(iterating)을 함으로써 컬렉션을 읽는 방법을 이미 살펴보았습니다. 우리는 이제, WriteStreams를 사용하여 컬렉션(collections)을 읽는 방법을 학습할 것입니다.
WriteStreams은 다양한 위치에서 컬렉션에 많은 데이터를 덧붙이는 작업에 유용합니다. 이것은 종종 이 예제에서 정적 동적 부분(static parts, dynamic parts)들에 기초한 문자열을 구축하는 작업에 사용됩니다.
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: for example의 다른 실행에서 사용됩니다. 만약 여러분이 스트림의 컨텐츠에만 관심이 있으시다면 스트림(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:는 여러분을 위해 컬렉션 위에 컬렉션과 스트림을 만듭니다. 그 다음 여러분이 부여한 파라미터로서 스트림을 패스하는 블록을 실행합니다. 블록이 끝날 때, streamContents:는 컬렉션의 컨텐츠를 리턴합니다.
다음 WriteStream 메서드는 이 컨텍스트(context)에서 특별히 유용합니다:
nextPut: 파라미터를 스트림(stream)에 추가합니다.
nextPutAll: 파라미터로서 패스된 컬렉션의 각 구성요소를 스트림(stream)에 추가합니다.
print: 파라미터의 문자 표현(the textual representation)을 스트림에 추가합니다.
space, tab 그리고 cr(캐리지 리턴/개행문자)과 같이, 다른 종류의 문자들을 인쇄하기 위한 유용한 메서드들이 있습니다. 또 다른 유용한 메서드는, 만약 마지막 문자가 space가 아닐 경우 space를 추가하여, 스트림에서 마지막 문자가 space가 되도록 보장하는 ensureASpace입니다.
연속(Concatenation)에 관하여. WriteStream에서 nextPut: 과 nextPutAll을 사용하는 것은 종종 문자를 연속되게 하는 최고의 방법입니다. 콤마 연속 연산자 (,)[the comma concatenation operator (,)]는 훨씬 덜 효과적입니다:
[| 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)"
스트림을 사용하는 것이 좀더 효과적인 이유는 콤마가 수신자(the receive)와 인수의 연결(the concatenation)을 포함하고 있는 새로운 문자열을 만들어, 그것이 둘 모두를 복사해야 하기 때문입니다. 여러분이 반복적으로 동일한 수신자(receive)에 연결시키면(concatenate), 매번 길이가 더 늘어나게 되므로 문자의 개수들이 틀림없이 기하 급수적으로 복제되게 될 것입니다. 스트림 사용은 또한, 가비지(garbage)를 만들며, 이 가비지는 반드시 모아져야 합니다. 문자열 연결(String concatenation) 대신에 스트림을 사용하는 것은 잘 알려진 최적화 입니다. 실제로, 여러분은 이 작업을 돕기 위해 streamContents (223페이지에서 언급)를 사용하실 수 있습니다:
String streamContents: [ :tempStream |
(1 to: 100000)
do: [:i | tempStream nextPutAll: i asString; space]]
동시에 읽기와 쓰기
동시에 읽기와 쓰기를 위해 컬렉션(collection)에 접근하기 위해 스트림(stream)을 사용하는 것이 가능합니다. 여러분이 웹브라우저(web browser)에서 앞으로 버튼(forward button)과 뒤로(backward) 버튼을 관리하기 위해 History class를 만들기를 원한다고 상상해 보십시오. 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)을 포함하고 있는 새로운 클래스(class)를 정의합니다. 스트림(stream)은 초기화 메서드(the initialize method)를 실행하는 동안에 만들어 집니다.
우리는 앞으로 (forward) 뒤로(backward) 가기 위한 메서드들이 필요합니다:
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.
그럼에도 불구하고 이 버전은 완전하지 않습니다. 그 이유는 유저가 링크를 클릭할 때, 앞으로 나아가야 할 미래의 페이지들이 더 이상 없어야 하기 때문입니다. 예를 들어 앞으로 버튼(forward button)은 반드시 비활성화 되어야 합니다. 이 작업을 수행하기 위한 가장 단순한 솔루션은 history end를 나타낸 바로 후에 nil을 쓰기 하는 것입니다.
History>>goTo: anObject
stream nextPut: anObject.
stream nextPut: nil.
stream back.
이제 오직 canGoBackward 와 canGoForward만 실행되어야만 합니다.
스트림은 항상 2 개의 구성요소 사이에 배치됩니다. 뒤로 가기전에, 현재 위치(position) 전에 반드시 2개의 페이지(page)가 있어야 만 합니다. 한 개의 페이지(page)는 현재 페이지 이고, 다른 페이지는 우리가 가기 원하는 page 입니다.
History>>canGoBackward
↑ stream position > 1
History>>canGoForward
↑ stream atEnd not and: [stream peek notNil]
스트림(the stream)의 컨텐츠에 있는 peek에 메서드를 추가하겠습니다:
History>>contents
↑ stream contents
History는 알려진 대로 동작합니다:
History new
goTo: #page1;
goTo: #page2;
goTo: #page3;
goBackward;
goBackward;
goTo: #page4;
contents ⇒ #(#page1 #page4 nil nil)