트랜스포터와 디버거를 사용자 관점에서 살펴보고, Self 언어와 환경, 그리고 프로토타입 기반 프로그래밍의 단점들을 정리한다.
@2019/02/15
오늘은 중간 편으로, 다른 곳에 넣기 어려웠던 것들을 모아 담았다. 트랜스포터와 디버거를 좀 더 자세히 보고, Self라는 언어와 환경, 그리고 일반적으로 프로토타입 기반 프로그래밍을 사용하는 원리의 단점도 함께 살펴본다.
트랜스포터가 정확히 어떻게 동작하는지는 다루지 않는다. 그런 내용은 예를 들어 논문 Sifting Out the Gold: Delivering Compact Applications From an Exploratory Object-Oriented Environment에서 볼 수 있다. 여기서는 https://bluishcoder.co.nz/2015/11/19/exporting-self-objects-to-source-files-via-transporter.html 페이지의 영상처럼 사용자 측면에 집중한다.
그림이 많은 탓에 이 소단락이 길어 보이지만, 실제로 복잡한 일은 없다. 익명 객체를 하나 만든다. 그 객체를 트랜스포터에서 새 모듈에 추가한다. 그다음 전역 계층에 슬롯을 만들고, 그 슬롯에 이 객체를 저장하며, 이미 만들어 둔 모듈에도 추가한다. 그리고 트랜스포터의 애노테이션으로 슬롯을 기본값으로 설정하지 말고, 슬롯에 저장된 객체를 "따라가도록(Follow)" 지정한다. 마지막으로 트랜스포터 대화상자를 통해 모든 것을 디스크에 저장한다.
가장 먼저, 값이 "1"인 슬롯 "a" 하나만 가진 아주 간단한 객체를 만든다.
그다음 아웃라이너(outliner)를 가운데 버튼(휠)으로 클릭하고 메뉴에서 "Set module..."을 고른다:
이제 다양한 정보를 묻는 대화상자가 연달아 뜬다. 첫 번째는 슬롯을 묻는다:
다음으로, 객체를 저장할 모듈 이름을 묻는다.
"other"를 고르면 이름 입력 대화상자가 뜬다. 모듈 이름은 "test"로 하겠다:
이제 모듈을 생성할지, 방금 것이 실수인지 묻는다. 실제로 생성하겠다:
다른 모듈의 서브모듈로 만들고 싶지 않으므로 다음 입력란은 비워 둔다:
파일을 저장할 폴더를 고른다. 기본값인 "applications"를 그대로 둔다:
이제 내 객체는 공식적으로 모듈의 일부다. 다만 지금은 허공에 둥둥 떠 있는 상태라, 로드 후에는 참조하기가 좋지 않다. 나는 이 객체를
globals test
경로에 저장하고 싶다. 그러니 가운데 버튼으로 바탕 화면을 클릭해 globals
용 아웃라이너를 연다:
그리고 "applications" 하위 섹션을 펼친다:
이미 네 개의 애플리케이션이 있음을 볼 수 있다. "applications"라는 글자를 가운데 버튼으로 클릭하면 컨텍스트 메뉴가 뜨고, 여기서 "Add Slot"을 고른다:
녹색 직사각형을 클릭하거나 CTRL+Enter를 눌러 저장한다.
이제
nil
대신 바탕 화면에 있는 내 익명 객체에 대한 참조를 그래픽으로 끌어다 놓는다. 물론 내 익명 객체로 가서 셸을 열고 globals test: self
같은 걸 입력해도 된다.
슬롯
globals test
는 이제 내 객체를 가리킨다. 즉, 내 객체가 이 경로에 존재한다.
이제 이 슬롯의 모듈을 설정해야 한다. 가운데 버튼으로 클릭하고 "Set module"을 고른다:
훨씬 큰 메뉴가 뜨는데, 맨 아래까지 스크롤해서 other를 고른다:
이제 다시 위로 미친 듯이 스크롤하지 않기 위해, 빈 공간을 두 번 클릭해 "home"으로 가겠다고 고른다:
그러면 원래 보던 곳으로 돌아온다. 이제 슬롯
globals test
의 애노테이션을 수정할 필요가 있다. 다시 가운데 버튼으로 클릭하고 "Show Annotation"을 고른다:
모듈 설정에서 슬롯을
nil
로 설정하지 말고 "Follow"를 선택한다.
이제 바탕 화면의 빈 곳을 가운데 버튼으로 클릭해 메뉴에서 "Changed Modules"를 고른다:
여기서 버튼을 클릭해 기록할 수 있다. 옆의 "W"라고 적힌 버튼을 클릭한다:
자, 이게 전부다. 단계가 많아 보이지만 실제로는 꽤 논리적이다. 이제 한 단계 위의 폴더(왜 그런지는 묻지 말자) "objects/applications/" 안에 "test.self" 파일이 생겼음을 확인할 수 있다:
''
'
Copyright 1992-2016 AUTHORS.
See the legal/LICENSE file for license information and legal/AUTHORS for authors.
'
[
"prefileIn" self] value
'-- Module body'
bootstrap addSlotsTo: bootstrap stub -> 'globals' -> 'modules' -> () From: ( | {
'ModuleInfo: Module: test InitialContents: FollowSlot'
test = bootstrap define: bootstrap stub -> 'globals' -> 'modules' -> 'test' -> () ToBe: bootstrap addSlotsTo: (
bootstrap remove: 'directory' From:
bootstrap remove: 'fileInTimeString' From:
bootstrap remove: 'myComment' From:
bootstrap remove: 'postFileIn' From:
bootstrap remove: 'revision' From:
bootstrap remove: 'subpartNames' From:
globals modules init copy ) From: bootstrap setObjectAnnotationOf: bootstrap stub -> 'globals' -> 'modules' -> 'test' -> () From: ( |
{} = 'ModuleInfo: Creator: globals modules test.
CopyDowns:
globals modules init. copy
SlotsToOmit: directory fileInTimeString myComment postFileIn revision subpartNames.
\x7fIsComplete: '.
| ) .
} | )
bootstrap addSlotsTo: bootstrap stub -> 'globals' -> 'modules' -> 'test' -> () From: ( | {
'ModuleInfo: Module: test InitialContents: FollowSlot\x7fVisibility: public'
directory <- 'applications'.
} | )
bootstrap addSlotsTo: bootstrap stub -> 'globals' -> 'modules' -> 'test' -> () From: ( | {
'ModuleInfo: Module: test InitialContents: InitializeToExpression: (_CurrentTimeString)\x7fVisibility: public'
fileInTimeString <- _CurrentTimeString.
} | )
bootstrap addSlotsTo: bootstrap stub -> 'globals' -> 'modules' -> 'test' -> () From: ( | {
'ModuleInfo: Module: test InitialContents: FollowSlot'
myComment <- ''.
} | )
bootstrap addSlotsTo: bootstrap stub -> 'globals' -> 'modules' -> 'test' -> () From: ( | {
'ModuleInfo: Module: test InitialContents: FollowSlot'
postFileIn = ( |
| resend.postFileIn).
} | )
bootstrap addSlotsTo: bootstrap stub -> 'globals' -> 'modules' -> 'test' -> () From: ( | {
'ModuleInfo: Module: test InitialContents: InitializeToExpression: (\'30.21.0\')\x7fVisibility: public'
revision <- '30.21.0'.
} | )
bootstrap addSlotsTo: bootstrap stub -> 'globals' -> 'modules' -> 'test' -> () From: ( | {
'ModuleInfo: Module: test InitialContents: FollowSlot\x7fVisibility: private'
subpartNames <- ''.
} | )
bootstrap addSlotsTo: bootstrap stub -> 'globals' -> () From: ( | {
'Category: applications\x7fModuleInfo: Module: test InitialContents: FollowSlot'
test <- bootstrap setObjectAnnotationOf: bootstrap stub -> 'globals' -> 'test' -> () From: ( |
{} = 'ModuleInfo: Creator: globals test.
'.
| ) .
} | )
bootstrap addSlotsTo: bootstrap stub -> 'globals' -> 'test' -> () From: ( | {
'ModuleInfo: Module: test InitialContents: FollowSlot'
a = 1.
} | )
'-- Side effects'
globals modules test postFileIn
여기에 온갖 메타데이터가 설정되어 있다. 이 파일은 git으로 버전 관리할 수도 있고, 새 이미지에 로드할 수도 있다:
이렇게 하면 모듈에 대한 메타데이터를 손에 넣을 수 있고, 옆의
globals
용 아웃라이너에 새로운 슬롯 "test"가 나타난 것을 볼 수 있다:
Self는 스몰토크 계열의 디버거를 제공하며, 여기서 코드를 인터랙티브하게 수정할 수 있다. 짧은 예시가 아마 천 마디 말보다 더 많을 것을 보여줄 것이다.
셸에서 간단한 객체를 만든다. 이 객체에는 "a"라는 슬롯이 있고, 그 안에는 표준 출력으로 숫자 "1"의 값을 출력해야 하는 코드가 들어 있다. 다만 의도적으로 오타를 내서, "printLine
" 메시지를 보내는 대신 "printLin
"이라는 메시지(끝의 "e"가 없음)만 보낸다:
바탕 화면에 내려놓고 슬롯을 펼쳐 보면 정말 그 객체인지 확인할 수 있다:
객체에서 오른쪽 위의 E를 클릭해 하위 셸(podshell)을 열고, 그 객체에 "a" 메시지를 보낸다:
이제 "Get it"을 클릭하면 코드가 실행되어(즉, 콘솔에 1을 출력) 동시에 커서에 호출 결과(마찬가지로 1)를 놓아야 한다. 하지만 코드에 오류가 있으므로, 대신 디버거가 손에 들어온다:
디버거는 아웃라이너와 약간 비슷하게 생겼다. 클릭해 펼칠 수 있는 상자다. 아래는 전부 펼친 모습이다:
전체 스택 트레이스를 볼 수 있을 뿐 아니라, 그와 멋지게 상호작용할 수 있다. 예를 들어 왼쪽 위에 보이는 1은 메시지(셀렉터)의 리시버다. 그 1을 클릭하면 바로 그 객체를 얻는다:
숫자 1의 경우에는 크게 의미가 없지만, 다른 객체라면 이를 실시간으로 탐색해 보는 것이 아주 유용할 수 있다.
에디터에서 곧바로 오류를 고칠 수도 있다:
초록색 사각형을 클릭하면 코드가 바로 수정된다. 옆의 아웃라이너를 보라:
이제 "continue"를 클릭하면, 아무 오류가 없었던 것처럼 인터프리터가 정상적으로 진행한다. 즉, 콘솔에 1이 출력되고, 그 1이 손에도 쥐어진다:
디버거는 이제 자신이 죽었다고 표시하고 최소화된다. 다시 "Get it" 버튼을 누르면, 두 번째부터는 모든 것이 올바르게 진행된다;
실용적인 관점에서 Self는 꽤나 슬픈 꾸러미다. 먼저 말해 두자면, 상당 부분 쓸 만하지만, 초보 사용자가 접근할 정도는 전혀 아니다. 무언가 문제를 겪는다면, 구글링해서 답을 찾는 건 잊는 편이 좋다. 높은 확률로 당신이 그 문제의 첫 번째 당사자일 것이다. 설령 아니라 해도, 그 문제가 해결되지 않았을 가능성이 아주 높다.
Self는 32비트 프로그램이다. 64비트로의 포팅도 가능하긴 하나, 사소한 일이 아니고 누구도 적극적으로 나서지 않는다. 아마 앞으로 10년 동안도 바뀌지 않을 것 같다.
한편으로 이는 꽤 불쾌하다. 최대 4GB 이미지에 묶이기 때문이다. 하지만 이 문제는 예컨대 여러 인스턴스를 사용해 우회할 수 있고, 무엇보다 그 전에 다른 온갖 문제들에 먼저 부딪히게 된다. 예를 들어 이미지 저장 자체가 수십 MB 정도만 되어도 꽤 느리고, GB 단위가 되면 아마 몇 분은 족히 걸릴 것이다.
프로토타입은 이해하기 쉽고, 위임(delegation)과 함께라면(보고 있다, 자바스크립트야!) 코드 조직을 위한 강력하고 효율적인 메커니즘을 제공한다.
하지만 단점이 하나 있다. 한 번만 clone(클론)을 빼먹어도 시스템의 절반을 갈아엎을 수 있다는 점이다.
예컨대 프로토타입 방식의 dictionary
를 사용할 때는 보통 그것을 복제(clone)한 다음 값을 써 넣는다. 그런데 복제를 잊고 곧장 dictionary에 써 버리면, 이후 복사로 만들어질 모든 새 dictionary에도 동시에 써 넣는 셈이 된다. 대개 이는 매우 빠른 시스템 붕괴로 이어진다.
불행히도 Self는 이를 방지할 방법을 제공하지 않는다. 개인적으로라면, 시스템 객체를 잠가(lock) 써 넣기 전에 반드시 클론하도록 강제하고 싶다. 아니면 어떤 자동 클론 메커니즘도 가능할지 모르겠다.
Self의 리터럴(객체 정의를 위한 축약 네이티브 표기)은, 오직 람다 함수만 지원하는 언어들과 비슷한 면이 있다. 익명 객체는 멋지지만, 그래도 사물에는 이름을 붙일 수 있을 때 사람이 더 쉽게 길을 잃지 않는다.
Self에서는 애노테이션으로 이를 처리한다. 객체 이름을 설정할 수 있고, 그러면 아웃라이너에 그 이름이 표시된다. 혹은 객체를 적절한 계층에 배치하면 된다. 예를 들어 객체를 globals dictionary
경로에 두면, 그게 사전(dictionary)이라는 사실이 자연스레 드러난다.
이 점을, 반대로 정확히 상반된 문제를 겪는 "클래스 기반 언어"와 비교해 볼 만하다. 이런 언어들은 거의 모든 것에 이름을 붙이려 든다. 클래스는 분명한 이름이 있고, 인스턴스는 그것이 존재하는 변수 이름을 가진다.
무엇이 사람에게 더 낫고 더 자연스러울까? 객체가 고정된 이름을 갖는 것이 좋을까, 아니면 객체의 이름이 그것이 저장된 변수(혹은 경로)에 의해 주어지는 편이 좋을까?
에디트 모르프(edit morphs)는 되돌리기(undo)를 지원하지 않는다. 유감. 그리고 다른 모든 키보드 단축키들은 이맥스(Emacs)의 일부 서브셋이다.
아쉽지만 동작하지 않는다. 아주 옛날 X 바인딩을 쓰기 때문이다. 해결책은 autocutsel을 설치하고 백그라운드에서 실행하는 것이다.
앞서 링크한 것들을 제외하면 문서가 없다시피 하다. 대부분은 메일링 리스트에서 물어볼 수 있지만, 그리 이상적인 방법은 아니다.
이는 정보를 저장하기 위해 객체 자체를 사용할 때 다소 슬프다. 순서 있는 컬렉션을 쓰도록 강제되기 때문이다.
그게 무엇인지 모르면 아마 신경 쓰이지 않을 것이다. 하지만 스몰토크에서 왔다면 꽤 아플 것이다.
Self는 ISO 8859-1을 인코딩으로 쓰기 때문에, 이 범위를 벗어나는 문자는 받지도(아예 입력할 수 없다) 못하고, 표시할 수도 없다.
이 문제를 다뤄 보고 싶은 분이 있다면 다음을 참고하라:
아웃라이너는 흥미로운 개념이지만, 꽤 빨리 성가셔진다. 왜 그런지는 예를 들어 여기에서 볼 수 있다:
혹은 더 완연한 모습으로 여기에서:
좀 더 복잡한 것을 살피다 보면, 아웃라이너 수십 개로 끝나기 일쑤다.
개인적으로는 여기에 스몰토크 스타일의 모듈 브라우저를 보완하고 싶다:
Self는 일반적인 어떤 포맷의 이미지도 표시하지 못한다. 오직 어떤 Sun 래스터 포맷만 지원한다:
마지막 편인 Self의 환경과 프로그래밍 언어 (4부: 커뮤니티, 역사, 미래, 그리고 형이상학)에서는 Self를 둘러싼 커뮤니티, 역사, 미래를 살펴보고, 약간의 철학적인 이야기까지 해 본다.