Self 언어의 문법과 메시지 전송 모델, 객체/슬롯/parent 슬롯, 블록과 클로저, 위임, 주석과 애너테이션, 표준 라이브러리의 조건·반복·컬렉션 API, 데이터 구조, 미러를 통한 리플렉션, 그리고 실전 팁을 예제와 함께 소개한다.
@2019/02/07
지난 편 Self의 환경과 프로그래밍 언어 (첫 번째: 환경)에서는 Self라는 프로젝트를 소개하고, 어디서 다운로드하며 morphic 인터페이스 환경에서 어떻게 탐색하는지 살펴봤다. 오늘은 언어 자체와 표준 라이브러리를 알아보자.
Self는 문법적으로 Smalltalk의 영향을 받았다. 실제로 십여 년의 시차는 있지만 둘 다 Xerox PARC에서 태어났다. Self에서도 Smalltalk처럼 모든 것이 메시지 전송을 중심으로 돌아간다. 다만 Smalltalk와 달리 Self는 객체를 위한 문법적 구성요소를 도입한다.
객체는 key: val(키: 값) 저장소로 볼 수 있다. 각 키는 Self 용어로 슬롯(slot)이라 부른다.
객체에 메시지를 보내면, 메모리에서 해당 슬롯의 내용을 찾는다. 슬롯이 데이터 슬롯(그 안의 객체가 코드를 포함하지 않음)이면 해당 값이 그대로 반환된다. 슬롯이 코드가 들어있는 객체라면, 그 객체의 코드를 실행한 뒤 남는 값이 반환된다.
객체에 메시지를 보내려면 객체 오른쪽에 메시지를 적는다.
코드
obj zpráva
는 C 계열 문법의 다음 표현과 비슷하다:
obj.zpráva()
()
는 빈 객체를 만든다. 아무 메서드도 없고 어떤 메시지에도 반응하지 못하는 객체다.
코드 (| a. b. |)
는 a
와 b
라는 두 슬롯을 가진 객체를 만든다. 둘의 값은 모두 nil
로 설정된다.
수직 막대 기호 |
는 슬롯 정의의 시작을 나타낸다. 각 슬롯은 마침표로 구분한다. 위의 객체는 내부에 a
와 b
라는 두 서랍을 가진 "상자"다. 이 서랍에는 데이터나 코드를 담을 수 있다.
Self의 객체는 일종의 해시맵이나 딕셔너리처럼 동작한다. 특정 슬롯에 특정 값을 저장할 수 있다. 이 값은 초기화 시점에 할당할 수 있다:
(| a <- nil. b = nil. |)
예제에서는 두 가지 값 할당 방식이 보인다. 첫 번째는 나중에 덮어쓸 수 있고(<−
), 두 번째(=
)는 덮어쓸 수 없다.
저수준에서 첫 번째 방식은 실제 슬롯과 그 슬롯에 쓰기 위한 메서드, 이렇게 두 슬롯이 만들어진다. 두 번째 방식은 슬롯 자체만 생성되고 여기에 쓸 수 있는 경로가 없으므로 변경할 수 없다. 이는 각종 상수를 위해 유용하다.
객체는 또한 두 번째 수직 막대 |
뒤에 코드를 포함할 수 있다:
(| a = 1 | a printLine)
위 예시는 이름 없는 객체를 정의한다. 하나의 슬롯 a
를 만들고 숫자 1
객체로 설정한다. 이어서 a
슬롯, 즉 그 슬롯의 값에 printLine
메시지를 보낸다.
Self에는 parent 슬롯(부모 슬롯)이라는 특별한 슬롯이 있다. 이 슬롯들은 객체에서 찾을 수 없는 메시지를 이 슬롯이 가리키는 객체로 위임(delegate)하게 만든다.
만약 다음과 같은 객체에
(| p* = traits clonable |)
clone
메시지를 보내면, 그 객체는 자신의 복사본을 반환한다. 비록 그 안에 "clone" 슬롯이 없더라도 말이다. 해당 메서드는 p*
슬롯이 가리키는 객체, 혹은 그 객체에 또 다른 parent 슬롯이 있다면 상위 체인 어딘가에 정의되어 있다.
이 메커니즘으로 사실상 상속이 구현된다. 곰곰이 생각해 보면, 이는 예컨대 파이썬에서 "traits clonable"을 상속한 객체의 상위 메서드를 호출하는 상황과 비슷하다.
|슬롯들|
사이에는 매개변수도 올 수 있다. 매개변수는 이름 시작에 콜론을 붙인다. 예를 들어 다음 객체:
(|
x:Y: = (| :a. :b. | a printLine)
|)
하나의 슬롯 x:Y:
를 포함하며, 이는 매개변수 a
와 b
를 받는 메서드-객체를 가리킨다. 이 메서드는 매개변수 a
에게 printLine
메시지를 보낸다.
메시지에는 다음과 같은 종류가 있다:
first
> a
set: a
또는 set: a And: b
단항 메시지는 매개변수가 없다. 이항 메시지는 정확히 하나의 매개변수를 가지며 연산자에 사용된다. 키워드 메시지는 임의 개수의 매개변수를 가질 수 있다. Smalltalk와 달리, 다중 매개변수 키워드 메시지의 이어지는 단어들은 항상 대문자로 시작한다. 덕분에 메시지의 끝이 명확하다.
메시지 a first
는 객체 a
에서 "first"라는 슬롯(프로퍼티)을 찾는다. 그 안에 데이터가 있으면 반환하고, 코드가 있으면 실행한 뒤 결과(마지막 표현식)를 반환한다.
메시지 a > 1
은 객체 a
에서 ">"
라는 슬롯을 찾고 인자 1
을 전달한다. 이런 종류의 슬롯에는 항상 정확히 하나의 매개변수를 받기 때문에 코드(메서드-객체)만 올 수 있다.
메시지 x set: a And: b
는 객체 x
에서 "set:And:"라는 슬롯을 찾고 인자 a
와 b
를 전달한다.
네 번째 종류의 메시지는 밑줄로 시작하는 프리미티브 메시지다:
_print
_set: s And: b
이 메시지들은 인터프리터의 프리미티브(즉 C++로 구현된 부분)를 호출하는 데에만 사용된다는 점에서 다른 메시지들과 다르다.
Self 전체 프로그래밍 환경은 프리미티브로 정의된 공리 위에 세워진 언어로 이해할 수 있다.
Self라는 이름은, Smalltalk와 달리 자기 자신에게 보내는 모든 메시지 앞에 이 키워드(self)를 매번 적을 필요가 없기 때문에 붙었다.
어떤 객체가 자신의 print
메서드를 호출하고 싶다면, 다른 메서드의 코드에서 다음처럼 쓸 수 있다:
self print
하지만 self
는 생략할 수 있으므로 다음처럼 호출해도 된다:
print
이는 생각해 볼 만한 흥미로운 특성이다. 네임스페이스에서 찾을 수 없는 모든 식별자는 객체 자신과 모든 parent 슬롯으로 위임된다. 객체가 모든 것을 암묵적으로 자기 자신에게 보내는데, 과연 로컬 네임스페이스란 무엇일까?
블록은 객체와 비슷하게 동작하지만 세 가지 차이가 있다. 실행 시점까지 평가되지 않고, 생성된 네임스페이스를 가리키는 parent 슬롯을 자동으로 포함하는 것처럼 행동하며, 자동으로 parent*
가 traits block
에 설정된다.
이런 특성 덕분에 블록은 다른 프로그래밍 언어에서의 클로저처럼 동작한다.
[]
는 빈 블록을 만든다.
객체와 마찬가지로 블록에도 슬롯을 넣을 수 있다: [| a <- 1. |]
는 값이 덮어쓰기 가능한 a
슬롯을 값 1
과 함께 갖는 블록을 만든다.
블록은 매개변수도 받을 수 있다: [| :a | a printLine]
는 호출 시 하나의 매개변수를 기대하고 이를 출력하는 코드-객체를 만든다. 블록을 어떻게 호출할까? 블록에 value
메시지를 보내면 된다. 하나의 매개변수를 받는다면 value:
, 여러 개를 받는다면 value:With: .. With: ..
를 사용한다.
Self에서는 모든 제어 구조가 블록으로 구현된다. if 분기, 반복문 등 모두 그렇다.
예를 들어 if 조건은 bool 객체의 키워드 메시지 ifTrue:
, 또는 ifTrue:False:
일 뿐이며, 인자로 코드를 담은 블록을 전달한다:
(| :a. :b. |
(a > b) ifTrue: [^a] False: [^b].
)
여기서 보이는 것은 매개변수 a
와 b
를 받는 코드-객체로, 두 값을 서로 비교한다(객체 a
에게 이항 메시지 > b
를 보내고, 결과 bool 객체에 키워드 메시지 ifTrue:False:
를 보낸다. 첫 번째 인자는 a
를 반환하는 블록이고 두 번째 인자는 b
를 반환하는 블록이다).
캐럿 기호 ^
는 return을 의미한다. 블록 안에서 사용될 때, 단지 블록 자신에서만 값을 반환하는 것이 아니라 상위 네임스페이스에서도 반환한다. 위의 경우 전체 코드-객체/메서드에서 반환되며, 단순히 블록에서만 반환되는 것이 아니다(블록 내부에서만 반환하려면 return:
메시지를 사용할 수 있다).
일반적으로 반환값은 ^
로 명시적으로 반환하거나, 해당 코드의 마지막 메시지의 값을 반환한다.
(|
parent* = traits boolean.
a = (true ifTrue: [1])
|)
여기서는 traits boolean
을 가리키는 parent 슬롯을 포함한 객체 정의를 볼 수 있다. 이를 통해 그 밖에 true
메시지 등도 사용할 수 있게 된다.
슬롯 a
에 저장된 메서드는 자기 자신에게 true
메시지를 보낸다. 이는 parent 슬롯의 위임을 통해 traits boolean
어딘가에 정의된 true
슬롯을 통해 값이 true
인 객체의 복사본을 반환한다. 그런 다음 이 객체에 블록 인자를 갖는 키워드 메시지를 보낸다. 블록에는 단지 객체 1
만 들어 있다.
블록의 마지막 값이 객체 1
이므로 그 값이 반환된다. ifTrue:
메시지의 결과도 반환되어, 슬롯 a
의 메서드에서 마지막 값이 되고 곧 반환값이 된다.
이미 말했듯이, Self에는 상속처럼 동작하지만 상속은 아닌 무언가가 있다. 바로 객체가 이해하지 못한 메시지를 parent 슬롯에 정의된 객체로 위임하는 것이다.
객체는 여러 개의 parent 슬롯을 가질 수 있으며, 어떤 경우에는 어느 parent 슬롯에서 슬롯을 선택할지 명시해야 한다. 이는 resend 메시지로 가능하다. 문법은 parent 슬롯 이름 뒤에 마침표로 메시지 이름을 붙여 parent.message
처럼 쓴다.
예를 들어 다음과 같은 객체가 있다고 하자:
(|
firstParent* = traits something.
secondParent* = traits different.
|
copy.
)
두 parent 슬롯이 가리키는 객체가 모두 copy
슬롯을 정의하고 있다면, 어느 쪽을 보낼지 점 표기법으로 수동으로 선택해야 한다: secondParent.copy
.
위임은 꽤 흥미로운 개념으로, 단순 상속부터 다중 상속까지 가능하게 해 준다. 뿐만 아니라 실행 중에 parent 슬롯을 바꿔, 찾히지 않는 메시지가 위임될 대상을 사실상 전환하는 등 전통적 언어가 제공하지 않는 것들도 가능하게 한다.
언뜻 보기에는 다소 야생적인 구조 같지만, 예컨대 파서를 작성할 때 컨텍스트를 전환하는 데 유용하다.
주석은 큰따옴표 안에 쓴다:
"toto je komentar"
애너테이션은 객체에 메타데이터를 추가하는 방법이다. 문서에서 놀랍게도 크게 다뤄지지 않지만, 이미지 전반에 걸쳐 사용된다.
문법은 중괄호를 사용한다:
(|
p* = traits clonable.
{'Category: accessing'
slot = nil.
}
|)
이는 예를 들어 GUI에게 해당 슬롯을 accessing 카테고리에 표시하라고 알려준다:
개인적으로 애너테이션 문법은 조금 혼란스럽게 느껴진다. 임의의 라벨로 애너테이션을 만들다 보면 온갖 종류의 에러를 내기도 했다:
그럼에도 애너테이션은 곳곳에서 사용되며, 특히 Transporter가 객체가 어느 모듈에 속하는지, 마지막으로 언제 업데이트되었는지를 표시하는 데 사용한다.
애너테이션 자체는 Outliner 같은 도구로는 객체 안에서 보이지 않는다. 이를 보려면 미러를 사용해야 한다(다음 장 참조).
엽서에 적힐 만큼 문법이 단순한 언어들의 함정은, 복잡성이 stdlib로 넘어간다는 데 있다. Self도 예외가 아니다. 그래서 여기서는 stdlib 전체를 훑지 않고 일부만 보겠다. 궁금한 독자는 Self 안에서 직접 세부 내용을 확인하자.
Smalltalk 계열 언어에서 흔히 그렇듯 if 조건은 bool 타입 객체의 메시지로 구현된다. 제공되는 메시지는 다음과 같다:
ifTrue: []
ifFalse: []
그리고 "else" 분기를 가진 동등한 형태들:
ifTrue: [] False: []
ifFalse: [] True: []
조건과 마찬가지로 반복도 컬렉션이나 블록 객체에 보내는 메시지로 구현된다.
기본 메시지는 loop
다:
[ ... ] loop
이는 블록 본문을 무한히 호출한다.
조건 반복도 있다:
[ 조건 ] whileTrue: [ ... ]
그리고 whileFalse:
, untilTrue:
, untilFalse:
, loopExit
, loopExitValue
의 동등한 형태가 있다.
그 밖의 반복으로 숫자 타입에 대한 do:
메시지 및 to:Do:
, to:By:Do:
가 있으며, 파이썬의 range()
이터레이터처럼 어떤 값에서 어떤 값까지 반복한다.
컬렉션에는 비슷한 방식으로 동작하는 다양한 변환기와 이터레이터도 있다. 예를 들어 배열에 mapBy:
, mapBy:Into:
, gather:
, filterBy:
같은 메시지를 보낼 수 있다.
이와 관련해서는 해당 컬렉션을 직접 보는 것을 권한다. 이런 종류의 메시지가 수십 개 있으며, 필터링부터 검색, 정렬, 매핑, 변환, 출현 횟수 세기까지 모두 지원한다. 파이썬보다 더 많다고 할 수 있다.
흥미로움 차원에서, list
타입 컬렉션이 반응하는 메시지들만 봐도 다음과 같다:
<= x
> x
>= x
areKeysOrdered
copare: x IfLess: lb Equal: eb Greater: gb
copy
KeyedStoreStringIfFail: fb
max: x
min: x
at: k
at: i IfAbsent: b
first
first: v
firstIfAbsent: noneBlk
isEmpty
last
firstLinkFor: elem IfPresent: presentBlock ifAbsent: absentBlock
firstLinkSatisfying: conditionBlock IfPresent: presentBlock ifAbsent: absentBlock
ifNone: noneBlock
ifNone: noneBlock IfOne: oneBlock IfMany: manyBlock
keys
last: v
soleElement
add: elem
add: v WithKey: k
addAll: c
add:allFirst: c
addFirst: elem
addLast: elem
asList
< c
= c
compare: c IfLess: lb Equal: eb Greater: gb
hash
isPrefixOf: c
isSuffixOf: c
equalsCollection: c
, c
copy
copyContaining: c
copyRemoveAll
unsafe_with: c1 Do: b FirstKey: firstK1 FirstValue: firstV1
insert: x AfterElementSatisfying: blk IfAbsent: aBlk
insert: x BeforeElementSatisfying: blk IfAbsent: aBlk
insertAll: x AfterElementSatisfying: blk IfAbsent: aBlk
insertAll: x BeforeElementSatisfying: blk IfAbsent: aBlk
do: b
doFirst: f Middle: m Last: lst IfEmpty: mt
reverseDo:
with: x Do: b
with: x ReverseDo: b
withNonindexable: c Do: b
do: elementBlk SeparatedBy: inBetweenBlk
doFirst: f Middle: m Last: lst
doFirst: f Middle: m Last: lst IfEmpty: e
doFirst: f MiddleLast: ml
doFirst: f MiddleLast: ml IfEmpty: e
doFirstLast: f Middle: ml
doFirstLastt: f Middle: ml IfEmpty: e
doFirstMiddle: fm Last: lst
doFirstMiddle: fm Last: lst IfEmpty: e
collectionName
comment1
printStringSize: smax Depth: dmax
statePrintString
storeStringForUnkeyedCollectorIfFail: fb
storeStringIfFail: fb
storeStringNeeds
unkeyedStoreStringIfFail: fb
buildStringWith: block
continued
defaultPrintSize
leftBracket
minContentsSize
minElSize
printStringKey: k
rightBracket
separator
statePrintStringOfElements
statePrintStringOfSize
countHowMany: testBlock
dotProduct: aCollection
harmonicMean
max
mean
median
min
percentile: nth
product
reduceWith: b
reduceWith: b IfSingleton: sb
reduceWith: b IfSingleton: sb IfEmpty: mt
rootMeanSquare
standardDeviation
sum
remove: x
remove elem IfAbsent: block
removeAll
removeAll: aCollection
removeFirstIfAbsent: ab
removeLast
removeLastIfAbsent: ab
allSatisfy: b
anySatisfy: b
findFirst: eb IfPresent: fb
findFirst: eb IfPresent: fb IfAbsent: fail
includes: v
keyOf: elem
keyOf: elem IfAbsent: ab
noneSatisfy: b
occurrencesOf: v
occurrencesOfEachElement
includesAll: c
intersect: c
difference: c
isEmpty
nonEmpty
size
ascendingOrder
comment2
copySort
copySortBy: cmp
copySortBySelector: sel
isAlreadyKnownToBeSortedBy: cmp
sortedBy: cmp Do: b
sortedDo: b
isOrdered
asByteVector
asDictionary
asList
asOrderedSet
asSequence
asSet
asString
asTreeBag
asTreeSet
asVMByteVector
asVector
copyFilteredBy: eb
copyMappedBy: eb
filterBy: filterBlock
filterBy: eb Into: c
gather: aBlock
gather: aBlock Into: aCollection
mapBy: eb
mapBy: eb Into: c
적지 않죠?
데이터 구조는 트레이트의 계층으로 구성되고, 그 위에 기능이 차곡차곡 얹힌다.
모든 컬렉션은 키-값 쌍에 기반한다. 심지어 리스트조차도 개별 원소가 동시에 키이자 값으로 사용된다.
Self는 다양한 set, 딕셔너리, 트리를 제공한다:
트리는 딕셔너리와 달리 비균형 이진 트리를 사용하며, 이는 변질과 성능 저하로 이어질 수 있다.
또한 리스트, 벡터, 문자열, 큐의 변종도 있다:
사실상 모든 컬렉션이 지원하는 가장 중요한 메시지는 다음과 같다:
메시지 | 설명 |
---|---|
at: | 위치/키의 원소를 가져온다. |
at:Put: | 위치/키에 원소를 넣는다. |
add: | 원소를 추가한다(정렬되어 있다면 끝에). |
addAll: | 모든 원소를 추가한다. |
do: [ .. ] | 각 원소에 대해 실행한다. |
컬렉션을 사용하려면, 셸/코드에서 그 이름을 적고 복제(clone)하면 된다. clone
이나 copy
(둘은 동일)를 사용한다.
컬렉션을 반드시 복제해야 한다는 점이 정말 중요하다. 다른 프로토타입들이 모두 같은 곳에서 그것을 가져가고 있기 때문에, 데이터를 집어넣기 시작하면 다른 코드 조각들도 모두 그 영향을 받기 시작한다!
이제 Get it을 클릭한다.
객체를 바탕화면으로 끌어놓고, 왼쪽 위의 화살표로 "펼친다":
size 0
으로 원소가 0개인 것을 볼 수 있다. 안에서 셸을 열고 뭔가를 추가해 보자:
이제 Do it을 선택한다. 메시지 호출의 값을 아웃라이너로 "손에 쥐고" 싶은 것이 아니라, 그냥 코드를 실행하기만 하면 되기 때문이다.
값이 바뀐 것을 볼 수 있다. 이제 values
메시지가 반환한 객체가 무엇인지 살펴볼 수 있다.
그리고 이것이 아웃라이너 결과다. 바탕화면에 내려놓자..
.. 그리고 안을 보기 위해 펼친다:
벡터의 인덱스 1
에 값 'value'가 있는 것을 볼 수 있다. 내가 의도한 그대로다. 참고로 딕셔너리는 정렬되어 있지 않다.
왼쪽 위 셸의 이상한 모양은 신경 쓰지 말자. 그래픽 드라이버가 조금 깨져 있고, Self는 X에 매우 옛날 바인딩을 사용해서 전체가 느리고 다시 그려지는 모양도 이상하다. 노트북에서는 제대로 동작한다.
다음은 do:
메시지를 사용해서 원소와 키를 콘솔에 출력하는 예다:
do:
는 Self의 모든 이터레이터와 마찬가지로 블록을 기대한다. 블록은 두 개의 선택적 매개변수(값과 키)를 받을 수 있으며, 예시에서는 단순화를 위해 v
와 k
로 이름 붙였다. 약간 이상한 순서에 주목하라. 보통은 반대로 기대하기 쉽다.
_Collector_는 이항 메시지 &
에 반응하는 특수한 데이터 구조다. 본질적으로 Self에는 배열 리터럴이 없기 때문에 존재한다. 배열을 만들고 싶다면 가장 간단한 방법은 _collector_를 사용하는 것이다:
(1 & 2 & 3) asList
_Collector_는 배열도, 딕셔너리도 아니지만, as<N>
형태의 메시지를 보내 모든 종류의 구조로 변환할 수 있다. 예를 들어 asList
.
Self에는 예외가 지원되지 않는다. 오류가 날 수 있는 메시지들은 대개 IfFail:
인자를 가진 키워드 메시지 대안을 제공한다. 예를 들어 운영체제 접근 객체에서 잘 보인다:
프로그래머가 적절한 오류 처리를 해야 한다(오류에 반응하는 블록을 인자로 전달). 그렇게 하지 않으면 디버거가 뜨거나 프로그램이 크래시할 수 있다.
마찬가지로 라이브러리 작성자로서 오류 처리를 허용하려면, IfFail:
인자를 가진 메시지 변형을 추가해야 한다.
개인적으로 완전히 만족스러운 해결책은 아니라고 생각하지만, 방법이 그렇다.
참고: Self의 객체 모델을 주제로 한 흥미로운 토론이 여기에서 벌어졌다: https://news.ycombinator.com/item?id=14409088
앞서 설명했듯 Self는 프로토타입 기반 객체 모델을 사용한다. 새로운 객체는 clone
이나 copy
메시지로 복사하거나, 소스 코드에서 빈 객체를 만들고 어떤 기능을 제공하는 parent*
를 참조하도록 만든다. 이는 상속과 유사한 기능을 제공한다.
객체의 계층은 크게 트레이트(trait)와 믹스인(mixin)으로 나뉜다.
트레이트는 일종의 "조상"으로, 공통 기능을 담지만 종종 자체만으로는 완전하지 않다. 다른 객체들이 parent*
슬롯으로 참조하도록 만들어진 객체들이다.
Self에는 앞선 장에서 본 컬렉션 예시처럼 꽤 풍부한 트레이트 계층이 있다.
믹스인은 보통 parent*
슬롯 없이, 특정 수준에서만 공유되는 작은 기능 묶음이다. 목적은 객체에 기능을 "섞어 넣는" 것이다. 부분 구현이 있는 인터페이스와 유사하다고 볼 수 있다.
미러는 Self의 특수 기능로, 다른 프로그래밍 언어에서는 거의 보지 못했다. 널리 쓰이는 언어들은 보통 다양한 내부 프로퍼티로 리플렉션을 제공한다. 예컨대 파이썬은 .__class__
, .__dict__
, .__name__
같은 것으로 객체 내부 정보를 접근한다.
Self는 미러를 사용한다. 미러는 parent 계층 어딘가에 traits clonable
이 있는 객체에 reflect:
메시지를 보내 만들어진다.
이렇게 하면 인자로 전달한 객체를 미러링(반사)하는 객체를 얻게 된다.
그 안에는 미러링되는 객체를 가리키는 의사 슬롯이 있음을 볼 수 있다.
parent를 펼치면, 어떤 메시지들에 반응하는지 볼 수 있다:
WIN+화살표 단축키로 화면 좌우로 이동하고 있다. 지금은 화면의 오른쪽 절반으로 이동했다.
traits mirrors slots
parent에서는 기능이 많지 않으므로, 그 parent를 보자:
여기에는 미러로 온갖 작업을 할 수 있게 해 주는 풍부한 카테고리 목록이 보인다:
예를 들어 반응하는 메시지 목록을 볼 수 있다:
비어 있는 set인 것을 볼 수 있다:
설명하자면: 우리가 미러를 만든 객체에는 메시지(코드를 실행하는 객체)가 없고, 두 개의 슬롯만 있다. 여기서 메시지라 함은 코드를 실행하는 객체를 의미한다.
슬롯 자체는 slotAccess
카테고리의 메시지들로 볼 수 있다:
예를 들어 firstKey
를 시험해 볼 수 있다
응답은 실제로 슬롯 이름 "a"였다. 키 "a"의 값을 표시하면 기대한 결과를 얻는다:
미러의 아름다움은 비활성화할 수 있다는 데 있다. 예를 들어 어떤 코드에서 reflect:
메시지를 제거하거나, 이를 nil
을 반환하는 객체로 바꾸면 된다. 이렇게 하면 eval 같은 함수로 상대적으로 안전하게 코드를 실행할 수 있다(물론 syscalls와 파일시스템 접근도 함께 제거해야 한다).
시간이 지나며 개인 위키에 Self 프로그래밍을 더 즐겁게 만드는 유용한 메모, 팁, 트릭을 모았다.
객체가 copy
메시지를 지원하려면 기본 기능을 상속해야 한다. 이는 traits clonable
에서 찾을 수 있다.
상속된 슬롯을 보기 위해 Outliner에서 parent*
를 계속 열어야 하는 것은 조금 불편하다.
다행히 Outliner는 간단히 설정을 바꿔 상속된 슬롯까지 표시할 수 있다:
preferences outliner kevooidal: true
예시:
중첩 카테고리가 많을 때 검은 화살표를 일일이 펼치는 것은 성가시다. 화살표를 더블 클릭하면 모든 하위 화살표도 함께 열린다.
환경은 코드에서도 saveThenQuit
또는 quitNoSave
메시지로 종료할 수 있다. 개인적으로는 이 중 하나를 호출하는 버튼을 바탕화면에 꺼내놓고, 눌러서 바로 종료하곤 한다. 시간을 조금 절약해 준다.
Self 배포판에 포함된 이미지를 사용하지 않고, 어떤 이유로든 직접 이미지를 빌드하고 싶다면 프로젝트 소스 저장소의 objects/
디렉터리(주의: 반드시 이 디렉터리로 이동해야 한다)에서 다음 명령을 실행하면 된다:
Self -f worldBuilder.self -o morphic
-o
는 출력 파일 이름을 지정하는 옵션이 아니다(!). 오버클럭(overclock)을 의미하며, morphic
인자는 GUI도 함께 빌드하라는 뜻이다.
스크립트가 끝나면 콘솔에 다음을 입력하자:
desktop open
GUI를 열기 위해서다. 아니면 앞 장의 명령을 사용해 저장할 수도 있다.
어떤 Outliner에서든 가운데 버튼으로 "Find slot" 메뉴를 띄울 수 있다:
이렇게 하면 이름으로 슬롯을 검색할 수 있는 객체가 제공된다:
아래쪽에는 검색의 루트가, 위쪽에는 무엇을 검색할지가 들어간다:
입력 바는 초록 사각형을 클릭하거나 CTRL+Enter로 확정한다. 왼쪽 위 화살표를 클릭하면 검색이 실행된다:
각 슬롯은 옆의 사각형을 클릭해 열 수 있다..
또는 컨텍스트 메뉴에서 다양한 작업을 수행할 수 있다:
스크립트를 실행하거나 저장한 모듈을 불러오려면 두 가지 접근법이 있다:
bootstrap read: 'name' From: 'directory'
끝의 name에는 .self
가 붙지 않는다는 점에 주의. 또는:
'path/to/file.self' runScript
자세한 내용: Reading a module.
앞서 말했듯, radarView는 다음처럼 표시할 수 있다:
desktop worlds first addMorph: radarView
또는
desktop w hands first addMorph: radarView
기본 Self 폰트가 형편없다고 느껴진다면, 실제로 그렇다. 원래는 Verdana를 찾지만, 리눅스에서는 찾지 못해 폴백을 사용한다. 해결책은 여기:
실험적으로 알아낸 바에 따르면 editorRowMorph
가 필요하며, 그 안에 editorMorph
가 들어 있다. contentsString
에 반응한다.
ui2_textField
, ui2_textBuffer
, textViewerMorph
, uglyTextEditorMorph
는 어떻게 쓰는지 알아내지 못했다.
다음 편 Self의 환경과 프로그래밍 언어 (세 번째: 디버거, 트랜스포터, 그리고 문제들)에서는 Self의 역사, 커뮤니티, 그리고 실전 프로그래밍의 몇 가지 측면을 살펴본다.
여기서 보이는 것은 매개변수 a
와 b
를 받는 코드-객체로, 두 값을 서로 비교한다(객체 a
에게 이항 메시지 > b
를 보내고, 결과 bool 객체에 키워드 메시지 ifTrue:False:
를 보낸다. 첫 번째 인자는 a
를 반환하는 블록이고 두 번째 인자는 b
를 반환하는 블록이다).