Icon是一門領域特定高階程式語言,有著「目標(goal)導向執行」特徵,和操縱字串和文字模式的很多設施。它衍生自SNOBOL和SL5字串處理語言[7]。Icon不是物件導向的,但在1996年開發了叫做Idol的物件導向擴充,它最終變成了Unicon

Icon
編程範型多範式:面向文字, 結構化
設計者Ralph Griswold英語Ralph Griswold
面市時間1977年,​47年前​(1977
目前版本
  • v9.5.23a(2023年8月19日;滾動更新)[1]
  • 951(2013年6月5日;穩定版本)[2]
編輯維基數據鏈結
型態系統動態
許可證公有領域
網站www.cs.arizona.edu/icon
主要實作產品
Icon, Jcon
衍生副語言
Unicon
啟發語言
SNOBOL[3], SL5[4], ALGOL
影響語言
Unicon, Python[5], Goaldi

歷史

編輯

在1971年8月,SNOBOL的設計者之一Ralph Griswold英語Ralph Griswold離開了貝爾實驗室,成為了亞利桑那大學的教授[8]。他那時將SNOBOL4介入為研究工具[9]

作為最初在1960年代早期開發的語言,SNOBOL的語法帶有其他早期程式語言的印記,比如FORTRANCOBOL。特別是,語言是依賴列的,像很多要錄入到打孔卡的語言一樣,有著列布局是很自然的。此外,控制結構幾乎完全基於了分支,而非使用,而塊在ALGOL 60中介入之後,已經成為了必備的特徵。在他遷移到亞利桑那的時候,SNOBOL4的語法已然過時了[10]

Griswold開始致力於用傳統的流程控制結構如if…then,來實現SNOBOL底層的成功和失敗概念。這成為了SL5,即「SNOBOL Language 5」的簡寫,但是結果不令人滿意[10]。在1977年,他考慮設計語言的新版本。他放棄了在SL5中介入的非常強力的函式系統,介入更簡單的暫停和恢復概念,並為SNOBOL4自然後繼者開發了新概念,具有如下的原則[10]

  • SNOBOL4的哲學和語意基礎;
  • SL5的語法基礎;
  • SL5的特徵,排除廣義的過程機制。

新語言最初叫做SNOBOL5,但因為除了底層概念外,全都與SNOBOL有著顯著的差異,最終想要一個新名字。在這個時候Xerox PARC發表了他們關於圖形化使用者介面的工作,術語「icon」從而進入了電腦詞彙中。起初確定為「icon」而最終選擇了「Icon」[10]

基本語法

編輯

Icon語言衍生自ALGOL類的結構化編程語言,因而有著類似CPascal的語法。Icon最類似於Pascal的,是使用了:=語法的賦值,procedure關鍵字和類似的語法。在另一方面,Icon使用C風格的花括號來結構化執行分組,並且程式開始於執行叫做main的過程。

Icon還在很多方面分享了多數手稿語言(還有SNOBOL及SL5)的特徵:變數不需要聲明,類型是自動轉換的,就說數字和字串可以自動來迴轉換。另一個常見於很多而非全部的手稿語言的特徵是,缺少行終止字元;在Icon中,不結束於分號的行,若其確有意義則由暗含的分號來終結。

過程是Icon程式的基本建造塊。儘管它們使用Pascal名稱,但工效上更像C函式並可以返回值;在Icon中沒有function關鍵字。

procedure main() 
    write("Hello, world!")
end

目標導向執行

編輯

Icon的關鍵概念之一就是其控制結構基於表達式的「成功」或「失敗」,而非大多數其他程式語言中的布林運算。這個特徵直接衍生自SNOBOL,在其中表達式求值、模式匹配和模式匹配連帶替換,都可以跟隨著成功或失敗子句,用來指定在這個條件下要分支到一個語句標籤。例如,下列代碼列印「Hello, World!」五次[11]

* 打印Hello, World!五次的SNOBOL程序 
      I = 1
LOOP  OUTPUT = "Hello, World!"
      I = I + 1
      LE(I, 5) : S(LOOP)
END

要進行迴圈,在索引變數I之上呼叫內建的函式LE()(小於等於),並且S(LOOP)測試它是否成功,即在I小於等於5之時,分支到命名標籤LOOP而繼續下去[11]

Icon保留了基於成功或失敗的控制流程的基本概念,但進一步發展了語言。一個變更是將加標籤的GOTO式的分支,替代為面向塊的結構,符合在1960年代後期席捲電腦工業的結構化編程風格[10]。另一個變更是允許失敗沿著呼叫鏈向上傳遞,使得整個塊作為一個整體的成功或失敗。這是Icon語言的關鍵概念。而在傳統語言中,必須包括基於布林運算的測試成功或失敗的代碼,並接著基於產出結果進行分支,這種測試和分支是原生於Icon代碼的,而不需要明確的寫出[12]。考慮如下複製標準輸入標準輸出的簡單代碼:

while a := read() then write(a)

它的含義是:「只要讀取不返回失敗,呼叫寫出,否則停止」[13]。在Icon中,read()函式返回一行文字或&fail&fail不是簡單的Java中的特殊返回值EOF(檔案結束)的類似者,因為它被語言依據上下文明確理解為意味著「停止處理」或「按失敗狀況處理」。這裡即使read()導致一個錯誤它都會工作,比如說如果檔案不存在。在這種情況下,語句a := read()會失敗,而寫操作簡單的不呼叫。

成功和失敗將沿著呼叫鏈向上傳遞,意味著可以將函式呼叫嵌入其他函式呼叫內,在巢狀的函式呼叫失敗時,它們整體停止。例如,上面的代碼可以精簡為[14]

while write(read())

read()命令失敗的時候,比如在檔案結束之處,失敗將沿著呼叫鏈上傳,而write()也會失敗。while作為一個控制結構,在失敗時停止。Icon稱謂這個概念為「目標導向執行」,指稱這種只要某個目標達到執行就繼續的方式。在上面的例子中目標是讀整個檔案;讀命令在有資訊讀到的時候成功,而在沒有的時候失敗。目標因此直接編碼於語言中,不用再去檢查返回碼或類似的構造。

Icon使用目標導向機制用於進行傳統的布林測試,儘管有著微妙的差異。一個簡單的比較如if a < b then write("a is smaller than b"),這裡的if子句,不像在多數語言中那樣意味著:「如果右側運算求值為真」;轉而它的意味更像是:「如果右側運算成功」。在這種情況下,如果這個比較為真,<算子成功。如果if子句的這個表達式成功,則呼叫then子句,如果它失敗了,則呼叫else子句或下一行。結果同於在其他語言中見到的傳統if…then,如果a小於bif進行then子句。微妙之處是相同的比較表達式可以放置在任何地方,例如:

write(a < b)

另一個不同是<算子如果成功,返回它的第二個實際參數,在這個例子中,如果b大於a,則導致它的值被寫出,否則什麼都不寫。因為並非測試本身,而是一個算子返回一個值,它們可以串聯在一起,允許像if a < b < c這樣的事情[14] ,在多數語言中平常類型的比較下,必須寫為兩個不等式的結合,比如if (a < b) && (b < c)

將成功和失敗的概念與異常的概念相對比是很重要的;異常是不尋常的狀況,不是預期的結果。在Icon中失敗是預期的結果;到達檔案的結束處是預期的狀況而不是異常。Icon沒有傳統意義上的例外處理,儘管失敗經常被用於類似異常的狀況下。例如,如果要讀取的檔案的不存在,read()失敗而不指示出特殊狀況[13]。在傳統語言中,沒有指示這些「其他狀況」的自然方式,典型的例外處理是「丟擲」一個值,下面是用Java處理缺失檔案的例子:

try {
    while ((a = read()) != EOF) {
        write(a);
    }
} catch (Exception e) {
    // 某个事情出错了,使用这个catch来退出循环
}

這種情況需要兩個比較:一個用於檔案結束(EOF)而另一個用於所有其他錯誤。因為Java不允許異常作為邏輯元素來比較,就像Icon中那樣,轉而必須使用冗長的try/catch語法。try塊即使沒有異常丟擲,也強加了效能上的懲罰,Icon避免了這種分攤成本英語Distributed cost

目標導向執行的一個關鍵方面,是程式可能必須在一個過程失敗時倒轉到以前的狀態,這個任務叫做回溯。例如,考慮設定一個變數為一個開始位置,並接著進行可以改變這個值的操作,這是在字串掃描中常見情況,這裡前進游標通過它所掃描的字串。如果這個過程失敗了,任何對這個變數的後續讀取都返回最初的狀態,而非被內部操縱後的狀態是很重要的。對於這種任務,Icon有一個「可逆賦值」算子<-,和「可逆交換」算子<->。例如,考慮如下嘗試在一個更大字串內找到一個模式字串的代碼:

{
  (i := 10) &
  (j := (i < find(pattern, inString)))
}

這個代碼開始於移動i10,這是尋找的開始位置。但是,如果find()失敗,這個塊將作為整體失敗,作為一個不想要的副作用,它導致i的值留下為10。故而應將i := 10替代為i <- 10,指示i在這個塊失敗時應當被重設為它以前的值。這提供了執行中的原子性英語Atomic commit的類似者。

生成器

編輯

在Icon中表達式經常返回一個單一的值,例如5 > x,將求值並且如果x的值小於5則成功並返回x,否則失敗。但是,Icon還包括了過程不立即返回成功或失敗,轉而每次呼叫它們之時返回一個新值的概念。這些過程叫做生成器,並且是Icon語言的關鍵部份。在Icon的用語中,一個表達式或函式的求值產生一個「結果序列」。結果序列包含這個表達式或函式生成的所有可能的值。在結果序列被耗盡的時候,這個表達式或函式失敗。

Icon允許任何過程返回一個單一值或多個值,使用failreturnsuspend關鍵字來控制。缺乏任何這種關鍵字的過程返回&fail,它在執行進行到一個過程的end處的時候發生。例如:

procedure f(x)
  if x > 0 then {
    return 1
  }
end

呼叫f(5)將返回1,而呼叫f(-1)將返回&fail。這將導致不明顯的行為,比如write(f(-1))將什麼都輸出,因為f失敗而暫停了write()的操作[15]

將一個過程轉換成一個生成器,要使用suspend關鍵字,它意味著「返回這個值,並且在再次呼叫時,從這一點開始執行」。例如[13]

procedure ItoJ(i, j)
  while i <= j do {
    suspend i
    i +:= 1
  }
  fail
end

建立一個生成器,它返回一系列的數,開始於i並結束於j,接著在它們之後返回&fail[a]suspend i停止執行,並返回i的值,而不重設任何狀態。當對相同函式做出另一次呼叫的時候,執行在這一點上拾起以前的值。在這種情況下,導致它進行i +:= 1,迴圈回到while的開始處,並接著返回下一個值並再次暫停。這將持續直到i <= j失敗,在這一點上它退出這個塊並呼叫fail。這允許輕易的構造迭代器[13]

另一種類型的生成器建造器是|即「交替英語Alternation (formal language theory)算子」(alternator),它的感觀和運算就像布林算子or,例如:

if y < (x | 5) then write("y=", y)

這看起來是在說「如果y小於x或者5那麼...」,實際上它是生成器的一種簡寫形式,它返回值直到脫離於這個列表的結束處。這個列表的值被注入到運算之中,在這裡是<。所以這個例子,系統首先測試y < x,如果x實際上大於y,它返回x的值,這個測試通過,而y的值在then子句中寫出。然而,如果x不大於y,它失敗了,交替算子繼續,進行y < 5。如果這個測試通過,寫出y。如果y不小於x或者5,交替算子用完了,測試失敗,if子句失敗,而不進行write()。因此,y的值如果小於x5,則它將出現在控制台上,從而履行了布林or的作用。函式不會被呼叫,除非求值它們的參數成功,所以這個例子可以簡寫為:

write("y=", (x | 5) > y)

在內部,交替算子不是簡單的一個or,它還可以用來構造值的任意列表。這可以用來在任意的一組值上迭代,比如:

every i := (1|3|4|5|10|11|23) do write(i)

every類似於while,迴圈經過一個生成器的返回的所有專案,在失敗時退出[15]

因為整數列表在很多編程場景都是很常見的,Icon還包括了to關鍵字來構造「事實上的」整數生成器:

every k := i to j do write(k)

在這種情況下,從ij的值,將注入到write()並寫出多行輸出[15]。它可以簡寫為:

every write(i to j)

Icon不是強型別的,所以交替算子列表可以包含不同類型的專案:

every i := (1 | "hello" | x < 5)  do write(i)

這將依賴於x的值,寫出1"hello"或可能的5

同樣的「合取算子」&,以類似於布林算子and的方式來使用[16]

every x := ItoJ(0,10) & x % 2 == 0 do write(x)

這個代碼呼叫ItoJ並返回一個初始值0,它被賦值給x。接著進行合取的右手端,並且因為x % 2不等於0,它寫出這個值。接著再次呼叫ItoJ生成器,它賦值1x,這使得右手端失敗而不列印任何東西。最終結果是從010的所有偶數的一個列表[16]

生成器的概念對於字串操作是很強大的。在Icon中,find()函式是個生成器。下面的例子代碼,在一個字串中找出"the"的所有出現位置:

s := "All the world's a stage. And all the men and women merely players"
every write(find("the", s))

find()在每次被every恢復的時候,將返回"the"的下一個實例的索引,最終達到字串結束處並失敗。

當然人們有時會想要找到在輸入中某點之後的一個字串,例如,掃描包含多列資料的一個文字檔案。目標導向執行也能起效:

write(5 < find("the", s))

只返回"the"出現在位置5之後的那些位置;否則比較會失敗。成功的比較返回右手側的結果,所以把find()放置到這個比較的右手側是重要的。

搜集

編輯

Icon包括了一些搜集類型,包括列表(它還可以用作堆疊佇列)、表格(在其他語言中也叫做對映或字典)和集合英語Set (abstract data type)等。Icon稱它們為「結構」。搜集是原生的生成器,並可以使用「嘆號語法」來輕易呼叫。例如:

lines := []                    # 建立一个空列表
while line := read() do {      # 循环从标准输入读取行
  push(lines, line)            # 使用类堆栈语法来将行压入列表
}
while line := pop(lines) do {  # 循环于行可以从列表弹出之时
  write(line)                  # 将行写出
}

使用如前面例子中見到的失敗傳播,可以組合測試和迴圈:

lines := []                    # 建立空列表
while push(lines, read())      # 压入直到为空
while write(pop(lines))        # 写直到为空

由於列表搜集是個生成器,可以使使用嘆號語法進一步簡化:

lines := []
every push(lines, !&input)
every write(!lines)

在這種情況下,在write()內的嘆號,導致Icon從陣列一個接一個的返回一行文字,並且在結束處失敗。&input是基於生成器的read()的類似者,它從標準輸入讀取一行,所以!&input繼續讀取行直到檔案結束。

因為Icon是無類型的,列表可以包含任何不同類型的值:

aCat := ["muffins", "tabby", 2002, 8]

在列表內的專案可以包括其他結構。為了建造更大的列表,Icon包括了list生成器;i := list(10, "word")生成包含"wold"10個複本的一個列表。

就像其他語言中的陣列,Icon允許專案按位元置來尋找,比如weight := aCat[4]。就像陣列分片英語Array slicing那樣,索引是在元素之間的,可以通過指定範圍來獲得列表的分片,比如aCat[2:4]產生列表["tabby",2002]

表格本質上是具有任意索引鍵而非僅為整數的列表:

symbols := table(0)
symbols["there"] := 1
symbols["here"] := 2

這個代碼建立使用的0作為任何未知鍵的預設值的一個table。接著向它增加了兩個專案,具有鍵"there""here",和分別的值12

集合也類似於列表,但是只包含任何給定值的一個單一成員。Icon包括了++來產生兩個集合的併集,**用於交集,和--用於差集。Icon包括一些預定義的Cset,即包含各種字元的集合。在Icon中有四個標準Cset&ucase&lcase&letters&digits。可以通過用單引號包圍字串來建造Cset,例如vowel := 'aeiou'.

字串

編輯

在Icon中,字串是字元的列表。作為一個列表,它們是生成器,並可以使用「嘆號語法」來迭代:

every write(!"Hello, world!")

這將在獨立行上列印出字串的每個字元。

子字串可以使用在方括號內的一個範圍規定從字串中提取出來。範圍規定可以返回到一個單一字元的一個點,或字串的一個分片(slice)。字串可以從左或從右索引。在一個字串內的位置被定義為在字元之間:1A2B3C4,也可以從右規定:−3A−2B−1C0。例如:

"Wikipedia"[1]     == "W"
"Wikipedia"[3]     == "k"
"Wikipedia"[0]     == "a"
"Wikipedia"[1:3]   == "Wi"
"Wikipedia"[-2:0]  == "ia"
"Wikipedia"[2+:3]  == "iki"

這裡最後例子採用了x1[i1+:i2] : x2表達式,產生x1i1i1 + i2之間的子字串。

子字串規定可以用作字串內的左值。這可以被用來把字串插入到另一個字串,或刪除字串的某部份。例如:

s := "abc"
s[2] := "123"
# s现在的值是"a123c"
s := "abcdefg"
s[3:5] := "ABCD"
# s现在的值是"abABCDefg"
s := "abcdefg"
s[3:5] := ""
# s现在的值是"abefg"

Icon的下標索引是在元素之間的。給定字串s := "ABCDEFG",索引是1A2B3C4D5E6F7G8。分片s[3:5]是在索引35之間的字串,它是字串"CD"

字串掃描

編輯

對處理字串的進一步簡化是「掃描」系統,通過?來發起,它在一個字串上呼叫函式:

s ? write(find("the"))

Icon稱呼?的左手端為「主語」,並將它傳遞到字串函式中。所呼叫的find()接受兩個參數,尋找的文字作為參數一,而要在其中尋找的字串是參數二。使用?,第二個參數是隱含的,而不由編程者來指定。在多個函式被依次呼叫在一個單一字串上的常見情況下,這種風格可以顯著的所見結果代碼的長度並增加清晰性。

?不是簡單的一種語法糖,它還為任何隨後的字串操作,建立一個「字串掃描環境」。這基於了兩個內部變數,&subject&pos,這裡的&subject是要掃描的字串,而&pos是在這個主語字串內的「游標」或當前位置。例如:

s := "this is a string"
s ? write("subject=[",&subject,"] pos=[",&pos,"]")

將產生:

subject=[this is a string] pos=[1]

內建和使用者定義的函式,可以被用於在要掃描的字串上移動。所有內建函式預設採用&subject&pos,來允許用上掃描語法。比如函式tab (i) : s,它設定掃描位置:產生&subject[&pos:i],並將i賦值到&pos。下列例子代碼,寫出在一個字串內,所有空白界定出的word

s := "this is a string"
s ? {                           # 建立字符串扫描环境
  while not pos(0) do {         # 测试字串结束
    tab(many(' '))              # 跃过任何空白
    word := tab(upto(' ') | 0)  # 下一个word是直到下一个空白或行结束
    write(word)                 # 写这个word
  }
}

將產生:

this
is
a
string

這個例子介入了一些新函式。pos()返回&pos的當前值。為何需要這個函式,而不簡單的直接使用&pos的值,不是顯而易見的;原因是&pos是一個變數,而不能呈現值&fail,而過程pos()能。因此pos()提供對&pos的輕量級包裝,它允許輕易使用Icon的目標導向控制流,而不用針對&pos提供手寫的布林測試。在這種情況下,測試是「&pos是零」,在Icon的字串位置的特異編碼中,零是行結束。如果它不是零,pos()返回&fail,它通過not反轉而使得迴圈繼續。

many()從當前&pos開始,找到提供的Cset參數的一個或多個例子。在這種情況下,它尋找空格字元,所以這個函式的結果是在&pos之後的第一個非空格字元的位置。tab()移動&pos到那個位置,這種情況下再次具有潛在的&fail,例如many()在字元結束處脫離。upto()本質上是many()的反函式;它返回緊前於提供的Cset的例子的位置,接著由另一個tab()來設定&pos。這裡的交替用來在行結束處也停止。

這個例子通過使用更合適的「字分隔」Cset,可以包括句號、逗號和其他標點,還有其他空白字元如tab和不換行空格,能夠變得更加健壯。這個Cset可以接著用於many()upto()

一個更複雜的例子演示了在這個語言內生成器和字串掃描的整合:

procedure main()
  local s
  s := "Mon Dec 8"
  s ? write(Mdate() | "not a valid date")
end
# 定义一个匹配函数
# 它返回匹配day month dayofmonth的一个字符串
procedure Mdate()
  # 定义一些初始值
  static months
  static days
  local retval
  initial {
    days := ["Mon","Tue","Wed","Thr","Fri","Sat","Sun"]
    months := [
     "Jan","Feb","Mar","Apr","May","Jun",
     "Jul","Aug","Sep","Oct","Nov","Dec"
    ]
  }
  every suspend 
    (retval <- 
      tab(match(!days)) ||           # 匹配一个day
      =" " ||                        # 跟随着一个空白
      tab(match(!months)) ||         # 跟随着一个month
      =" " ||                        # 跟随着一个空白
      matchdigits(2)) &              # 跟随着最多2位数字
    (=" " | pos(0)) &                # 要么是空白要么是字符串结束
    retval                           # 最终返回这个字符串
end
# 返回最多n位数字的一个字符串的匹配函数
procedure matchdigits(n)
  local v
  suspend (v := tab(many(&digits)) & *v <= n) & v
end

表達式*x計算x的大小。算子||串接兩個字串。這裡介入了內建函式match (s1,s2,i1,i2) : i3,它匹配初始字串:如果s1 == s2[i1+:*s1],產生i1 + *s1,否則失敗;它設定有預設值:s2&subjecti1s2預設時為&pos,否則為1i20

參見

編輯

註解

編輯
  1. ^ fail在這種情況下是不要求的,因為它緊前於end,增加它是為了清晰性。

參照

編輯
  1. ^ https://github.com/gtownsend/icon/releases/tag/v9.5.23a.
  2. ^ Release 951. 2013年6月5日 [2023年9月19日]. 
  3. ^ Griswold, Ralph E.; Poage, J.F.; Polonsky, Ivan P. The SNOBOL 4 Programming Language 2nd. Englewood Cliffs NJ: Prentice-Hall. 1971. ISBN 0-13-815373-6. 
  4. ^ Ralph E. Griswold, David R. Hanson, "An Overview of SL5", SIGPLAN Notices 12:4:40-50 (April 1977)
  5. ^ Schemenauer, Neil; Peters, Tim; Hetland, Magnus. PEP 255 -- Simple Generators. 2001-12-21 [2008-09-05]. (原始內容存檔於2020-06-05). 
  6. ^ v9.5.22e. [2022-11-09]. (原始內容存檔於2022-11-11). 
  7. ^ Griswold, Ralph E.; Griswold, Madge T. History of the Icon programming language. Bergin, Thomas J.; Gibson, Richard G. (編). History of Programming Languages II. New York NY: ACM Press. 1996. 
  8. ^ Griswold 1981,第609頁.
  9. ^ Griswold 1981,第629頁.
  10. ^ 10.0 10.1 10.2 10.3 10.4 Griswold & Griswold 1993,第53頁.
  11. ^ 11.0 11.1 Lane, Rupert. SNOBOL - Introduction. Try MTS. 26 July 2015 [2022-02-03]. (原始內容存檔於2022-05-09). 
  12. ^ Tratt 2010,第73頁.
  13. ^ 13.0 13.1 13.2 13.3 Tratt 2010,第74頁.
  14. ^ 14.0 14.1 Griswold 1996,第2.1頁.
  15. ^ 15.0 15.1 15.2 Tratt 2010,第75頁.
  16. ^ 16.0 16.1 Tratt 2010,第76頁.

參考書目

編輯

外部連結

編輯