Self (程式語言)

Self語言是一種基於原型的面向對象的程序設計語言，也是一個集成開發環境和運行環境，由David Ungar和Randy Smith，最初在1986年於施樂帕羅奧多研究中心設計。Self語言在Smalltalk的基礎上，採用「槽」取代了「變量」，從而徹底體現了一切都是對象的風格。在實現Self系統的過程中，設計研究人員發展出了一種動態自適應編譯技術。

## 簡介

Self語言把在概念上精簡Smalltalk作為設計原則。它在把消息作為最基本的操作的同時，取消了類的概念，只有對象的概念。它把對象的特性，理解為獲取或更改特性的這兩種方法，從而把特性的概念簡化為方法，並且通過消息來讀槽和寫槽的方式，取代了變量及其賦值。Self提出了特質的概念，用動態綁定實現了委託。

儘管Self系統一次運行在一個進程中，但實際上可以分成兩個部分：Self虛擬機和Self世界。Self世界是一個Self對象庫，Self對象包括數據對象和方法對象，方法對象的代碼部份，是用一種指令非常簡單的字節碼表示的，字節碼由Self虛擬機來解釋。當Self程序從終端、文件或者圖形使用者界面輸入到系統之中時，Self系統把源程序解析轉化為Self世界裡的對象。

動態自適應編譯技術的採用，提高了Self代碼的執行效率。對經常執行的方法，虛擬機將進一步把字節碼轉化為本機代碼。Self虛擬機還提供了一些可供調用的語言原語，用來實現算術運算、對象複製、輸入輸出等。

Self還擁有一個圖形使用者界面Morphic，Self的編程環境，也是基於Morphic來實現的。Self在精簡語言概念的同時，也把大量的工作轉交給環境來處理，語言中的反射機制也同環境密切相關。

## 歷史

在1986年，David Ungar和Randy Smith在施樂帕羅奧多研究中心，提出了Self語言的最初設計，並在1987年的OOPSLA'87的論文《Self：簡單性的能力》中給出了描述，此文在2006年被評為1986年到1996年間三個最有影響的OOPSLA論文之一。

1987年初，Craig Chambers、Elgin Lee和Martin Rinard，在Smalltalk上給出了Self的第一個實驗性解釋器。1987年夏，Self項目在史丹福大學正式開始，1988年夏給出了第一個有效率的實現，並發布了1.0和1.1兩個版本。1991年初，Self項目移至Sun微系統，並且在1992年發布了2.0版。1993年1月，Self 3.0版發布。

1995年7月，Self 4.0版發布。在這個版本中包括了一個全新的圖形使用者環境Morphic。在2016年發行了4.3版本並可運行在Mac OS X和Solaris上。在2010年發行了版本4.4，由最初團隊的某些人和獨立編程者形成的小組開發，它和所有後續版本可以運行在Mac OS X和Linux上。2014年1月發行了4.5版本。2017年5月發行了版本2017.1。

Self的發展基本已經停滯，但在發展Self過程中探索出的一些技術，在其他的系統中得到了應用。在Self的實現中採用的各種編譯優化技術，直接導致了Java Hotspot虛擬機的產生；在Smalltalk的一個實現Squeak中，採用了Self圖形使用者界面Morphic的設計方案，放棄了Smalltalk-80中採用的MVC的方案。Self是對JavaScript程式語言設計有最主要影響者之一。

## 基於原型編程

傳統的基於類的面向對象語言，基於了根深蒂固的二元性：

- 類，定義對象的基本品質（quality）和行為（Behavior）。
- 對象實例，是類的特定體現（manifestation）。

例如，假設車輛類`Vehicle`的對象有一個「名字」，和進行各種動作的能力，比如「開車上班」和「運送建材」。`Bob's car`是類`Vehicle`的特定對象（實例），它的「名字」是「Bob's car」。在理論上，你可以向`Bob's car`發送消息，告訴它去「運送建材」。

這個例子展示了這種方式的一個問題：Bob的汽車，恰巧是一個跑車，在任何意義上都不能裝載和運送建材，但這是建模`Vehicle`所必須擁有的能力。通過從`Vehicle`建立特殊化的子類，可產生一個更有用的模型；比如建立跑車類`SportsCar`和平板卡車類`FlatbedTruck`。只有`FlatbedTruck`的實例需要提供「運送建材」的機能；`SportsCar`的實例不適合這種工作，它只需要「快速行駛」。但是，這種深入建模在設計期間，需要更多的洞察力，洞察那些可能只在引起了問題時才顯現出的事情。

這個問題是在原型（prototype）這個概念背後的動機因素之一。除非你能必然性的預測出一組對象和類，在遙遠未來時所要有的品質，你不能恰當的設計好一個類的層級。程序最終需要增加行為，實在是太頻繁了，而系統的很多節段將需要重新設計或重新構建，來以不同的方式迸發出對象。早期的面向對象語言如Smalltalk的實驗，顯示出這種問題反反覆覆的出現。系統趨向於增長到一定程度後，就變得非常僵化，因為在編程者的代碼下的深層的基本類，簡直就像是逐漸變成了一個「錯誤」；沒有變更原來的類的容易方式，就會出現嚴重的問題。

動態語言如Smalltalk，允許通過周知的按照類的方法進行這種變更；即通過改變類，基於它的對象就可以改變它們的行為。但是，進行這種變更必須非常小心，因為基於相同類的其他對象，可能把它當作「錯誤行為」：「錯誤」經常是依賴於場景的，這是脆弱基類問題的一種形式。進一步的說，在靜態語言如C++中，這裡的子類可以從超類分別的編譯，對超類的變更實際上可以破壞預編譯的子類方法；這是脆弱基類問題的另一種形式，也是脆弱二進位接口問題的一種形式。

在Self和其他基於原型的程式語言中，消除了在類和對象之間的這種二元性。不再有基於某種「類」的一個對象「實例」，在Self中，你可以複製一個現存的對象，並改變它。故而`Bob's car`可以通過製作現存的`Vehicle`對象的複本來建立，並增加「快速行駛」方法，建模它恰好是一輛保時捷911的事實。

主要用來製作複本的基本對象叫做「原型」。這種技術被稱為是一種非常簡化的機制。如果一個現存的對象或對象的集合，被證明是個不適當的模型，編程者可以簡單的建立有正確行為的一個修改的對象，並轉而使用它。使用現存對象的代碼不會改變。

## 語法和語義

下面簡要描述Self語言的語法和語義。

### 對象

文字 (電腦編程)（literal）包括：數、用`'`包圍起來的字串對象，塊和一般的對象。對象文字用圓括號來界定。在圓括號內，對象描述構成自豎槓`|`界定的一個槽列表，隨後是在這個對象被求值時要執行的代碼。例如：

```smalltalk
 (| 槽1. 槽2 | 一些代码 )
```

槽（slot）是名字-值對，槽包含到其他對象的引用。槽列表由點號分隔的（可以為空的）一序列的槽描述符組成。在槽列表結束處的點號是可選的。槽描述符（descriptor）有兩種：

- `槽 <- 表达式`，指示將指名的數據槽初始化為求值表達式的結果，它有相同名字附加冒號的包含賦值原語的賦值槽，這兩個槽對應於其他語言中的一個讀寫變量。
- `槽 = 表达式`，指示將指名的數據槽初始化為求值表達式的結果，這個槽對應其他語言的一個只讀變量。

在Self中沒有單獨的賦值運算。其他面向對象語言中的訪問子方法對應數據槽，變異子方法對應賦值槽。假如`myPerson`對象中有個叫做`name`的數據槽，則通過myPerson name，可返回在這個槽中的值；如果它有對應的賦值槽`name:`，則通過myPerson name: 'foo'，可設置數據槽`name`的值為`'foo'`。

任何槽都可以通過增加星號後綴，來製成父槽。星號不是槽名字的一部份，在將名字與消息進行匹配時候忽略它。例如一個初始化了的可變的點可以定義為：

```smalltalk
(|  parent* = traits point.
    x <- 3 + 4.
    y <- 5.
|)
```

一個對象的代碼是以點號分隔的一序列的表達式。尾隨的點號是可選的。每個表達式有一系列的消息發送和文字組成。在一個對象的代碼中最後的表達式，可以前導著指示返回的`^`算符。

一個真正的空對象，指示為`(| |)`或簡單的`()`，它根本不接收任何消息。

### 消息

通過消息訪問槽的語法，類似於Smalltalk，有三類消息可以獲得：

一元: `接收者 槽名字`

二元: `接收者 算符 参数`

關鍵字: `接收者 关键字1: 参数1 关键字2: 参数2`

所有消息都返回結果，所以顯式指定的接收者和參數自身可以是其他消息的結果。下面是Self版本的hello world程序例子：

```smalltalk
'Hello, World!' print.
```

組合可以通過使用圓括號來進行強制。在缺乏明確組合的情況下，一元消息具有最高優先級，其次是二元消息，而關鍵字消息最低。一元消息從左至右複合。二元消息對於同一個算符從左至右結合，例如`3 + 4 + 7`被解釋為`(3 + 4) + 7`，而對於不同的算符沒有結合性，例如`3 + 4 * 7`是非法的，而必須顯式的寫為要麼`(3 + 4) * 7`要麼`3 + (4 * 7)`。

關鍵字消息的第一部份必須開始於小寫字母，而後續部份都必須開始於大寫字母。例如表達式：

```smalltalk
5 min: 4 Max: 7
```

是一個單一的消息`min:Max:`，它被發送給`5`並具有參數`4`和`7`，而表達式：

```smalltalk
5 min: 4 max: 7
```

涉及兩個消息：第一個消息`max:`被發送給`4`並接受`7`作為它的參數，而接下來消息`min:`被發送給`5`，並接受`4 max: 7`的結果作為它的參數。關鍵字消息從右至左結合，例如：

```smalltalk
5 min: 6 min: 7 Max: 8 Max: 9 min: 10 Max: 11
```

被解釋為：

```smalltalk
5 min: (6 min: 7 Max: 8 Max: (9 min: 10 Max: 11))
```

由於很多消息被發送給當前消息接收者`self`，故而可以將`self`作為隱含接收者而不需要顯式的寫出。

### 方法

方法（method）是除了參數槽及或局部槽之外，還包含代碼的對象。參數（argument）槽名字開始於一個冒號，它不是槽名字的一部分，在將名字與消息進行匹配時候忽略它。參數槽總是只讀的，並且不能對它們指定初始化者。下面例子是計算平方的方法對象：

```smalltalk
(| :arg | arg * arg )
```

一個普通方法（簡稱方法），是不嵌入到其他代碼之中的方法，它只能存放在只讀槽中。普通方法總是有一個叫做`self`的隱含的父參數槽。Self的普通方法等價於Smalltalk的方法。

如果一個槽包含一個方法，在求值這個槽來響應發來的消息的時候，這個方法對象被對象複製（clone），從而新建它的一個活動（activation）對象，它包含這個方法的參數槽和局部槽；複製體的`self`父槽，初始化為這個消息的接收者；複製體如果有參數槽，將它們初始化為實際參數；在這個新的活動對象的上下文中，執行這個方法的代碼。例如計算點的加法的一個方法：

```smalltalk
(| + arg = 
  ( (clone x: x + arg x) y: y + arg y ) 
|)
```

可以被無歧義的分析，其含義同於：

```smalltalk
(| + = 
  (| :arg | (clone x: ((x + (arg x)))) y: ((y + (arg y))) ). 
|)
```

這裡出現了三個隱含接收者一元消息`clone`、`x`和`y`。

作為語法約定，參數名字可以直接寫在槽名字中對應關鍵字之後，它不再帶有前綴冒號，從而隱含的聲明參數槽。例如下面的方法定義：

```smalltalk
(| ifTrue: False: =
  (| :b1. :b2 | b1 value ).
|) 
```

可以等價的定義為：

```smalltalk
(| ifTrue: b1 False: b2 =
  ( b1 value ).
|)
```

返回算符`^`的出現或缺席，不影響普通方法的行為，因為普通方法總是會返回它最終的表達式的值。

### 塊

塊是Self的閉包，Self就像Smalltalk，使用「塊」用於控制流程和其他職責。塊文字的寫法，除了方括號替代了圓括號之外，類似於其他對象文字。例如嵌入在下列表達式中的塊：

```smalltalk
1 to: 5 * i By: 2 * j Do: [| :k | k print ]
```

一個塊文字定義兩個對象：一個塊數據對象，和它包圍的一個塊方法對象。

- 塊數據對象，它有包含塊方法對象的一個槽，其選擇子也就是槽名字，由參數數目決定，沒有參數是`value`，一個參數是`value:`，兩個參數是`value:With:`，隨著參數增多`With:`個數也隨之遞增。此外，它還有一個叫`parent*`的父槽，指向含有塊對象都共享的行為的那個對象（`traits block`）。
- 塊方法對象，它含有這個塊的代碼。不同於普通方法對象，它不包含`self`槽，轉而有一個匿名父槽，它被初始化指向在詞法上處於外圍的塊或方法的活動對象。匿名的含義，是這個槽的名字在Self層面是不可見的，而不能顯式的訪問。作為結果，在一個塊方法內發送的隱含接收者消息，被限定在這個塊所在表達式的詞法作用域之內，而非這個塊經過可能有的多次轉送，最終向它發送適當的`value`消息變體之時的那個作用域。

相應的，塊求值分為如下兩個階段：

- 在這個塊被求值的時候，即它被用作發送消息的參數，比如例子中`to:By:Do:`消息的參數之時，建立塊對象（即塊數據對象）；這個塊被淺層複製（clone），並將指向這個塊在詞法上外圍的活動記錄，即當前的活動記錄的一個指針交給它匿名保存。
- 在向這個塊發送適當的`value`消息變體的時候，求值這個塊方法；這個塊方法接著被淺層複製，並填充此複製體的方法槽，使用第一階段確定的指針來初始化匿名父槽，最後執行這個塊的代碼。

在塊中，返回算符`^`導致從包含這個塊的普通方法中返回控制權，立即終止這個方法的活動，這個塊的活動，和在其間的所有活動。這種返回叫做「非局部返回」，因為它可以穿越很多活動。普通方法求值的結果，是非局部返回所返回的值。

### 委託

在理論上，所有Self對象都是獨立實體，Self既沒有類也沒有元類。對任何特定對象的變更，都不影響任何其他對象，但是在某些情況下，卻需要它們有關聯。正常的一個對象，只能理解對應於它的局部槽的消息，但擁有一個或更多的指示父（parent）對象的槽，對象可以將任何自身不理解的消息，委託（delegate）給父對象。

Self採用這種方式，處理在基於類的語言中使用繼承來擔負的責任。委託還可以用來實現一些特徵，比如命名空間和詞法作用域。通過下面的例子展示委託與傳統的類的不同之處：

```smalltalk
myObject parent: someOtherObject.
```

這個句子通過改變與叫做`parent`的父槽關聯的值，在運行時間改變`myObject`的「類」。不同於繼承或詞法作用域，委託對象可以在運行時間修改。

### 特質

例如，假定在一個簡單的帳簿應用中，定義了一個對象叫做「銀行帳號」（bank account）。通常建立的這個對象，具有內部的方法，比如說「存款」（deposit）和「取款」（withdraw），和任何它所需要的數據槽，比如說「餘額」（balance）。這只是一個原型，它只在使用方式上特殊，因為它恰好是一個全功能的銀行帳號。

為「Bob的帳戶」製作銀行帳號對象的複製品（clone），將建立一個新對象，它在起初時完全同於原型。在這種情況下，將複製（copy）包括方法和任何數據的槽。但更常用的解決方案，是首先建立叫做它的特質（trait）對象的一個簡單對象，它包含通常與一個類有關的項目。

在這個例子中，「銀行帳戶」將沒有存款和取款方法，而是委託給一個父對象來做這些。採用這種方式，可以製作銀行帳號對象的很多複本，但是我們仍可以通過改變它所委託的特質對象中的槽，來改變它們全體的行為。

### Self世界

當處在於提示符下鍵入表達式的場景時，由叫做「大廳」（lobby）的一個對象，引領使用者進入Self世界。當建立一個新對象的腳本被讀入系統的時候，腳本中的表達式都在大廳的上下文中求值。就是說大廳是這個腳本中所有發送給`self`的消息的接收者。

要引用在腳本中的某個現存的對象，必須通過發送一個消息到大廳才可以訪問到它。大廳的`traits`、`globals`和`mixins`槽，是從大廳可以訪問的對象命名空間的根。大廳的`lobby`槽允許大廳自身通過名字來提及。路徑名字是一個一元選擇子的序列，它描述從大廳到這個對象的路徑。路徑名字也是可以在大廳的上下文中求值的表達式，它產出這個對象。

例如，原型列表的完全路徑名字是`globals list`。因為`globals`是父槽，它可以從路徑名字表達式中省略，生成簡短路徑名字`list`。大廳的`traits`不是父槽，特質對象的名字必須開始於前綴`traits`，因此列表的特質對象必須稱呼為`traits list`。

不是所有對象都有路徑名字，只有那些從大廳可以到達的對象才有，這些對象稱為「周知的」。大廳向使用者提供三類對象：

- 特質（traits）對象，封裝共享行為的對象，典型的每個原型對象，都有一個關聯的同名特質對象，用來描述它的行為的共享部份。任何Self實現都需要為整數、浮點數、字串和塊提供特質對象。Sefl世界中多數具體對象派生自兩種個頂層特質對象：不唯一性對象承襲自`traits clonable`，而唯一性對象承襲自`traits oddball`。唯一性的對象通過返回自身，來響應消息`copy`，並使用同一性來測試相等。
- 全局（globals）對象，即不唯一性的原型對象，和「每種只有一個」的獨特（`oddball`）對象。一些對象比如`true`、`false`和`nil`是唯一性的，在系統中它們只需要有一個。因為一個`oddball`不需要在它的很多實例間共享它的行為，它不需要有分立的特質對象和原型對象。很多`oddball`對象從`traits oddball`繼承`copy`方法，它返回對象自身而非一個新複本。
- 混入（mixins）對象，即小而無父對象的行為束（bundle），是持有共享行為的一種對象，通常用來混入那些有其他父對象的對象之中。混入對象的一個例子是`mixins identity`。兩個對象測試相等，通常基於在一個共同的域（domain）內是否有相同的值。例如，在數的域內`3.0 = 3`，即使它們不是相同的對象甚至不是同種類的對象。但是在一些域中，兩個對象相等若且唯若它們是相同的對象，例如兩個進程即使有相同的狀態也不被當作是相等除非它們是同一個。在這種情況上，使用同一性比較來實現相等測試，並混入`mixins identity`來得到想要的行為。

在大廳的`defaultBehavior`槽中，定義了系統中大多數對象所繼承的預設行為。

### 新建對象

有兩個消息與對象複製有關：

- clone，對象複製，返回包含著與最初對象完全相同的槽和代碼的一個新對象。它用在對象內部，客戶應當使用`copy`。
- copy，複製接收者，可能具有嵌入的複製或初始化。

考慮一個圖形使用者界面有關的例子：

```smalltalk
(desktop activeWindow) draw: (labelWidget copy label: 'Hello, World!').
```

首先進行的是desktop activeWindow，它向桌面對象`desktop`發送消息`activeWindow`，從其擁有的一個窗口列表中返回活動窗口。按從內向外從左至右的次序，接著是labelWidget copy label: 'Hello, World!'，通過`copy`消息製作標籤組件對象`labelWidget`的一個複本，接著向它發送一個消息，將`Hello, World!`放入它的用作「標籤」的`label`槽中。最後將返回的這個組件，發送到這個活動窗口用於「繪製」的`draw`槽中。

### 增加槽

在Self中的對象，可以通過包括新加的槽來修改。這可以通過被推薦使用的圖形編程環境來做，或者直接使用原語`_AddSlots:`。原語與正常關鍵字消息有相同的語法，但是它的名字開始於下劃線字符。給`_AddSlots:`的參數是一個對象文字，它的槽將被複製進入接收者。例如，在大廳中新增叫做`newObject`的對象，並初始化這裡舉出的它的叫做`entries`的槽，採用如下這樣的表達式：

```smalltalk
_AddSlots: (| newObject = (| entries <- list copy …… |) |)
```

因為`_AddSlots:`原語未指定接收者，這裡的消息隱含接收者`self`是大廳，由它來理解產生初始值的消息`list copy`，`list`是周知的原型對象。

## 例子

在下面的例子中，將基於類語言中叫做`Vehicle`的一個簡單的車輛類，重新構造為Self中的`vehicle`對象，從而能夠區分出在轎車和卡車之間共有的行為：

```smalltalk
_AddSlots: (| vehicle <- (|parent* = traits clonable|) |).
```

在大廳中創建了一個叫做`vehicle`的槽，它的值是一個對象文字，同時還創建了一個叫做`vehicle:`的賦值槽。作為`vehicle`槽初始值的對象文字，也就是新建的叫做`vehicle`的對象，包括了一個單一的父槽`parent`，沒有相應的`parent:`，它委託了`traits clonable`對象，這個頂層的特質對象可以理解與複製有關的消息。

然後向這個新建對象繼續增加`name`槽和相應的`name:`槽：

```smalltalk
vehicle _AddSlots: (| name <- 'automobile'|).
```

在大廳中從`vehicle`對象建立一個跑車對象`sportsCar`，並接著向`sportsCar`增加`vehicle`所沒有的用於「開車上班」的一個新的方法槽`driveToWork`：

```smalltalk
_AddSlots: (| sportsCar <- vehicle copy |).
sportsCar _AddSlots: (| driveToWork = ("这个方法的代码") |).
```

在大廳中從`sportsCar`對象建立一個保時捷911對象`porsche911`，接著向新建對象`porsche911`發送一個消息改變它的`name`槽的值：

```smalltalk
_AddSlots: (| porsche911 <- sportsCar copy |).
porsche911 name: 'Bobs Porsche'.
```

對象`porsche911`與它的原型對象`sportsCar`，仍有著完全相同的槽，但是其中的一個槽有著不同的值。

## 環境

Self的一個特徵，是它基於了早期Smalltalk系統所用的某種虛擬機系統。就是說，程序不是像C語言中那樣的獨立實體，而是需要它們的整體記憶體環境來運行。這要求應用程式被裝載入保存記憶體的大塊 (資訊)（chunk）之中，這叫做「快照」或系統映像。這種方式的缺點，是映像有時很大並且笨重；但是調試一個映像，經常被調試一個傳統程序要簡單，因為運行時狀態更容易檢查和修改。在基於原始碼和基於映像的開發之間的不同，是類似於在面向類的和面向原型的面向對象編程之間的區別。

此外，環境是為了讓在系統之中的對象能快速和可持續的變更而定製的。重新構建一個「類」設計，就像從現存的祖先拖動出來方法放入新造的之中一樣容易。簡單任務像測試方法，可以通過製作複本來處理，拖動方法進入這個複本，接著變更它。不同於傳統系統，只有變更了的對象有新代碼，不需要重建任何東西來測試它。如果這個方法有效，可以簡單的把它拖動回祖先之中。

### 性能

Self的VM實現的性能，在某些測試之中大約是優化的C程序速度的一半。這是通過即時編譯技術達到的，它是在Self研究中首創並改進的，能夠使高級語言表現得這麼好。

### 垃圾收集

Self的垃圾收集器使用分代垃圾回收，它按年齡分離對象。通過使用記憶體管理系統記錄頁面寫，可以維護一個寫屏障。這個技術給出了卓越的性能，儘管在運行一些時間之後，出現完全的垃圾收集，要花相當可觀的時間。

### 優化

運行系統選擇性的扁平化調用結構。這給出適當的自身提速，但允許了對不同調用者類型的類型資訊和多版本的代碼的大量緩存。這去除了對做很多方法查找的需要，並允許條件分支語句和硬編碼調用被插入，這經常能給出類似C語言的性能，而又不失去語言層面的通用性，但要建立在完全的垃圾收集系統之上。

## 延伸閱讀

- Published papers on Self
- Series of four articles "Environment and the programming language Self"

## 站外連結

- www.selflanguage.org
- russellallen/self
- Prototype-Based Application Construction Using SELF 4.0
- Automated Refactoring application on sourceforge.net, written for and in Self
- Gordon's Page on Self
- dSelf: distributed extension to the delegation and language Self

## 基本資料

- 日語原文：Self
- 編程范型：面向對象, 基於原型
- 發行時間：1987年
- 設計者：Randall Smith
- 開發者：David Ungar, Randall Smith, 史丹福大學, Sun微系統
- 類型系統：動態, 強類型
- 軟體實現：Self
- 語言家族：Smalltalk
- 受影響於：Smalltalk, APL
- 施影響於：NewtonScript, JavaScript, Io, Agora (程式語言), Squeak, Lua, Factor, REBOL
- 許可證：類BSD許可證