Loading AI tools
来自维基百科,自由的百科全书
Scheme是一種函數式程式設計語言,是Lisp的兩種主要方言之一,不同於與之並列的Common Lisp,Scheme遵循極簡主義哲學,以一個小型語言核心作為標準,加上各種強力語言工具(語法糖)來擴充語言本身[19]。Scheme是第一個使用靜態作用域的Lisp方言,也是第一個引入頭等續體和「乾淨宏」的程式語言。
編程範型 | 多範式:函數式, 指令式, 元程式設計 |
---|---|
語言家族 | Lisp |
設計者 | 小蓋伊·史提爾和傑拉德·傑伊·薩斯曼 |
面市時間 | 1975年 |
目前版本 |
|
型態系統 | 強型別,動態型別 |
作用域 | 詞法 |
副檔名 | .scm .ss |
網站 | https://www.scheme.org/ |
主要實作產品 | |
支援R7RS:BiwaScheme[2], Chibi[3], Chicken, Cyclone[4], Foment[5], Gambit, Gauche, Gerbil[6], GNU Guile, Kawa, Larceny[7], LIPS[8], Loko[9], MIT/GNU Scheme, Mosh[10], Picrin[11], Rapid[12], Sagittarius[13], STklos, TR7[14], Ypsilon[15] 其他:Bigloo, Chez, IronScheme, GNU Mes[16], Scheme 48, SCM, TinyScheme | |
衍生副語言 | |
femtolisp[17], Racket, SIOD, T | |
啟發語言 | |
ALGOL, Lisp, MDL | |
影響語言 | |
Clojure, Common Lisp, Dylan, EuLisp, Haskell, Hop.js, ISLISP, JavaScript, Julia, Lua, R, Racket, Ruby, Rust, S, Scala, Swift LispKit[18] |
在1975年,麻省理工學院的傑拉德·傑伊·薩斯曼與蓋伊·史提爾二世,開發出了Scheme語言最初版本,隨後兩人通過發表「λ論文集」而不斷對它進行完善和推廣。Scheme與λ演算關係十分密切,故將小寫字母「λ」用作標誌。
麻省理工學院與其他一些院校,曾採用Scheme教授電腦科學入門課程。著名的入門教材《電腦程式的構造和解釋》(SICP),利用Scheme來詮釋程式設計[20]。Scheme有眾多實現可視為一個主要優勢[21],然而不同實現之間的差異成為了它的一個劣勢,Scheme掌控委員會聲稱,它是「世上最不可移植的程式語言」,並且是一個「程式語言家族」而非一個單一的語言[22]。
Scheme起源於1958年由約翰·麥卡錫提出的Lisp語言。麥卡錫通過Lisp證明了,經由幾個簡單的算子,與用作匿名函式的借鑒自阿隆佐·邱奇的λ表示法[23],就可以構建出圖靈完備的系統。麥卡錫提出的S-表達式,可以將程式與資料用相同的結構儲存,這被稱為同像性。Scheme的語法即來自S-表達式,這一特性使得在Scheme中實現自循環直譯器變得非常簡單[24]。
在1973年,麻省理工學院的Carl Hewitt提出的一種叫做演員模型的計算模型[25],並用Lisp開發當時叫做Planner-73的新語言來實現它[26]。傑拉德·薩斯曼與蓋伊·史提爾為了理解演員模型,決定在Maclisp工作環境中實現一個微型Lisp直譯器,並接著增加建立演員和傳送訊息的機制[27]。兩人很快發現了演員模型與λ演算之間的相似性,所謂「演員」就是彼得·蘭丁提出的閉包[28],而Joel Moses在1970年已將它介入Lisp用來解決函式參數問題[29];故而實現演員的關鍵,是將詞法作用域介入到Lisp中[30]。
在1975年,基於對λ演算表達能力的理解[31],傑拉德·薩斯曼與蓋伊·史提爾開發出了新型Lisp直譯器[32],並將在其上完成的微縮版的演員實現命名為「Schemer」,後因ITS作業系統檔名的字元數限制而改為Scheme[33]。儘管Hewitt指出了Scheme不能表達特定類型的原語演員[34],Scheme直譯器本身採用的簡約的語法和語意,很快贏得廣泛接受。
在1978年,蓋伊·史提爾二世和傑拉德·傑伊·薩斯曼發表了《修訂的Scheme報告:一種LISP方言》[35],從而完善了Scheme的功能,並正式將其確立為Lisp的一種主要方言。在1998年,二人在總結Scheme歷史時指出,簡單而強大的λ演算,使得Scheme最終得以實現極度的精簡化[36]。
「λ論文集」是傑拉德·薩斯曼與蓋伊·史提爾撰寫的關於Scheme的一系列論文,最早作為麻省理工學院的內部備忘錄發表[37]。通常認定λ論文集包括:
Scheme的業界標準,是由專門的掌控委員會發表的《第n次修訂的演算法語言Scheme報告》(Revisedn Report on the Algorithmic Language Scheme),這種標題形式參照了ALGOL 60標準文件的標題[47]。Scheme曾經由IEEE標準化為IEEE 1178–1990,它於2019年11月07日停用[48]。
1998年通過的R5RS是現在被普遍接受的標準[49]。2007年通過的R6RS[50],做出了很大的變動[51],Scheme社群中有使用者指責它在堆積華而不實的功能[52][53]。Scheme掌控委員會決定在R7RS中,將Scheme分化為兩個獨立而相容的語言[54]:一個是2013年通過的R7RS-small[55],它為教學場合提供對IEEE/R5RS標準的及時更新,R7RS-small目前已經有了一些實現支援[56];而另一個是作為標準庫合集的R7RS-Large[57],它包含了各種提議被接納並進入凍結狀態的Scheme實現要求(SRFI),以此為實際編程場合提供對R6RS標準的繼續完善,現已發表了有關資料結構部份的R7RS-Large過渡草案紅色版[58]。
在正式場合比如Scheme標準的命名法中,提及一個λ表達式或原始過程時,偏好使用詞語「過程」而非「函式」。在一般使用中,詞語「過程」和「函式」是可互換使用的。過程應用有時被正式稱呼為「組合」(combination)。
同其他Lisp一樣,在Scheme中使用術語「thunk」,來提及沒有實際參數的過程。術語「適當」(proper)尾遞迴,稱謂所有Scheme實現的一個性質,它們都進行尾遞迴最佳化,從而支援無限數目的活躍尾遞迴。
Scheme大體上是一個函數式程式語言,並支援其他程式設計範式。它同其他Lisp程式語言家族語言共享了很多特徵。它的非常簡單的語法基於Lisp的S-表達式:即圓括號包圍的列表,其中的字首是算符,而隨後那些的是實際參數。故而Scheme程式由巢狀的列表的序列構成。列表也是Scheme中的主要資料結構,這導致了在原始碼和資料格式之間的緊密等價性,即同像性。Scheme程式可以輕易的動態建立和求值Scheme代碼片段。
Scheme與其他Lisp方言的列表,都是基於最基礎的數據結構有序對(pair)。Scheme從它的Lisp先輩繼承了一組豐富的列表處理原始運算,比如cons
、car
和cdr
。Scheme的變數都使用動態強型別系統,Scheme中的過程都是頭等物件,即頭等函式,因此過程可以作為值賦值給變數,或作為實際參數傳遞給過程。
本章主要集中於語言的創新特徵,它們使得Scheme區別於其他Lisp方言。除非專門指出,這裡描述的特徵都有關於R5RS標準。在本章例子中使用「===> 结果
」表示法,來指示求值在緊前行上的表達式的結果。這同於R5RS中使用的約定。本章節描述的特徵使得Scheme不同於在它之前的其他程式語言。Scheme的這些方面強烈的影響了Scheme語言的所有產品,並共通於始自1975年最初的λ論文,特別是自從R2RS以來[59],所有版本的Scheme程式語言。
Scheme的簡約性,使它成為具備同級別功能的程式語言中最易於實現的語言,這得益於使用λ演算來從更原始的形式匯出多數的語言語法[60]。例如在R5RS標準中,定義了23個基於S-表達式的語法構造,其中14個被歸類為衍生形式或庫形式,它們可以被寫為涉及原則上為lambdad的更基礎形式的宏。正如R5RS(§3.1)所說:「最基礎的變數繫結構造是lambda表達式,因為所有其他的變數繫結構造,都可以依據lambda表達式來做出解釋」[49]。
define
、lambda
、quote
、if
、define-syntax
、let-syntax
、letrec-syntax
、syntax-rules
、set!
do
、let
、let*
、letrec
、cond
、case
、and
、or
、begin
、命名let
、delay
、unquote
、unquote-splicing
、quasiquote
下面是一個宏的例子,它將let
實現為使用lambda
來進行變數繫結的一個表達式:
(define-syntax let
(syntax-rules ()
((let ((var expr) ...) body ...)
((lambda (var ...) body ...) expr ...))))
這裡的(let ((var expr) ...) body ...)
稱為「模式」,模式中起始的let
被稱為「關鍵字」,syntax-rules ()
的空括號中可添入作為輔助關鍵字的叫做「文字」的識別碼,var
、expr
和body
稱為「模式變數」,...
是稱為「省略號」的識別碼,它表示所跟隨的子模式可以匹配零或多個輸入中的元素;而((lambda (var ...) body ...) expr ...)
稱為「模板」。使用上述定義的let
,一個Scheme實現可以將(let ((a 1)(b 2)) (+ b a))
,重寫為((lambda (a b) (+ b a)) 1 2)
,這將實現任務縮減為編碼過程實例化的工作。
不像更早期的Lisp比如Maclisp[61],Scheme像多數現代語言一樣,採用了詞法作用域[62]:在一個程式單元中所有可能的變數繫結,都可以通過閱讀這個程式單元來分析出來,而不需要考慮到它可能在其中被呼叫的那些上下文。這對比於動態作用域,它是早期Lisp方言的特徵,因為在當時的編譯器和直譯器中,用來實現詞法作用域演算法的原始的文字替換方法,關聯著處理代價。在動態作用域的Lisp中,對一個過程內的自由變數的參照,依賴於這個呼叫的上下文,完全有可能參照到這個過程外部的相當不同的繫結。
邱奇的λ演算的數學表示法,啟發了Lisp使用lambda
作為介入一個過程的關鍵字,並影響了Lisp中涉及到使用高階函式的函數式程式設計技術的發展。但是早期的Lisp由於對自由變數的處理方式[63],而不適合表達λ演算[31]。
一個正式的λ系統,擁有一些公理和一個完備的推理規則。這有助於使用數理邏輯和工具來進行分析。在這個系統中,演算可以被看作直接的演繹。λ演算的語法,來自用x, y, z, ...
、圓括號、空格、點號和符號λ形成的遞迴表達式[64]。λ演算的功能包括:首先,充當強力的數理邏輯的起點。其次,它可以縮減編程者在考慮實現細節上的要求,因為它可以用於類比機器求值。最後,λ演算建立了一個堅實的元理論[65]。
在Scheme中,lambda
關鍵字被用於定義匿名過程,並且使用define
基礎形式來定義命名過程,即將一個lambda
過程繫結到指名的全域變數[66]。在Scheme中,(define (foo x) (+ x 1))
與(define foo (lambda (x) (+ x 1)))
,在語法上是等同的。例如有序對可以表示為[67]:
(define (cons x y)
(lambda (m) (m x y)))
(define (car z)
(z (lambda (p q) p)))
(define (cdr z)
(z (lambda (p q) q)))
這樣定義出來的cons
、car
和cdr
,滿足恆等式(car (cons x y))
等於x
,和(cdr (cons x y))
等於y
。甚至遞迴也可以表示為利用λ演算的Y組合子[68]。
詞法作用域的介入[62],通過在某些形式的λ表示法,和在工作程式語言中它們的實際表達之間做出等價,解決了早期Lisp的有關問題。Sussman和Steele展示了,通過將λ表達式用作「控制結構和環境修改器」,而非簡單的過程實例化,可以用這個新語言優雅的匯出其他程式語言包括ALGOL和Fortran的,所有指令式和聲明式語意,和其他Lisp的動態作用域[69]。他們在第一篇λ論文中,與對Scheme的首次描述一起,介入了續體傳遞風格[70],並在後續的論文中,他們推進演示了在這種實際使用中體現出的λ演算的原生能力。
Scheme採用了傳值呼叫,但不同於多數Lisp規定了過程實際參數的求值次序,Scheme不規定求值次序。對比於其他Lisp,Scheme表達式在算符位置(第一個專案)上可以是一個表達式,只要在算符位置上的表達式的結果是一個過程,這種表示就是完全合法的[71]。
在Scheme中,在算符和實際參數位置上的這些表達式的求值次序,可以在逐個呼叫的基礎上由實現來選擇,而唯一的約束是:「運算子和運算數表達式的任何並行求值的效果,被約束為一致於某種順序次序的求值。」(R5RS sec. 4.1.3)[49]
(let
((ev (lambda (n)
(display "Evaluating ")
(display (if (procedure? n) "procedure" n))
(newline)
n)))
((ev +) (ev 1) (ev 2)))
ev
是一個過程,它描述傳遞給它的實際參數,接著返回這個實際參數的值。在呼叫過程+
來加1
和2
之中,表達式(ev +)
、(ev 1)
和(ev 2)
可以按任何次序求值,只要效果上它們不像是並列求值的。因此在上述例子被執行的時候,標準Scheme可以按任何次序來顯示前三行:
Evaluating procedure
Evaluating 1
Evaluating 2
3
然而一行的文字不可以同另一行的文字產生交錯,因為這會違反順序求值約束。
Scheme的代碼塊結構,不同於早先Lisp的語言構造[72]。自從R2RS開始[59],Scheme中的塊,是通過三個「繫結構造」來實現的:let
、let*
和letrec
。例如,下列構造建立一個塊,其中叫做var
的符號被繫結到數10
:
(define var "goose")
;; 这里任何到var的引用都会被绑定到"goose"
(let ((var 10))
;; 语句走到这里时,任何到var的引用都会绑定到10
)
;; 这里任何到var的引用都会绑定到"goose"
依據編程者的需要,塊可以巢狀來建立任意複雜的塊結構。使用塊結構來建立局部繫結,減輕了不然可能發生的命名空間衝突。
let
的變體之一let*
,允許繫結參照在前面相同構造中定義的變數,例如:
(let*
((var1 10)
(var2 (+ var1 12)))
;; 但是var1的定义不可以引用var2
)
let
的另一個變體letrec
,被設計用來確使互遞迴過程可繫結彼此。
下面的例子是侯世達雌雄序列:
;; 将侯世达雌雄序列计算为有序对的列表
(define (hofstadter-male-female n)
(letrec
((female (lambda (n)
(if (= n 0)
1
(- n (male (female (- n 1)))))))
(male (lambda (n)
(if (= n 0)
0
(- n (female (male (- n 1))))))))
(let loop ((i 0))
(if (> i n)
'()
(cons (cons (female i) (male i))
(loop (+ i 1)))))))
(hofstadter-male-female 8)
===> ((1 . 0) (1 . 0) (2 . 1) (2 . 2) (3 . 2) (3 . 3) (4 . 4) (5 . 4) (5 . 5))
在一個單一的letrec
中的所有過程,可以通過名字參照其他的過程,還有在相同的letrec
中此前定義的變數的值,但是它們不可以參照在相同的letrec
中此後定義的變數的值。
let
的變體「命名let
」形式,在let
關鍵字之後有一個識別碼。它將這些let
變數繫結到一個過程的實際參數,這個過程的名字由這個識別碼給出,它的過程體是let
形式的主體。這個過程體可以通過呼叫這個過程達成想要的重複。命名let
被廣泛用於實現迭代。
一個簡單的計數器例子:
(let loop ((n 1))
(if (> n 10)
'()
(cons n
(loop (+ n 1)))))
===> (1 2 3 4 5 6 7 8 9 10)
就像Scheme中的任何過程一樣,在命名let
中建立的這個過程是頭等對象。
Scheme擁有一個迭代構造do
[73],但是在Scheme中更推崇的慣用法,是使用尾遞迴來表達迭代[74]。遵循標準的Scheme實現被要求最佳化尾遞迴,從而支援無限數量的活躍尾遞迴(R5RS sec. 3.5[49]),這個性質被Scheme報告描述為「適當」(proper)尾遞迴,它使得Scheme編程者可以安全的使用遞迴結構來書寫迭代演算法,這在很多時候是更符合直覺的。尾遞迴過程和命名let
形式,提供對使用尾遞迴的迭代的支援。
;; 建造从0到9的平方的列表:
;; 注意:loop简单的就是用作标签的一个任意符号。任何符号都行。
(define (list-of-squares n)
(let loop ((i n) (res '()))
(if (< i 0)
res
(loop (- i 1) (cons (* i i) res)))))
(list-of-squares 9)
===> (0 1 4 9 16 25 36 49 64 81)
在Scheme中續體是頭等對象[75]。自從R2RS開始[59],Scheme提供了過程call-with-current-continuation
,簡寫為call/cc
,它擷取當前續體,將其包裝成逃脫(escape)過程,再繫結到編程者提供的一個過程的唯一形式參數上。如果逃脫過程在後來某個時候被呼叫,它會放棄在後來時候正生效的任何續體,轉而使用在建立這個逃脫過程之時生效的那個續體。逃脫過程與原本呼叫call/cc
的續體,接受相同數目的實際參數;除了用call-with-values
建立的續體,所有續體恰好接受一個值(R5RS sec. 6.4)[49]。
頭等續體使得編程者能夠建立非局部控制結構比如迭代器、協程和回溯。續體可以被用來類比指令式程式語言中return語句的行為。下列函式find-first
接受函式func
和列表lst
,返回lst
中的第一個元素x
,它使得(func x)
返回真。
(define (find-first func lst)
(call/cc
(lambda (return)
(for-each
(lambda (x)
(if (func x)
(return x)))
lst)
#f)))
(find-first integer? '(1/2 3/4 5.6 7 8/9 10 11))
===> 7
(find-first zero? '(1 2 3 4))
===> #f
David Madore在1999年提出的陰陽謎題[76],展示了Scheme可以將續體當作頭等對象處理,繫結它們到變數,和把它們作為給過程的實際參數來傳遞[77]。
對比於Common Lisp,在Scheme中所有的資料和過程共享一個共同的命名空間,而Common Lisp中有函式和資料分離的命名空間,使得一個函式和一個變數可以有相同的名字,並且將一個函式作為值參照時要求特殊的表示法。這有時叫做「Lisp1與Lisp2」差異,二者分別稱謂Scheme的統一的命名空間,和Common Lisp的分立的命名空間[78]。
在Scheme中, 可以使用操縱和繫結資料的原始運算來繫結過程。沒有等價於Common Lisp的defun
和#'
的原始運算。
;; 变量绑定到一个数:
(define f 10)
f
===> 10
;; 变化(改变绑定值)
(set! f (+ f f 6))
f
===> 26
;; 将一个过程赋值给相同的变量:
(set! f (lambda (n) (+ n 12)))
(f 6)
===> 18
;; 将一个表达式的结果赋值给相同的变量:
(set! f (f 1))
f
===> 13
;; 函数式编程:
(apply + '(1 2 3 4 5 6))
===> 21
(set! f (lambda (n) (+ n 100)))
(map f '(1 2 3))
===> (101 102 103)
本章歸檔多年來做出的給與Scheme特定特徵的設計決定,它們不是最初設計的直接產物。
直到R5RS標準,在Scheme中的標準注釋是分號,它使得這行餘下部份對Scheme不可見。許多實現支援可替代的約定,允許注釋擴充為多於一行,而R6RS標準允許其中的兩種:一個完整的S-表達式,可以通過前導#;
而變成一個注釋(介入於SRFI 62[79]),和通過用#|
和|#
圍繞文字,產生的「多行注釋」或「塊注釋」。
在多數Lisp方言包括Common Lisp中,布林表達式中的值NIL
按照慣例被求值為值假。在Scheme中,自從1991年的IEEE標準[48],除了#f
的所有的值,包括在Scheme中寫為'()
的NIL
的等價者,在布林表達式中求值為值真(R5RS sec. 6.3.1)[49]。
在多數Lisp中表示布林值真的常數是T
,而在Scheme中它是#t
。
Scheme的語法可以輕易的通過宏系統來擴充[80]。R5RS標準介入了強力的乾淨宏系統[81],它允許編程者使用一種簡單的模式匹配子語言,向語言增加的新的語法構造(R5RS sec 4.3)[49]。在此前的R4RS標準中,乾淨宏系統已經作為「進階」系統,和「低階」宏系統一起被編排入標準的附錄中[82],二者都被當作對Scheme的擴充而非語言的本質部份。
乾淨宏的實現,也叫做syntax-rules
, 被要求遵守語言的其餘部份的詞法作用域。這是通過針對宏展開的特殊命名和作用域規則來確保的,從而避免在其他程式語言的宏系統中可能出現的常見編程錯誤。R6RS規定了更加複雜的變換系統syntax-case
,它已經作為對R5RS Scheme的一個語言擴充而能夠獲得到有一段時間了。例如:
;; 定义一个宏来实现“if”的具有多个表达式的变体
;; 有真分支而无假分支
(define-syntax when
(syntax-rules ()
((when pred exp exps ...)
(if pred (begin exp exps ...)))))
宏和過程的呼叫看起來非常相似,二者都是S-表達式,但是它們被不同的對待。在編譯器遇到程式中的一個S-表達式的時候,它首先檢視這個符號是否被定義為在當前詞法範圍內的語法關鍵字。如果是這樣,它接著嘗試展開這個宏,將在這個S-表達式尾部的專案當作實際參數,而不用編譯代碼來求值它們,遞迴的重複這個處理過程直到沒有餘留的宏呼叫。如果它不是一個語法關鍵字,編譯器編譯代碼來求值在這個S-表達式尾部的實際參數,並接著求值在這個S-表達式頭部的符號所表示的變數,把它作為過程來呼叫,並把最終的尾部表達式作為實際參數傳遞給它。
多數Scheme實現還提供額外的宏系統。其中最流行是語法閉包、顯式重新命名宏和define-macro
,它是類似於Common Lisp中提供的defmacro
系統的非乾淨宏。
不能指定一個宏是否為乾淨的,是宏系統的一個缺點。可作為替代的展開模型比如作用域集合,提供一種潛在的解決方案[83]。
自從R2RS開始[59],Scheme通過delay
形式和過程force
支援延遲求值。
(define a 10)
(define eval-aplus2 (delay (+ a 2)))
(set! a 20)
(force eval-aplus2)
===> 22
(define eval-aplus50 (delay (+ a 50)))
(let ((a 8))
(force eval-aplus50))
===> 70
(set! a 100)
(force eval-aplus2)
===> 22
delay
原始運算,產生叫做promise的一個對象,它在將來的某個時間點上被詢問從而遞交結果值,promise的原本定義的文字內容會被留存,並且在第一次使用force
之後它的值也會被留存。promise永遠只求值一次。它們可以被用來實現進階的惰性求值構造比如串流[84]。
在R5RS中,給出了delay
和force
的推薦實現,將promise實現為沒有實際參數的一個過程(thunk),並使用記憶化來確保它永遠只求值一次,與呼叫force
的次數無關(R5RS sec. 6.4)[49]。在R6RS標準中,它們不再是原始運算,轉而作為R5RS相容庫的一部份提供(rnrs r5rs (6))。
SRFI 41確使表達有限和無限序列能夠具有非凡的經濟性。例如,這是使用在SRFI 41中的函式定義的一個斐波那契數列[84]:
;; 定义斐波那契序列
(define fibs
(stream-cons 0
(stream-cons 1
(stream-map +
fibs
(stream-cdr fibs)))))
;; 计算这个序列的第一百个数
(stream-ref fibs 99)
===> 218922995834555169026
R5RS標準之前,在其他Lisp中無處不在的eval
過程,在Scheme中沒有等價者。在第一篇λ論文中,曾經將evaluate
描述為「類似於LISP函式EVAL」[85],但在具有詞法作用域的Scheme中,對這個表達式在哪個環境中求值存在困惑。例如,不明確求值下列表達式的結果應當是5
還是6
[86]:
(let ((name '+))
(let ((+ *))
(evaluate (list name 2 3))))
在求值實際參數name
的時候,在外層環境中找到了它的定義;在求值結果表達式(+ 2 3)
的時候,如果在外層環境中求值,結果是兩個運算數的總和;如果在內層環境中求值,這裡符號+
已經被繫結到過程*
的值,結果是兩個運算數的乘積。為此在1978年的最初修訂報告中,將evaluate
替代為enclose
,它接受分別為代碼和執行環境的兩個實際參數[87]。由於各種技術和實際原因,第二次、第三次和第四次修訂報告省略了任何eval
的等價者[86]。
R5RS通過規定三個返迴環境的過程,並提供接受一個S-表達式和一個環境的過程eval
,它在提供的這個環境內求值這個表達式(R5RS sec. 6.5)[49],解決了這個困惑。R6RS通過提供叫做environment
的一個過程進行了擴充,編程者通過它可以精確的指定將哪個對象匯入到求值環境之內。
通過現代Scheme(通常相容於R5RS)來求值表達式expr
,你需要定義如下這樣的一個函式evaluate
:
(define (evaluate expr)
(eval expr (interaction-environment)))
interaction-environment
是直譯器的全域環境。
Scheme規定了數值資料類型的相較完全的集合,包括複數和有理數類型,這在Scheme中叫做「數值塔」(R5RS sec. 6.2[49])。標準對它們作抽象處理,而不委以任何特定的內部表示。
數可以有精確性品質。一個精確數只能從涉及其他精確數的精確運算產生,不精確是傳染的。標準規定任何兩個實現對產生精確數的所有運算都必須產生等價的結果。
R5RS標準規定了過程exact->inexact
和inexact->exact
,它們可以用於改變一個數的精確性。inexact->exact
產生「在數值上最接近實際參數的精確數」。exact->inexact
產生「在數值上最接近實際參數的不精確數」。R6RS標準在主報告中省略了這些過程,而是在標準庫中將它們規定為R5RS相容過程(rnrs r5rs (6))。
在R5RS標準中,不要求Scheme實現去實現整個數值塔,而是它們必須實現「符合實現用途和Scheme語言精神二者的連貫子集」(R5RS sec. 6.2.3)[49]。新的R6RS標準要求實現整個數值塔,並且「精確整數對象和精確有理數對象實際上有無限的大小和精度,並且對於實現特定過程...所以它們在得到精確實際參數時總是返回精確結果」(R6RS sec. 3.4, sec. 11.7.1)[50]。
在支援精確有理複數的實現中的精確算術例子:
;; 三个有理实数和两个有理复数的总和
(define x (+ 1/3 1/4 -1/5 -1/3i 405/50+2/3i))
x
===> 509/60+1/3i
;; 检查精确性
(exact? x)
===> #t
在既不支援精確有理數也不支援複數卻接受有理數表示法的實數的實現中相同算術的例子:
;; 四个有理实数的总和
(define xr (+ 1/3 1/4 -1/5 405/50))
;; 两个有理实数的总和
(define xi (+ -1/3 2/3))
xr
===> 8.48333333333333
xi
===> 0.333333333333333
;; 检查精确性
(exact? xr)
===> #f
(exact? xi)
===> #f
二者實現都符合R5RS標準,但是第二個實現不符合R6RS,因為它沒有實現完整的數值塔。
在Scheme中原始資料類型是不相交的。對於任何Scheme對象,下列謂詞中只有一個可以為真:boolean?
、pair?
、symbol?
、number?
、char?
、>string?
、vector?
、port?
和procedure?
(R5RS sec 3.2)[49]。
作為對比,在數值資料類型之內,對數值值是可以有交疊的。例如,一個整數值可以同時滿足如下謂詞:integer?
、rational?
、real?
、complex?
和number?
(R5RS sec 6.2)[49]。
Scheme在任意對象之間有三種不同類型的等價,它們通過三種不同的「等價謂詞」來指示,即測試等式的關係算符eq?
、eqv?
和equal?
:
eq?
求值為#f
,除非它的實際參數列示在主記憶體中相同的對象;eqv?
大體上同於eq?
,但是特殊處理原始對象(比如字元和數),使得表示相同值的數eqv?
為真,即使它們不參照相同的對象;equal?
比較資料結構比如列表、向量和字串,來確定它們是否有全等的結構並且其內容eqv?
為真(R5RS sec. 6.1)[49]。在Scheme中還存在依賴於類型的等價運算:string=?
和string-ci=?
比較兩個字串(後者進行不依賴大小寫比較);char=?
和char-ci=?
比較字元;=
比較數[49]。
Scheme的輸入和輸出基於了「埠」資料類型(R5RS sec 6.6)[49]。R5RS定義了兩個預設埠,可通過過程current-input-port
和current-output-port
來訪問,它們對應於Unix概念的標準輸入和標準輸出。多數實現還提供current-error-port
。標準通過標準過程比如with-input-from-file
和with-output-to-file
,支援輸入和標準輸出的重新導向。多數實現提供有重新導向能力的字串埠,使用在SRFI 6中描述的過程[88],確使很多常規的輸入-輸出運算能在字串緩衝區上進行而非在檔案上。R6RS標準規定了更多複雜和有能力的埠過程和很多新的埠類型。
下面的例子是使用嚴格的R5RS Scheme書寫的。
例子1,預設輸出到current-output-port
:
(let ((hello0 (lambda() (display "Hello world") (newline))))
(hello0))
例子2,類似例子1但對輸出過程使用可選的埠參數的例子:
(let ((hello1 (lambda (p) (display "Hello world" p) (newline p))))
(hello1 (current-output-port)))
類似例子1,但是輸出被重新導向到一個新建檔案:
;; NB: with-output-to-file is an optional procedure in R5RS
(let ((hello0 (lambda () (display "Hello world") (newline))))
(with-output-to-file "helloworldoutputfile" hello0))
類似例子2,但是具有顯式的檔案打開和埠關閉來傳送輸出到檔案:
(let ((hello1 (lambda (p) (display "Hello world" p) (newline p)))
(output-port (open-output-file "helloworldoutputfile")))
(hello1 output-port)
(close-output-port output-port))
類似例子2,但是使用call-with-output-file
來傳送輸出到一個檔案:
(let ((hello1 (lambda (p) (display "Hello world" p) (newline p))))
(call-with-output-file "helloworldoutputfile" hello1))
對輸入提供了類似的過程。R5RS Scheme提供了謂詞input-port?
和output-port?
。對於字元輸入和輸出提供了write-char
、read-char
、peek-char
和char-ready?
。為了書寫和閱讀Scheme表達式,Scheme提供了read
和write
。在讀運算時,如果輸入埠到達了檔案的結束處,則返回的結果是end-of-file
對象,並且這可以使用謂詞eof-object?
來測試。
除了標準之外,SRFI 28定義了一個基本的格式化過程,類似Common Lisp的format
並以此命名[89]。
在Scheme中,過程被繫結到變數[66]。在R5RS中語言標準正式的授權編程者可以變更內建過程的變數繫結,在效果上重定義它們(R5RS "Language changes")[49]。例如,通過重定義+
可以將它擴充為同接受數一樣接受字串:
(set! +
(let ((original+ +))
(lambda args
(apply
(if (or (null? args) (string? (car args)))
string-append
original+)
args))))
(+ 1 2 3)
===> 6
(+ "1" "2" "3")
===> "123"
在R6RS中所有繫結,包括標準過程,都屬於某個庫,並且所有匯出的繫結都是不可變的(R6RS sec 7.1)[50]。因此,禁止通過變更進行標準過程的重定義。轉而,可以在標準過程的名字下匯入不同的過程,這在效果上類似於重定義。
Scheme語言正式定義於1998年的R5RS和2007年R6RS標準之中。它們描述了標準「形式」,即關鍵字和隨同的語法,它們提供語言的控制結構,和執行通常任務的標準過程。
在標準Scheme中,從一個資料類型轉換到另一個資料類型的過程,在它們的名字中包含字串->
,謂詞結束於一個?
,而改變已經分配了資料的值的過程結束於一個!
。Scheme編程者通常跟從這些命名約定。
本表格描述Scheme中的標準形式。某些形式出現在不止一行之中,因為它們不能被輕易的歸類入語言中的一個單一功能。
在表格中標記了「L」的形式被歸類為標準中的衍生的「庫」形式,並且在實踐中通常被實現為使用了更基礎形式的宏,這使得實現任務比在其他語言中要容易許多。
用途 | 形式 |
---|---|
定義 | define |
繫結構造 | lambda, do (L), let (L), let* (L), letrec (L) |
條件求值 | if, cond (L), case (L), and (L), or (L) |
順序求值 | begin (*) |
迭代 | lambda, do (L), 命名let (L) |
語法擴充 | define-syntax, let-syntax, letrec-syntax, syntax-rules (R5RS), syntax-case (R6RS) |
引述 | quote('), unquote(,), quasiquote(`), unquote-splicing(,@) |
賦值 | set! |
延遲求值 | delay (L) |
注意begin
在R5RS中被定義為庫語法,但是展開者需要知道它來完成分片功能。在R6RS中它不再是庫語法。
下面兩個表格描述在R5RS Scheme中的標準過程。R6RS更加具有可延伸性而如此總結是不實際的。
某些過程出現在不止一行之中,因為它們不能輕易的分類入語言的一個單一功能。
用途 | 過程 |
---|---|
構造 | vector, make-vector, make-string, list |
等價謂詞 | eq?, eqv?, equal?, string=?, string-ci=?, char=?, char-ci=? |
類型轉換 | vector->list, list->vector, number->string, string->number, symbol->string, string->symbol, char->integer, integer->char, string->list, list->string |
數值 | 參見獨立表格 |
字串 | string?, make-string, string, string-length, string-ref, string-set!, string=?, string-ci=?, string<? string-ci<?, string<=? string-ci<=?, string>? string-ci>?, string>=? string-ci>=?, substring, string-append, string->list, list->string, string-copy, string-fill! |
字元 | char?, char=?, char-ci=?, char<? char-ci<?, char<=? char-ci<=?, char>? char-ci>?, char>=? char-ci>=?, char-alphabetic?, char-numeric?, char-whitespace?, char-upper-case?, char-lower-case?, char->integer, integer->char, char-upcase, char-downcase |
向量 | make-vector, vector, vector?, vector-length, vector-ref, vector-set!, vector->list, list->vector, vector-fill! |
符號 | symbol->string, string->symbol, symbol? |
有序對和列表 | pair?, cons, car, cdr, set-car!, set-cdr!, null?, list?, list, length, append, reverse, list-tail, list-ref, memq. memv. member, assq, assv, assoc, list->vector, vector->list, list->string, string->list |
辨識謂詞 | boolean?, pair?, symbol?, number?, char?, string?, vector?, port?, procedure? |
續體 | call-with-current-continuation (call/cc), values, call-with-values, dynamic-wind |
環境 | eval, scheme-report-environment, null-environment, interaction-environment (可選) |
輸入/輸出 | display, newline, read, write, read-char, write-char, peek-char, char-ready?, eof-object? open-input-file, open-output-file, close-input-port, close-output-port, input-port?, output-port?, current-input-port, current-output-port, call-with-input-file, call-with-output-file, with-input-from-file(可選), with-output-to-file(可選) |
系統介面 | load (可選), transcript-on (可選), transcript-off (可選) |
延遲求值 | force |
函數式程式設計 | procedure?, apply, map, for-each |
布林運算 | boolean? not |
在其名字中包含-ci
的字串和字元過程,在它們的實際參數之間進行不依賴大小寫的比較:相同字元的大寫和小寫版本被認為是相等的。
用途 | 過程 |
---|---|
基本算術算符 | +, -, *, /, abs, quotient, remainder, modulo, gcd, lcm, expt, sqrt |
有理數 | numerator, denominator, rational?, rationalize |
近似 | floor, ceiling, truncate, round |
精確性 | inexact->exact, exact->inexact, exact?, inexact? |
不等式 | <, <= , >, >=, = |
雜項謂詞 | zero?, negative?, positive? odd? even? |
極大和極小 | max, min |
三角 | sin, cos, tan, asin, acos, atan |
指數 | exp, log |
複數 | make-rectangular, make-polar, real-part, imag-part, magnitude, angle, complex? |
輸入-輸出 | number->string, string->number |
類型謂詞 | integer?, rational?, real?, complex?, number? |
接受多於二個實際參數的-
和/
在R5RS中被定義了但留作為可選的。
由於Scheme的極簡主義,很多常見過程和語法形式不是由標準定義的。 為了保持核心語言很小並促進擴充的標準化,Scheme社群擁有「Scheme實現要求」(SRFI)流程,通過對擴充提案的仔細研討來定義擴充庫。這提升了代碼可移植性。很多SRFI被幾乎所有Scheme實現支援。
在不同的實現中具有相當廣泛支援的SRFI包括[90]:
set!
args-fold
:程式實際參數處理器cond
子句Scheme的精簡設計,使得程式語言設計人士與愛好者,特別鍾愛研究它,故而它有不斷湧現的新實作,而活躍開發的實作也在持續跟進語言標準更新[91]。儘管Scheme有眾多實現是它的一個主要長處[21],但是由於Scheme沒有庫函式標準,造成了很多不可互通的實作互相競爭,試圖使用Scheme編寫既複雜又便於移植的程式,往往比較困難[22]。R6RS和R7RS-Large,在核心語言之外制定了庫函式標準,使得編譯器開發者和貢獻者可以實現Scheme的可移植庫。
幾乎所有Scheme實作都有傳承自Lisp的「讀取﹣求值﹣輸出循環」互動模式,一些Scheme實作亦可作為編譯器。很多用C語言及衍生語言寫成的軟體,都利用Scheme作為手稿語言,很多嵌入式系統語言即是基於Scheme。Chez Scheme和Larceny等實現,可以將Scheme程式編譯為本機二進制碼。Gambit和Chicken等實現,可將Scheme程式編譯為C代碼,Bigloo還能編譯成JVM位元組碼或者.Net位元組碼。
一些實現支援額外的特徵。比如Kawa提供與Java類別的整合,而Scheme到C編譯器通常易於使用C寫成的外部庫,甚至允許將實際C代碼嵌入到Scheme原始碼中。另一個例子是用java書寫的Pvts,它提供了支援Scheme學習的一組可視工具。
很多著名的電腦科學院校都利用Scheme來教授入門級課程。以下為一些最為著名的教授Scheme的學校:
自由軟體影像處理程式GIMP利用Scheme為嵌入式指令碼語言[97]。GNOME中有到核心庫的一個GNU的擴充語言Guile包裝器[98]。在2012年出現的Julia所採用的語法解析器,是用Scheme方言Femtolisp實現的。
Seamless Wikipedia browsing. On steroids.
Every time you click a link to Wikipedia, Wiktionary or Wikiquote in your browser's search results, it will show the modern Wikiwand interface.
Wikiwand extension is a five stars, simple, with minimum permission required to keep your browsing private, safe and transparent.