Racket編程指南
17創造語言
前一章中定義的宏(macro)工具讓一個程序員定義語言的語法擴展,但一個宏在以下兩個方面是有限的:
一個宏不能限制上下文中可用的語法或改變包圍的表的意義;
一個宏僅在語言辭彙約定的參數範圍內能夠擴展語言中的語法,如用括弧對帶子表的宏名分組和用標識符、關鍵字和原意的核心語法。
讀取器層和擴展器層之間的區別在《列表和Racket語法》中介紹。
也就是說,一個宏只能擴展一種語言,它只能在擴展器(expander)層起作用。Racket為定義擴展器(expander)層的起始點提供額外的工具,以便擴展讀取器(reader)層,以便定義讀取器(reader)層的起始點,也以便封裝一個讀取器(reader)和擴展器(expander)起始點為一個方便命名的語言。
17.1模塊(module)語言
當使用手寫module表來書寫模塊,模塊路徑在新模塊名提供模塊最初導入後指定。由於初始導入模塊決定了模塊主體中可用的最基本綁定,就像require,最初的導入可以被稱為一個模塊語言(module language)。
最常見的模塊語言(module languages)是racket或racket/base,但你可以通過定義一個合適的模塊定義你自己的模塊語言(module languages)。例如,使用像all-from-out、except-out和rename-out那樣的provide子表,你可以添加、刪除或重命名綁定 從racket創作一個模塊語言(module languages),這個語言是racket}的一個變體:
《module表介紹了module表的正常寫法。
>(require"score)
"("love" "thirty")
17.1.1隱式綁定
如果你試圖從定義你自己的模塊語言(module language)中的racket里刪除太多 ,那麼生成的模塊將不會作為一個模塊語言(module language)正確工作:
eval:2:0: module: no #%module-begin binding in the module"s
language
in: (module identity (quote just-lambda) (lambda (x) x))
#%module-begin表是一個隱式表,它封裝一個模塊的主體。它必須由一個模塊來提供,該模塊將作為模塊語言(module language)使用:
>(require"identity)
#
被racket/base提供的其它隱式表是給函數調用的#%app、給字面量的#%datum和給沒有綁定標識符的#%top:
>(require"ten)
10
像#%app這樣的隱式表可以在一個模塊中明確地使用,但它們的存在主要是為了使一個模塊語言限制或改變隱式使用的意義。例如,一個lambda-calculus模塊語言(module language)可能限制函數為一個單一的參數,限制函數調用提供一個單一的參數,限制模塊主體到一個單一的表達式,不允許字面語法,以及處理非綁定標識符作為無解釋的符號:
>(require"ok)
"z
eval:4:0: lambda: use does not match pattern: (lambda (x)
expr)
in: (lambda (x y) x)
eval:5:0: #%module-begin: use does not match pattern:
(#%module-begin e)
in: (#%module-begin (lambda (x) x) (lambda (y) (y y)))
eval:6:0: #%app: use does not match pattern: (#%app e1 e2)
in: (#%app x x x)
eval:7:0: #%datum: no
in: (#%datum . 10)
模塊語言很少重定#%app、#%datum和#%top,但重定義#%module-begin往往更為有用。例如,當使用模塊構建HTML頁面的描述,那裡一個描述以頁(page)的形式從模塊被導出,一個交替的#%module-begin能幫助消除provide和反引用(Quasiquoting)樣板文件,就像在"html.rkt"那樣:
"html.rkt"
使用"html.rkt"模塊語言(module language),一個簡單的網頁可以不需要明確的定義或導出頁(page)而被描述,並且以quasiquote模式開始而不是表達式模式開始:
>(require"lady-with-the-spinning-head)
>page
"(html (title "Queen of Diamonds") (p "Updated: " "2018-03-26"))
17.1.2使用#langs-exp
在#lang級別上實現一個語言比聲明一個單一的模塊要複雜得多,因為#lang允許程序員控制語言的幾個不同方面。然而,s-exp語言,為了使用帶#lang簡寫的模塊語言(module language)扮演了一種元語言:
和以下內容是一樣的
這裡name來自包含#lang程序的源文件。這個名稱s-exp是S-expression的簡寫形式,這是一個讀取器(reader)層的傳統的名稱的 詞法約定:括弧、標識符、數字、帶反斜杠轉義的雙引號字元串等等。
使用#langs-exp,這個來自於以前的lady-with-the-spinning-head例子可以寫得更緊湊:
在這個指南的稍後邊,《定義新的#lang語言》會講解如何定義自己的#lang語言,但是首先我們講解你如何寫針對Racket讀取器(reader)級的擴展。
17.2讀取器擴展
(part ("(lib scribblings/reference/reference.scrbl)" "parse-reader"))in(part ("(lib scribblings/reference/reference.scrbl)" "top"))provides more on 讀取器擴展.
Racket語言讀取器(reader)層可以通過#reader表擴展。一個讀取器擴展作為一個模塊實現,該模塊在#reader之後命名。模塊導出的函數將原始字元解析成一個被擴展器(expander)層接受的表。
#reader的語法是
#reader?module-path??reader-specific?
在?module-path?命名一個模塊,它提供read和read-syntax函數。?reader-specific?部分是一個字元序列,它被解析為確定——被來自?module-path?的read和read-syntax函數。
例如,假設文件"five.rkt"包含
"five.rkt"
那麼,程序
相當於
因為"five.rkt"的read和read-syntax函數既從輸入流中讀取五個字元又把它們放入一個字元串進而成為一個列表。來自"five.rkt"的讀取器函數沒有義務遵循Racket詞法約定並作為一個單一的數處理連續序列234567。由於只有23456部分被read或read-syntax所接受,7仍然需要用通常的Racket方式進行分析。同樣,來自"five.rkt"的讀取器函數沒有義務忽略空白,以及
相當於
由於"five.rkt"後的第一個字元立即是一個空格。
一個#reader表也能夠在REPL中使用:
17.2.1源位置
read和read-syntax的區別在於read是用於數據,而read-syntax是用來解析程序的。更確切地說,當通過Racket的read解析封閉流時,將使用read函數,當封閉流由Racket的read-syntax函數解析時,使用read-syntax。沒有什麼需要read和read-syntax用同樣的方法解析輸入,但是使它們不同會混淆程序員和工具。
read-syntax函數可以返回與read相同的值,但它通常應該返回一個語法對象(syntax object),它將解析表達式與源位置連接起來。與"five.rkt"的例子不同的是,read-syntax函數通常是直接實現生成語法對象(syntax object),然後read可以使用read-syntax和揭開語法對象(syntax object)封裝器以產生一個原生的結果。
下面的"arith.rkt"模塊實現了一個讀取器以解析簡單的中綴算術表達式為Racket表。例如,1*2+3解析成Racket表(+(*12)3)。支持的運算符是+、-、*和/,而操作數可以是無符號整數或單字母變數。該實現使用port-next-location獲取當前源位置,並使用datum->syntax將原始值轉換為語法對象(syntax object)。
"arith.rkt"
如果"arith.rkt"讀取器在一個表達式位置中使用,那麼它的解析結果將被視為一個Racket表達。但是,如果它被用在引用的表中,那麼它只生成一個數字或一個列表:
"arith.rkt"讀取器也可以在毫無意義的位置中使用。由於read-syntax實現跟蹤源位置,語法錯誤至少可以根據原始位置(在錯誤消息的開頭)引用輸入部分:
17.2.2readtable(讀取表格)
一個讀取器擴展對以任意方式解析輸入字元的能力可以是強大的,但很多情況下,辭彙擴展需要一個不那麼一般但更可組合的方法。同樣,Racket語法的擴展器(expander)等級可以通過宏(macros)來擴展,Racket語法的reader等級可以通過一個readtable來複合擴展。
Racket讀取器是一個遞歸下降解析器,以及readtable映射特徵到解析處理器。例如,默認readtable映射(到一個處理器,它遞歸解析子表直到找到一個)。current-readtable參數(parameter)決定readtable由read或read-syntax使用。而不是直接解析原始特徵,一個讀取器擴展可以安裝一個擴展的readtable然後鏈接read或read-syntax。
參見《動態綁定:parameterize》——對《parameters》的介紹。
make-readtable函數構建一個新的readtable作為一個現有的一個擴展。它根據一個字元、一個字元映射類型和(對於的某些映射的類型)一個解析程序接受一系列規範。例如,擴展readtable使$可以用來開始和結束中綴表達式,實現read-dollar函數並使用:
read-dollar協議要求函數接受不同參數的數值,這取決於它是否被用於read或read-syntax模式。在read模式中,解析器函數給出兩個參數:觸發解析器函數的字元和正在讀取的輸入埠。在read-syntax模式中,函數必須接受提供字元源位置的四個附加參數。
下面的"dollar.rkt"模塊根據被"arith.rkt"提供的read和read-syntax函數定義了一個read-dollar函數,並將read-dollar連同新的read和read-syntax函數放在一起,它安裝readtable並鏈接Racket的read或read-syntax:
"dollar.rkt"
這個讀取器擴展,一個單一的#reader可用於一個表達式的開始以使交換中綴演算法的$的多重使用成為可能:
※什麼時候,我竟喪失了作為人的資格?
※何為藝術的邊界?藝術何為?
TAG:全球大搜羅 |