exports

December 20, 2021

在 Toy Lang 中,一個 .toy 檔案就是一個模組,載入模組的 .toy 檔案時,會執行整個 .toy 的內容,預設是不匯出任何東西,如果想要匯出變數、函式、類別之類,必須定義 exports 清單。

Toy 語法

math.toy 為例:

import 'sys'

exports = ['min', 'max', 'abs', 'sum', 'Random', 'random', 'pow']

def min(numbers) {
    return numsOrArgs(numbers, arguments).sort().get(0)
}

def max(numbers) {
    return numsOrArgs(numbers, arguments).sort((n1, n2) -> n2 - n1).get(0)
}

def abs(n) {
    return -n if n < 0 else n
}

def sum(numbers) {
    return numsOrArgs(numbers, arguments).reduce((acc, n) -> acc + n, 0)
}

def numsOrArgs(nums, args) {
    return nums if isInstance(nums, List) else args 
}

modulus = 2147483647     # m
multiplier = 1103515245  # a
increment = 12345        # c

... 略

exports 清單中包含之名稱,才會被匯出,若其他模組使用 importimport as,被匯出的名稱,就會成為 Module 實例上的特性,若是使用 from import,只有 exports 中包含之名稱,才能成為 import 的對象,或者是使用 from '/lib/math' import *,這會將 exports 清單中指定的全部名稱,都在環境物件中建立對應的變數,並將原模組中的值指定給該變數。

因為會在環境物件中建立對應的變數,並將原模組中的值指定給該變數,如果有個 lib/some.toy 寫這樣:

exports = ['X', 'FOO']

X = 10
FOO = [1, 2, 3]

那麼在另一個 main.toy 中寫這樣:

from '/lib/some' import *

X = 20
FOO.set(0, 10)

那麼,只是在 main 中的變數 X 被指定了新值,而不會影響 someX 的值,然而,main 中的變數 FOOsome 中的 FOO 參考了同一物件,因此若程式其他部份,試著取得 some 模組的 FOO,結果會得到 [10, 2, 3] 的清單。

若想知道目前已經載入了哪些模組,可以透過 sys 模組的 loadedModules 函式:

import '/lib/toy'
import '/lib/sys'
import '/lib/this'
println(sys.loadedModules())

Toy 實作

exports 這特性,其實是學 Python 的,不過 Python 的模組預設是公開全部的名稱,我偏好想公開的再加進清單。

就如同〈import、import as、from import〉中談過,模組功能是最後才加上去的,模組不參與語法樹,importimport asfrom import 其實也不一定要作為關鍵字,要實作為函式,甚至是清單,都是可以的,也就是說,想這麼使用 import,也可以是選項之一:

import = ['sys', 'math']

回過頭來看 exports,因為模組本身就是個 .toy 檔案,在執行完 .toy 之後,如果有 exports,執行該模組時的環境物件中,就可以取得清單了,這時只要依清單中的名稱,取得對應的物件就可以了,這實作在 module.jsModule 之中:

...
play() {
    const context = Context.initialize(environment, this);
    this.importers.forEach(importer => importer.importTo(context));

    // run module itself
    const moduleContext = this.eval(context, this.parse());
    const exportsValue = moduleContext.variables.get('exports');
    const exports = new Set(exportsValue ? exportsValue.nativeValue().map(p => p.value) : []);
    const exportVariables = new Map(
        Array.from(moduleContext.variables.keys())
                .filter(key => exports.has(key))
                .map(key => [key, moduleContext.variables.get(key)])
    );

    // exports
    const instance = moduleContext.variables.get('this');
    instance.properties = exportVariables;

    this.instance = instance;
    return this.instance;
}

如果沒有 exports 清單的話,會有個空清單,當然,這時就什麼也沒有匯出了,在上頭也看到了 this,在模組頂層,this 會參考至模組物件,也就是 Toy Lang 中 Module 類別的實字。

在上頭也可以看到,實際上只是取得匯出的物件,讓執行 import 的模組之環境物件中之名稱,參考至那些物件,因此也只是傳值的行為。