跳轉到內容

Rebol 程式設計/高階/繫結學

來自 Wikibooks,開放世界中的開放書籍

作者:Ladislav Mecir

我要感謝那些以某種方式影響了這篇文章的人。特別是 Galt Barber、Brian D.、Mark Dickson、Elan Goldman、Brian Hawley、Gregg Irwin、Thomas Jensen、Pierre Johnson、Holger Kruse、Volker Nitsch、Larry Palmiter、Patrick Philipot、Gabriele Santilli、Carl Sassenrath、Frank Sievertsen 和 Romano Paolo Tenca。任何錯誤都是我的版權。

參考文獻

[編輯 | 編輯原始碼]

本文中的程式碼在 Rebol/View 2.7.6.3.1 中測試過。其他版本的直譯器可能產生不同的結果。

想要檢查所有示例的讀者可以執行這段程式碼

do http://www.rebol.org/download-a-script.r?script-name=contexts.r

,它定義了本文中的所有函式。

詞語型別

[編輯 | 編輯原始碼]

Rebol 詞語像所有 Rebol 值一樣具有型別。讓我們看看所有可用的詞語型別

type? first [rebol] ; == word!
type? first [rebol:] ; == set-word!
type? first [:rebol] ; == get-word!
type? first ['rebol] ; == lit-word!
type? first [/rebol] ; == refinement!

此外,所有 Rebol 詞語都有一個通用的偽型別 ANY-WORD!

any-word? first [rebol] ; == true
any-word? first [rebol:] ; == true
any-word? first [:rebol] ; == true
any-word? first ['rebol] ; == true
any-word? first [/rebol] ; == true

每個詞語都有一個拼寫。詞語的拼寫是一個字串,它是上面示例詞語共有的屬性之一。我們可以使用 TO-STRING 函式獲取詞語的拼寫

to-string first [rebol] ; == "rebol"
to-string first [rebol:] ; == "rebol"
to-string first [:rebol] ; == "rebol"
to-string first ['rebol] ; == "rebol"
to-string first [/rebol] ; == "rebol"

觀察(不尋常的拼寫):詞語通常不會有一些拼寫,例如包含空格的拼寫、以冒號開頭的拼寫等。另一方面,可以建立具有任何拼寫的詞語,如下所示

unusual: make word! ":unusual word:"
type? unusual ; == word!
to-string unusual ; == ":unusual word:"

觀察(拼寫和詞語相等):拼寫完全相同的詞語是相等的。

說明

equal? first [rebol] first [rebol:] ; == true

反向蘊涵不成立(因為 Rebol 支援別名)。然而,我們至少可以以某種方式反轉蘊涵。

觀察(SAME? 和拼寫):如果兩個詞語根據 SAME? 函式相同,那麼它們具有完全相同的拼寫。

詞語的一個非常重要的屬性是能夠充當變數(引用 Rebol 值)。要設定一個變數 'rebolution 來引用 Rebol 字串 "uprising",我們可以選擇以下之一

rebolution: "uprising"
set 'rebolution "uprising"
set/any 'rebolution "uprising"
set first [rebolution:] "uprising"

等等。

要獲取變數引用的值,我們可以選擇以下之一

:rebolution
get 'rebolution
get/any 'rebolution

等等。

觀察(變數):詞語充當變數的能力與一個稱為繫結的詞語屬性密切相關。詞語是變數當且僅當它被繫結到一個上下文(具有上下文,在上下文之中)。

說明

; a refinement
get/any /rebol
** Script Error: rebol word has no context
** Near: get/any /rebol

找出詞語是否是變數的最簡單方法是使用 BIND? 函式的屬性,該函式對於不是變數的詞語返回 NONE

variable?: func [
    {is the given WORD a variable?}
    word [any-word!]
] [
    found? bind? :word
]

測試

variable? 'rebol ; == true
variable? /rebol ; == false

觀察(上下文唯一性):正如 BIND? 函式幫助我們發現的那樣,對於每個 Rebol 詞語,最多存在一個該詞語被繫結到的上下文。

推論(上下文層次結構):從上面的觀察結果可以得出,在 Rebol 中不存在上下文層次結構,因為上下文層次結構需要某個詞語被繫結到至少兩個不同的上下文,其中一個比另一個“更小”。

觀察(詞語相同):根據 SAME? 函式,兩個詞語相同當且僅當它們具有完全相同的拼寫和相同的繫結。

觀察(BIND? 函式的結果):當使用 BIND? 函式獲取物件中詞語的上下文時,BIND? 函式的結果“不被認為與”物件相同。

說明

o: make object! [a: none]
o-context: bind? in o 'a
same? o o-context ; == false

觀察(相等的詞語不一定具有相同的繫結):實際上,情況恰恰相反。對於任何詞語,我們都可以建立一個具有完全相同的拼寫、相同的型別和不同的繫結的詞語。

different-binding: func [
    {
        for a given WORD yield a word having
        strict equal spelling, equal type and different binding
    }
    word [any-word!] {the given word}
] [
    bind :word use reduce [to word! :word] reduce [to lit-word! :word]
]

讓我們測試一下該函式是否按我們宣告的那樣工作

word1: 'a ; == a
word2: different-binding word1 ; == a
strict-equal? to-string word1 to-string word2 ; == true
equal? type? word1 type? word2 ; == true
equal? bind? word1 bind? word2 ; == false
set word1 1
set word2 2
get word1 ; == 1
get word2 ; == 2

測試表明 WORD1 和 WORD2 具有完全相同的拼寫和相同的型別。它們的繫結不同。它們可以同時引用不同的值,因此它們是不同的變數。另一個測試

word1: /a ; == /a
word2: different-binding word1 ; == /a
same? word1 word2 ; == false
equal? bind? word1 bind? word2 ; == false

BIND 函式

[編輯 | 編輯原始碼]

當我們需要獲得一個詞語,該詞語具有給定 WORDS 詞語的拼寫和型別,以及給定 KNOWN-WORD 的上下文時,我們可以使用 BIND 函式。

BIND 函式的工作原理如下

繫結到無上下文

[編輯 | 編輯原始碼]

觀察(繫結到無上下文):如果 KNOWN-WORD 沒有上下文,BIND 會引發錯誤。

說明

a-word: second first context [rebol: 1] ; == rebol
bind? a-word ; == none
bind 'a a-word
** Script Error: rebol word has no context
** Near: bind 'a a-word

當 WORDS 引數是詞語時繫結

[編輯 | 編輯原始碼]

觀察(有效繫結):如果可能,BIND 函式將產生一個詞語,該詞語具有 WORDS 引數的拼寫和型別,以及 KNOWN-WORD 引數的上下文。

說明

words: 'a ; == a
known-word: use [a b] ['b] ; == b
result: bind words known-word ; == a
equal? bind? known-word bind? result ; == true
same? words known-word ; == false

我們觀察到,結果具有 WORDS 詞語的拼寫和型別,但它不是 WORDS 詞語,因為它被繫結到與 KNOWN-WORD 相同的上下文。

觀察(相等的詞語和有效繫結):如果 WORD1 是一個具有上下文 CONTEXT 的變數,而 WORD2 等於 WORD1,那麼 WORD2 也可以被繫結到 CONTEXT。

觀察(無效繫結):如果 BIND 函式無法產生一個詞語,該詞語具有 WORDS 引數的拼寫和型別,以及 KNOWN-WORD 引數的繫結,那麼 BIND 將返回 WORDS 引數。

words: 'c ; == c
known-word: use [a b] ['b] ; == b
result: bind words known-word ; == c
same? words result ; == true

在這種情況下,BIND 只是返回了 WORDS 詞語。

當 WORDS 引數是程式碼塊時繫結

[編輯 | 編輯原始碼]

觀察(不復制的繫結塊):如果沒有使用 /COPY 修飾,BIND 會用繫結結果替換塊中的元素。此規則有一個例外:BIND 不會繫結塊中包含的修飾。

定義(別名):我們稱拼寫不完全相同的等效詞為別名。

這是我們翻譯成 Rebol 的定義

aliases?: func [
    {find out, if WORD1 and WORD2 are aliases}
    word1 [any-word!]
    word2 [any-word!]
] [
    found? all [
        equal? :word1 :word2
        strict-not-equal? to-string :word1 to-string :word2
    ]
]

推論(詞語相等):當且僅當以下條件之一成立時,兩個詞語相等

  1. 詞語的拼寫完全相同
  2. 詞語是別名

推論(別名和 SAME?):當比較兩個別名時,SAME?函式返回 FALSE。

證明:參見 (SAME?和拼寫) 觀察和我們對別名的定義。

觀察(自動別名):由於 Rebol 試圖做到不區分大小寫,直譯器通常(除了不一致的情況)“認為”拼寫僅在大小寫方面不同的詞語是別名。

觀察(ALIAS):可以使用 ALIAS 函式顯式定義別名。

; let's create an alias 'revolutionary for the word 'rebol
alias 'rebol "revolutionary"
; 'rebol and 'revolutionary will be equal words with different spelling:
equal? 'rebol 'revolutionary ; == true
strict-equal? to-string 'rebol to-string 'revolutionary ; == false
aliases? 'rebol 'mean ; == false
aliases? 'rebol 'rebol ; == false
aliases? 'rebol 'revolutionary ; == true
aliases? 'system 'SYSTEM ; == true

觀察(ALIAS 返回值):ALIAS 函式返回未繫結的詞語。

y: alias 'x "xx" ; == xx
bind? y ; == none

觀察(變數一致性):當且僅當兩個詞語相等且它們的繫結也相等時,它們才是一個變數。

same-variable?: func [
    {are WORD1 and WORD2 the same variable?}
    word1 [any-word!]
    word2 [any-word!]
] [
    found? all [
        equal? :word1 :word2
        equal? bind? :word1 bind? :word2
    ]
]

觀察(ALIASES?的備選定義):根據我們的先前觀察,此定義等效於我們最初的定義

aliases?: func [
    {find out, if WORD1 and WORD2 are aliases}
    word1 [any-word!]
    word2 [any-word!]
    /local context
] [
    found? all [
        equal? :word1 :word2
        (
            if context: any [bind? :word1 bind? :word2] [
                word1: in context :word1
                word2: in context :word2
            ]
            ; WORD1 and WORD2 have equal binding now
            not same? :word1 :word2
        )
    ]
]

備選定義看起來更復雜,但由於它不需要操作字串,因此它更快。

上下文詞語

[編輯 | 編輯原始碼]

BIND?函式允許我們找到給定詞語的上下文。相反的任務是找到所有在給定 CONTEXT 上下文中的詞語。它可以按照以下步驟完成

context-words?: func [
    {get the words in a given CONTEXT}
    context [object!]
] [
    bind first context context
]

觀察(簡化的上下文詞語集):作為 first context 表示式的結果獲得的塊是簡化的上下文詞語集。與上述函式的結果相反,它包含未繫結的詞語。此外,與上述函式的結果類似,它不包含其詞語的別名,並且只包含 WORD!資料型別的詞語。

說明

alias 'rebol "rebellious"
o: make object! [rebellious: 1]
first o ; == [self rebellious]
bind? first first o ; == none
in o 'rebol ; == rebol

全域性上下文

[編輯 | 編輯原始碼]

定義(全域性上下文):全域性上下文可以定義為

global-context: bind? 'system

注意:這不是唯一的選項,另一個選項是將其定義為 SYSTEM/WORDS 物件。以上定義為我們提供了全域性詞語的最簡單定義。

定義(全域性詞語/全域性變數):繫結到全域性上下文的詞語我們稱之為全域性詞語(全域性變數)

global?: func [
    {find out if a WORD is global}
    word [any-word! object!]
] [
    same? global-context bind? :word
]

觀察(MAKE、TO、LOAD、BIND 和全域性上下文):由 MAKE WORD!、MAKE SET-WORD!、MAKE GET-WORD!、MAKE LIT-WORD!、MAKE REFINEMENT!、TO WORD!、TO SET-WORD!、TO GET-WORD!、TO LIT-WORD!、TO REFINEMENT!、LOAD 和 BIND WORD 'SYSTEM 建立的詞語是全域性的。

說明

global? make word! first first rebol/words ; == true
global? to word! first first rebol/words ; == true

觀察(自動增長):可以使用 MAKE、TO、LOAD 和 BIND 函式擴充套件全域性上下文。另一方面,IN 函式不會擴充套件全域性上下文。

觀察(MAKE、TO 和未繫結的詞語):如果 SPEC 引數是字串,則 MAKE BLOCK!、TO BLOCK!及其子塊的結果塊中包含的所有詞語都是未繫結的。

說明

bind? first make block! "unbound" ; == none
bind? first first first make block! "[[unbound-too]]" ; == none

區域性上下文

[編輯 | 編輯原始碼]

定義(區域性詞語/區域性變數):我們稱既不是未繫結也不是全域性的詞語為區域性詞語(區域性變數)

local?: func [
    {find out, if a WORD is local}
    word [any-word!]
] [
    not any [
        none? bind? :word
        global? :word
    ]
]

定義(區域性上下文):如果一個上下文的詞語是區域性詞語,則稱該上下文為區域性上下文。

觀察(區域性上下文型別):使用者定義的物件、函式上下文和 USE 上下文是區域性上下文。除了這些,我們還可以使用 BIND?函式將使用者定義的物件和埠“轉換為”上下文,並使用 DISARM 函式將錯誤轉換為物件。所有結果都是區域性上下文。函式和 USE 上下文與所有其他上下文型別之間的主要區別在於,函式和 USE 上下文不需要包含等於詞語 'self 的詞語。

觀察(擴充套件區域性上下文):區域性上下文不可擴充套件。

觀察(DIFFERENT-BINDING 函式的結果):DIFFERENT-BINDING 函式的結果(如我們上面定義的)始終是區域性詞語。

計算繫結

[編輯 | 編輯原始碼]

讓我們觀察一下 Rebol 直譯器在評估示例程式碼字串時的行為

code-string: {'f 'g 'h use [g h] [colorize "USE 1" 'f 'g 'h use [h] [colorize "USE 2" 'f 'g 'h]]}

COLORIZE 函式將程式碼列表著色如下

  • 未繫結的詞語將為 棕色
  • 全域性詞語將為 藍色
  • 由第一個 USE 評估繫結的詞語將為 紅色
  • 由第二個 USE 評估繫結的詞語將為 洋紅色
emit: func [text [char! string! block!]] [
    append result either block? text [rejoin text] [text]
]

colorize: func [
    {emit a table row containing text and the colorized code block}
    text [string!]
    /local space?
] [
    emit ["^/|-^/| " text "^/| "]
    space?: ""
    parse code-block rule: [
        (
            emit [space? #"["]
            space?: ""
        )
        any [
            [
                set word any-word! (
                    emit [
                        space?
                        {<font color="}
                        case [
                            not bind? :word ["brown"]
                            global? :word ["blue"]
                            equal? bind? :word bind? code-block/6/4 ["red"]
                            equal? bind? :word bind? code-block/6/8/5 [
                                "magenta"
                            ]
                        ]
                        {">}
                        mold :word
                        </font>
                    ]
                ) | into rule | set word skip (
                    emit [space? mold :word]
                )
            ]
            (space?: " ")
        ]
    ]
]

讓我們觀察程式碼是如何被解釋的

; the result will be a wikitable
result: {^{| class="wikitable" border="1"
|-
! Text
! Code}

; first, the interpreter creates a code block
code-block: make block! code-string
colorize "String to block conversion"

; next, the code block is bound to the global context
bind code-block global-context
colorize "Code block bound to the global context"

; and then the code block is interpreted
do code-block

; now we close the table
emit "^/|}^/"

write clipboard:// result

結果(從剪貼簿貼上到這裡)是

文字 程式碼
字串到塊的轉換 ['f 'g 'h use [g h] [colorize "USE 1" 'f 'g 'h use [h] [colorize "USE 2" 'f 'g 'h]]]
繫結到全域性上下文的程式碼塊 ['f 'g 'h use [g h] [colorize "USE 1" 'f 'g 'h use [h] [colorize "USE 2" 'f 'g 'h]]]
USE 1 ['f 'g 'h use [g h] [colorize "USE 1" 'f 'g 'h use [h] [colorize "USE 2" 'f 'g 'h]]]
USE 2 ['f 'g 'h use [g h] [colorize "USE 1" 'f 'g 'h use [h] [colorize "USE 2" 'f 'g 'h]]]

結果證明

  • 在字串到塊轉換之後,CODE-BLOCK 中的所有詞語都是未繫結的
  • 在 CODE-BLOCK 繫結到全域性上下文之後,CODE-BLOCK 中的所有詞語都是全域性的
  • 第一個 USE 呼叫用區域性 USE 1 詞語替換了其主體塊及其子塊中的所有 'g 和 'h 詞語
  • 第二個 USE 呼叫用區域性 USE 2 詞語替換了最內層塊中的 'h

觀察(計算繫結):在解釋過程中,程式碼中包含的 Rebol 詞語的繫結被改變(即詞語被替換為具有不同繫結的詞語),直到它們被正確繫結並評估。這就是為什麼 Rebol 的建立者將這種行為稱為“計算繫結”。

看起來我們在執行上面的 Rebol 程式碼時觀察到一個“範圍層次結構”。正如我們所演示的那樣,這僅僅是計算繫結的副作用。

藉助計算繫結,我們可以輕鬆建立不顯示任何“範圍”屬性的程式碼示例

; create a block CODE-BLK containing a word 'a
code-blk: copy [a]
a: 12

; now append another word 'a to CODE-BLK
make object! [append code-blk 'a a: 13]

code-blk ; == [a a]

; test if CODE-BLK contains equal words
equal? first code-blk second code-blk ; == true

; prove that the CODE-BLK is not a "scope"
equal? bind? first code-blk bind? second code-blk ; == false

CODE-BLK 示例表明,對於程式碼塊來說,在 Rebol 中沒有“當前上下文”這樣的東西,因為在 Rebol 中只有單個詞語與上下文相關聯。

USE 函式

[編輯 | 編輯原始碼]

為了儘可能精確,我們將用 Rebol 編寫 USE 函式行為的描述。

以下函式建立一個新上下文,其中所有單詞都未設定

make-context-model: func [
    {context creation simulation}
    words [block!] {context words, needs to be non-empty}
] [
    bind? first use words reduce [reduce [first words]]
]

USE 的描述

use-model: function [
    {USE simulation, works for non-empty WORDS block}
    [throw]
    words [block!] "Local word(s) to the block"
    body [block!] "Block to evaluate"
] [new-context] [
    unless empty? words [
        ; create a new context
        new-context: make-context-model words
        ; bind the body to the new Context
        bind body new-context
    ]
    do body
]

觀察 (USE-MODEL 和 BODY): USE-MODEL 在將 BODY 引數繫結到新上下文時會修改它的 BODY 引數。如果我們想讓 BODY 引數保持不變,我們應該使用 BIND/COPY 而不是當前的 BIND。

讓我們比較 USE-MODEL 的行為和 USE 的行為

body: ['a]
body-copy: copy body
same? first body first body-copy ; == true
use [a] body
same? first body first body-copy ; == false

正如我們所確定的,USE-MODEL 和原始 USE 都是一樣的。模擬非常準確,它幫助我們發現以下程式碼中的一個錯誤

f: func [x] [
    use [a] [
        either x = 1 [
            a: "OK"
            f 2
            a
        ] [
            a: "BUG!"
            "OK"
        ]
    ]
]
f 1 ; == "BUG!"

解釋/修正

觀察到的 USE 屬性導致函式 F 的主體在第二次 USE 執行期間被修改。修改之後,它不再包含在第一次呼叫 F 期間設定為“OK”的單詞“a”。相反,它只包含在第二次呼叫 F 期間設定為“BUG!”值的單詞“a”。

如果我們以某種方式保留 F 的主體,我們可以獲得正確的行為

f: func [x] [
    use [a] copy/deep [
        either x = 1 [
            a: "OK"
            f 2
            a
        ] [
            a: "BUG!"
            "OK"
        ]
    ]
]
f 1 ; == "OK"

另一種修正行為的方法是使用我們自己的 USE 版本,它不會修改它的主體引數

nm-use: func [
    {
        Defines words local to a block.
        Does't modify the BODY argument.
    }
    [throw]
    words [block!] {Local words to the block}
    body [block!] {Block to evaluate}
] [
    use words copy/deep body
]

建立物件!

[編輯 | 編輯原始碼]

我們需要一個函式來評估 SPEC 引數,就像 MAKE OBJECT! 一樣,這意味著它必須捕獲 RETURN、THROW 和 BREAK

spec-eval: func [
    {evaluate the SPEC like MAKE OBJECT! does}
    spec [block!]
] [
    any-type? catch [loop 1 spec]
]

MAKE OBJECT! 模擬

make-object!-model: function [
    {MAKE OBJECT! simulation}
    spec [block!]
] [set-words object sw] [
    ; find all set-words in SPEC
    set-words: copy [self]
    parse spec [
        any [
                copy sw set-word! (append set-words sw)
            |
                skip
        ]
    ]
    ; create a context with the desired local words
    object: make-context-model set-words
    ; set 'self in object to refer to the object
    object/self: object
    ; bind the SPEC to the blank object
    bind spec in object 'self
    ; evaluate it
    spec-eval spec
    ; return the value of 'self as the result
    return get/any in object 'self
]

觀察 (MAKE-OBJECT!-MODEL 和 SPEC): MAKE-OBJECT!-MODEL 在將 SPEC 引數繫結到新上下文時會修改它的 SPEC 引數。如果我們想讓 SPEC 引數保持不變,我們應該使用 BIND/COPY 而不是當前的 BIND。

描述的行為導致了類似於 USE 部分中描述的錯誤

f: func [x] [
    get in make-object!-model [
        a: "OK"
        if x = 1 [
            a: "BUG!"
            f 2
            a: "OK"
        ]
    ] 'a
]
f 1 ; == "BUG!"

解釋和修正與 USE 函式類似。在遞迴呼叫 F 後位置的 a: “OK” 行中繫結到首先建立的物件 F 的單詞 a: 被繫結到遞迴呼叫期間建立的物件 F 的單詞 a: 替換。因此,表示式 a: “OK” 對首先建立的物件 F 沒有影響,因此它保留了 'a 的最後一個值,即“BUG!”。如果我們保留 F 的主體,我們可以獲得正確的行為

f: func [x] [
    get in make object! copy/deep [
        a: "OK"
        if x = 1 [
            a: "BUG!"
            f 2
            a: "OK"
        ]
    ] 'a
]
f 1 ; == "OK"

如您所見,上面的程式碼在將 BODY 塊繫結到上下文之前深度複製了它。當 FUNC 函式建立 Rebol 函式時,如果未使用深度複製,就會發現類似的錯誤。

建立原型

[編輯 | 編輯原始碼]

這是 MAKE 函式獲取要建立的物件的原型的場景的模擬。首先,我們需要一個特殊的 BIND 類函式

specbind: function [
    {bind only known-words}
    block [block!]
    known-words [block!]
] [p w bind-one kw] [
    bind-one: [
        p:
        [
            copy w any-word! (
                if kw: find known-words first w [
                    change p bind w first kw
                ]
            ) | copy w [path! | set-path! | lit-path!] (
                if kw: find known-words first first w [
                    change p bind w first kw
                ]
            ) | into [any bind-one] | skip
        ]
    ]
    parse block [any bind-one]
    block
]

以下是模擬

make-proto: function [
    {MAKE PROTO simulation}
    proto [object!]
    spec [block!]
] [set-words object sw word value spc body pwords] [
    ; get local words from proto
    set-words: copy first proto

    ; append all set-words from SPEC
    parse spec [
        any [
            copy sw set-word! (append set-words sw) |
            skip
        ]
    ]

    ; create a blank object with the desired local words
    object: make-context-model set-words
    object/self: object

    ; copy the contents of the proto
    pwords: bind first proto object
    repeat i (length? first proto) - 1 [
        word: pick next first proto i
        any-type? set/any 'value pick next second proto i
        any [
            all [string? get/any 'value set in object word copy value]
            all [
                block? get/any 'value
                value: specbind copy/deep value pwords
                set in object word value
            ]
            all [
                function? get/any 'value
                spc: load mold third :value
                body: specbind copy/deep second :value pwords
                set in object word func spc body
            ]
            any-type? set/any in object word get/any 'value
        ]
    ]

    bind spec object
    spec-eval spec
    return get/any in object 'self
]

具有 MAKE OBJECT!-like 處理區域性單詞的函式

[編輯 | 編輯原始碼]

在我們嘗試模擬函式評估之前,我們可以問一下我們是否可以使用與 CONTEXT 函式相同的方法來處理區域性單詞。

答案是肯定的,能夠做到這一點的函式定義如下。

首先是一個函式,可以從函式的 SPEC 中提取所有區域性單詞

locals?: func [
    {Get all locals from a spec block.}
    spec [block!]
    /args {get only arguments}
    /local locals item item-rule
] [
    locals: make block! 16
    item-rule: either args [[
	refinement! to end (item-rule: [end skip]) |
	set item any-word! (insert tail locals to word! :item) | skip
    ]] [[
	set item any-word! (insert tail locals to word! :item) | skip
    ]]
    parse spec [any item-rule]
    locals
]

set-words: func [
    {Get all set-words from a block}
    block [block!]
    /deep {also search in subblocks/parens}
    /local elem words rule here
] [
    words: make block! length? block
    rule: either deep [[
        any [
            set elem set-word! (
                insert tail words to word! :elem
            ) | here: [block! | paren!] :here into rule | skip
        ]
    ]] [[
        any [
            set elem set-word! (
                insert tail words to word! :elem
            ) | skip
        ]
    ]]
    parse block rule
    words
]

funcs: func [
    {Define a function with auto local and static variables.}
    [throw]
    spec [block!] {Help string (opt) followed by arg words with opt type and string}
    init [block!] {Set-words become static variables, shallow scan}
    body [block!] {Set-words become local variables, deep scan}
    /local svars lvars
] [
    ; Preserve the original Spec, Init and Body
    spec: copy spec
    init: copy/deep init
    body: copy/deep body
    ; Collect static and local variables
    svars: set-words init
    lvars: set-words/deep body
    unless empty? svars [
        ; create the static context and bind Init and Body to it
        use svars reduce [reduce [init body]]
    ]
    unless empty? lvars: exclude exclude lvars locals? spec svars [
        ; declare local variables
        insert any [find spec /local insert tail spec /local] lvars
    ]
    do init
    make function! spec body
]

Rebol 函式模型

[編輯 | 編輯原始碼]

我們的 Rebol 函式模型將是一個 Rebol 物件 FUNCTION!-MODEL,它具有適當的屬性。Rebol 函式的完全必要屬性是 SPEC 和 BODY。

為了準確地模擬 Rebol 函式的當前行為,我們的 FUNCTION!-MODEL 需要 CONTEXT、CONTEXT-WORDS 和 RECURSION-LEVEL 屬性來模擬 Rebol 函式在遞迴呼叫期間的行為

function!-model: make object! [
    spec: none
    body: none
    context: none
    context-words: none
    recursion-level: none
]

FUNC 函式模型

[編輯 | 編輯原始碼]

該函式獲取 SPEC 和 BODY 屬性,建立一個新的 FUNCTION!-MODEL 並初始化它。

func-model: function [
    {create a function!-model}
    spec [block!]
    body [block!]
] [result aw] [
    result: make function!-model []

    ; SPEC and BODY are deep copied
    result/spec: copy/deep spec
    result/body: copy/deep body

    result
]

函式呼叫棧模型

[編輯 | 編輯原始碼]

直譯器啟動時呼叫棧為空。

call-stack-model: make block! []

函式評估模型

[編輯 | 編輯原始碼]

我們的模擬從收集函式引數的值並檢查它們的型別開始。

評估函式獲取一個 FUNCTION!-MODEL 以及它應該儲存到其區域性上下文單詞(即其所有引數、可選引數、細化和區域性單詞的值)的值塊。

我們只模擬沒有 THROW/CATCH 屬性的函式最常見的情況。

我們模型的第一部分執行主體

exec: func [body] [do body]

模擬

evaluate-model: function [
    {evaluate a function!-model}
    f-model {the evaluated function!-model}
    values [block!] {the supplied values}
] [old-values result] [
    ; detect recursive call
    if (f-model/recursion-level: f-model/recursion-level + 1) > 1 [
        ; push the old values of context words to the stack
        insert/only tail call-stack-model second f-model/context
    ]
    set/any f-model/context-words values

    ; execute the function body
    error? set/any 'result exec f-model/body

    ; restore the former values from the stack, if needed
    if (f-model/recursion-level: f-model/recursion-level - 1) > 0 [
        ; pop the old values of the context words from the stack
        set/any f-model/context-words last call-stack-model
        remove back tail call-stack-model
    ]

    return get/any 'result
]

我們的模型只使用一個上下文來處理 FUNCTION!-MODEL 的整個生命週期,無需更改其 BODY 的繫結。我將這種行為稱為動態遞迴補丁。

一些測試

probeblk: func [] [
    prin mold blk
    prin ": "
    print mold reduce blk
]

recfun: func-model [x] [
    append blk 'x
    either x = 2 [
        probeblk
    ] [
        evaluate-model recfun [2]
    ]
]

blk: copy []
evaluate-model recfun [1] ; [x x]: [2 2]
probeblk ; [x x]: [1 1]

如果我們將模擬的行為與真實的 Rebol 函式進行比較,我們會得到

recfun: func [x] [
    append blk 'x
    either x = 2 [
        probeblk
    ] [
        recfun 2
    ]
]

blk: copy []
recfun 1 ; [x x]: [2 2]
probeblk ; [x x]: [1 1]

這表明模擬非常準確,並且 Rebol 函式也使用動態遞迴補丁。

雖然動態遞迴補丁可以在某些情況下加速評估,但它也有其缺點

f-returning-x: func [x] [
    func [] [x]
]

f-returning-ok: f-returning-x "OK"
f-returning-ok ; == "OK"
f-returning-bug: f-returning-x "BUG!"
; so far so good, but now:
f-returning-ok ; == "BUG!"

計算繫結函式(閉包)

[編輯 | 編輯原始碼]

正如我們上面所看到的,計算繫結有其優點,而動態遞迴補丁並不理想。結果促使我實現計算繫結函式,並將它們的行為與動態遞迴補丁函式的行為進行比較。

計算繫結函式將在每次被呼叫時建立一個新上下文,並相應地繫結它們的主體。我們甚至可以使用上述模擬的一部分來實現它們。

closure: func [
    [catch]
    spec [block!] {Help string (opt) followed by arg words (and opt type and string)}
    body [block!] {The body block of the closure}
    /local spc item result
] [
    spc: make block! 1 + (2 * length? spec)
    insert/only spc [throw]
    result: make block! 5 + length? spec
    insert result reduce [:do :make :function! spc body]
    parse spec [
        any [
            set item any-word! (
                insert tail result to word! :item
                insert tail spc to get-word! :item
                insert/only tail spc [any-type!]
            ) | skip
        ]
    ]
    throw-on-error [make function! spec result]
]

第一次測試

recfun: closure [x] [
    append blk 'x
    either x = 2 [
        probeblk
    ] [
        recfun 2
    ]
]

blk: copy []
recfun 1 ; [x x]: [1 2]
probeblk ; [x x]: [1 2]

這肯定比以前好多了。第二次測試

f-returning-x: closure [x] [
    func [] [x]
]

f-returning-ok: f-returning-x "OK"
f-returning-ok ; == "OK"
f-returning-bug: f-returning-x "BUG!"
; so far so good, but now:
f-returning-ok ; == "OK"

結束。

華夏公益教科書