語言工作台實作 - MPS

使用語言工作台與使用傳統特定領域語言非常不同。這是使用 JetBrains 元程式設計系統 (MPS) 建立一個小但有趣的 DSL 的範例。你可以使用它來體驗使用語言工作台的感覺。

2005 年 6 月 12 日



(如果你不熟悉語言導向程式設計和語言工作台,或者至少我不熟悉這些術語的用法,你應該閱讀我在 語言工作台 上的概要文章。本文討論使用語言工作台的範例,並假設你熟悉我在該文章中討論的概念。)

其中一個語言工作台是 元程式設計系統 (MPS),來自 JetBrains。當我撰寫語言工作台文章時,我想在實際的語言工作台中加入一個更實質的範例,以幫助你更清楚了解使用此類工具的感覺。這個範例最後變得很大,所以我決定將它分成一篇獨立的文章。

我決定使用 MPS,並不是因為對哪個語言工作台最好有任何意見(畢竟它們仍處於開發的非常早期階段),而是因為 JetBrains 辦公室就在我家附近。開車距離很近,讓合作變得容易許多。因此,在你閱讀本文時,請記住重點之一是觀察 MPS。本文的真正重點是要讓你體驗使用這類工具的感覺。表面上,每個工具都非常不同,但它們共享許多基礎概念。

JetBrains 在其早期存取計畫中開放了 MPS,允許人們下載 MPS 的開發版本來玩玩看。你可以在其中找到本文的範例。但是,請記住,該工具仍處於開發階段,因此你現在看到的內容可能與我撰寫本文時看到的內容大不相同。特別是某些畫面可能已經改變,而且我不打算讓這裡的螢幕截圖隨著每次變更而保持最新。還有一些粗糙的邊緣,這對於仍在開發中的新型工具來說很常見。然而,我認為它仍然值得一看,因為原則才是最重要的。

協議 DSL

此範例使用了我多次遇到的模式,現在我稱之為 協議分派器。協議分派器的概念是,系統從外部世界接收事件,並根據各種因素對它們做出不同的反應,其中一個主要因素是主機公司與事件相關方的協議。

或許進一步討論這個問題最簡單的方法是展示一個我將用作範例的 DSL 範例。

圖 1:一般方案的合約 DSL。

這段 DSL 指出一個假設的公用事業公司如何對其一般方案中的客戶事件做出反應。合約定義包含值和事件處理常式,兩者都是暫時的 - 其值會隨著時間而改變。

此合約有一個值 - 向客戶收取的電費基本費率。從 1999 年 10 月 1 日起,它被設定為每千瓦時 10 美元,12 月 1 日,它突然上漲到每千瓦時 12 美元。

合約顯示對三種類型的事件的反應:使用(電力)、服務呼叫(例如有人進來修理電表)和稅。處理常式與基本費率相同,也是暫時的;我們可以看到服務呼叫的處理常式也在 12 月 1 日發生變化。

處理常式表示一個簡單的反應 - 將一些貨幣值過帳到一個帳戶。帳戶直接在 DSL 中陳述,金額使用公式計算。此公式可以包括在合約中定義的值以及事件的屬性。使用事件包含一個使用屬性,表示此帳單週期中使用了多少千瓦時的電力。USAGE 事件的過帳規則表示,當我們收到一個使用事件時,我們會將此使用量和基本費率的乘積過帳到客戶的基本使用帳戶。

圖 2:低薪案例的另一個 DSL 片段。

圖 2 顯示第二個合約,這個合約適用於特殊方案中的低薪人士。這裡唯一有趣的補充是,此處的使用公式涉及一個條件式,使用 Excel 語法表示。

關於這些片段的第一個注意事項是,它們非常面向領域,並且在領域方面具有可讀性。儘管 COBOL 推論籠罩著我,但我敢說它們對於非程式設計師領域專家來說是可讀的。

這些 DSL 片段將產生符合以 Java 編寫的架構的程式碼,事實上,這些 DSL 描述了我用於合約調度程式描述中的相同場景。

為了比較,以下是用 Java 編寫的相同組態代碼。

public class AgreementRegistryBuilder {

    public void setUp(AgreementRegistry registry) {
        registry.register("lowPay", setUpLowPay());
        registry.register("regular", setUpRegular());
    }

    public ServiceAgreement setUpLowPay() {
        ServiceAgreement result = new ServiceAgreement();
        result.registerValue("BASE_RATE");
        result.setValue("BASE_RATE", 10.0, MfDate.PAST);
        result.registerValue("CAP");
        result.setValue("CAP", new Quantity(50, Unit.KWH), MfDate.PAST);
        result.setValue("CAP", new Quantity(60, Unit.KWH), new MfDate(1999, 12, 1));
        result.registerValue("REDUCED_RATE");
        result.setValue("REDUCED_RATE", 5.0, MfDate.PAST);
        result.addPostingRule(EventType.USAGE,
                new PoorCapPR(AccountType.BASE_USAGE, true),
                new MfDate(1999, 10, 1));
        result.addPostingRule(EventType.SERVICE_CALL,
                new AmountFormulaPR(0, Money.dollars(10), AccountType.SERVICE, true),
                new MfDate(1999, 10, 1));
        result.addPostingRule(EventType.TAX,
                new AmountFormulaPR(0.055, Money.dollars(0), AccountType.TAX, false),
                new MfDate(1999, 10, 1));
        return result;
    }
    public ServiceAgreement setUpRegular() {
        ServiceAgreement result = new ServiceAgreement();
        result.registerValue("BASE_RATE");
        result.setValue("BASE_RATE", 10.0, MfDate.PAST);
        result.setValue("BASE_RATE", 12.0, new MfDate(1999, 12, 1));
        result.addPostingRule(EventType.USAGE,
                new MultiplyByRatePR(AccountType.BASE_USAGE, true),
                new MfDate(1999, 10, 1));
        result.addPostingRule(EventType.SERVICE_CALL,
                new AmountFormulaPR(0.5, Money.dollars(10), AccountType.SERVICE, true),
                new MfDate(1999, 10, 1));
        result.addPostingRule(EventType.SERVICE_CALL,
                new AmountFormulaPR(0.5, Money.dollars(15), AccountType.SERVICE, true),
                new MfDate(1999, 12, 1));
        result.addPostingRule(EventType.TAX,
                new AmountFormulaPR(0.055, Money.dollars(0), AccountType.TAX, false),
                new MfDate(1999, 10, 1));
        return result;
    }

}

組態代碼並非完全相同。張貼規則會傳遞一個應稅布林標記,我們尚未將其新增至 DSL。此外,公式會被替換為各種 Java 類別,這些類別可針對最常見的案例進行參數化設定 - 這通常比嘗試在 Java 解決方案中動態建立公式來得更好。但我認為基本訊息已傳達出來 - 在 Java 中很難看到網域邏輯,因為 Java 的語法會造成阻礙。對於非程式設計師來說尤其如此。

(如果您有興趣了解產生的架構實際運作的方式,請參閱 合約調度模式 - 我不會在此深入探討。該模式中的範例類似,但並非完全相同。)

您可能已注意到,DSL 範例使用螢幕截圖而非文字 - 這是因為,儘管 DSL 看起來像文字,但它們並非真正的文字。相反地,它們是基礎抽象表示的投影,我們會在編輯器中操作這些投影。

圖 3:新增費率

圖 3 指出這一點。在此,我新增新的基本費率。編輯器指出我需要填入的欄位,並視需要輸入適當的值。我實際上並未輸入太多文字 - 我的主要任務通常是從選取清單中選取。目前,日期以結構化數字輸入,但在完全開發的系統中,您可以使用日曆小工具輸入日期。

其中最有趣的元素之一是在計畫中使用 Excel 風格的公式。以下是我在公式中新增一個單詞時的編輯器。

圖 4:編輯公式。

請注意,快顯視窗包含您可能想要在公式中使用的各種表達式,以及計畫中定義的值,以及在此內容中處理的事件中的屬性。編輯器會使用大量內容知識來協助程式設計師正確輸入程式碼 - 就像後 IntelliJ IDE 所做的一樣。

關於公式的另一點是,它們來自與用於定義協定的語言不同的語言。因此,任何需要使用類 Excel 公式的 DSL 都可以將公式匯入其語言,而無需為自己建立所有定義。此外,這些公式可以包含使用公式語言的語言中的符號。這是語言工作台努力實現的符號整合的一個好範例。您需要能夠採用他人定義的語言,但同時也要盡可能無縫地將它們編織到您自己的語言中。

(作為充分揭露的一點,這個公式語言實際上是為了開發這個範例而編寫的,但它是分開的,因此可以被其他語言使用。這是我們看到一個正在開發的工具以及 MPS 的開發理念的事實的意外:找到 MPS 的有趣應用,並利用這些應用的需求來推動 MPS 的功能和設計。這是我所偏好的開發理念。)

最後一個螢幕截圖顯示了另一個重點。您會注意到,當我切換到公式時,我沒有完成新費率的工作。過去這些類型的智慧型或結構化編輯器所遇到的問題之一是,它們無法處理不正確的輸入。您在繼續之前,每個輸入位元都必須正確。這樣的要求是一個重大的可用性問題。在編程時,您需要能夠輕鬆地進行切換,即使這表示保留無效資訊。對於一個投影編輯器而言,其後果是您需要能夠在抽象表示中處理無效資訊。事實上,您希望能夠執行此操作,並盡可能保持功能。在這種情況下,一個選項是從計畫產生程式碼,忽略有錯誤的那些暫時元素。面對肆意無效時這種強健的行為是語言工作台的一個重要功能。

這裡的 MPS 範例使用類似文字的投影。到目前為止,MPS 專注於這種投影。相比之下,Microsoft DSL 工具則專注於圖形投影。我預計隨著工具的發展,它們將提供文字和圖形投影。儘管建模群體沉迷於說「一張圖片勝過千言萬語」,但文字表示仍然非常有用。我預計成熟的語言工作台將同時支援文字和圖形投影,以及許多人 不認為是程式設計環境 的投影

定義架構

現在我們可以看到語言的外觀,我們可以看看我們如何定義它。我不會在此處說明整個語言定義,我只會挑選一些重點,讓您了解它的運作方式。

圖 5:計畫的架構

圖 5 顯示計畫建構的架構。(我也在左側顯示此合約語言中其他概念的清單。)如果您已執行任何資料建模,或特別是元建模,這應該不會有任何驚喜。我不會在這裡解釋定義的所有元素,只會說明重點。和往常一樣,請記住這目前處於變動中,它可能不再看起來像這樣。

我們定義一個概念,允許它延伸(繼承)其他概念。我們可以在實例和概念(類別層級)中提供概念屬性和連結(類似於屬性和關係)。使用連結,我們指出多重性(雙向)和目標概念。

因此,在這種情況下,我們看到一個計畫由多個值和事件組成,每個值和事件都有自己的定義。圖 6 顯示事件的定義,這非常簡單。

圖 6:事件架構

我們在張貼規則時間屬性中獲得了一些新東西。值和張貼規則最終都受到此類時間規則的約束,因此將具有日期鍵控邏輯的通用能力分解出來是有意義的。因此,我們同時具有時間屬性定義(圖 7)並使用張貼規則的時間屬性(圖 8)進行延伸。

圖 7:時間屬性架構

圖 8:與張貼規則的時間連結架構。

在這種情況下,時間屬性定義有效日期和值的概念。張貼規則時間屬性延伸了這一點,但以與物件導向語言中的繼承略有不同的方式進行。它不是新增連結,而是將此值連結專門化,表示它只能連結到張貼規則。這類似於您在程式語言中使用泛型所要達成的目標。這種專門化關係的想法存在於多種建模語言(包括 UML)中。我發現它對大多數建模沒有太大的用處,但對元建模來說相當方便。您可以將它視為一種特定形式的約束。

最後,我將展示張貼規則本身是如何定義的。

圖 9:張貼規則架構。

它延伸了一個稱為公式的概念,這實際上是獨立公式語言的一部分。

圖 10:來自不同語言的公式架構。

因此,從這裡您可以了解在 MPS 中設定架構所涉及的內容。對於每個概念,您會編輯定義,在元素之間建立各種連結。我懷疑資料模型或 UML 類似類別圖會在此處發揮更好的作用,這類型的內容很適合以圖表形式呈現。不過,這種編輯器樣式也運作得很好,而且可以讓您相當快速地輸入新的語言架構。

如果您正在思考我所希望您思考的內容,您會注意到其他內容。編輯架構的畫面非常類似於編輯 DSL 的畫面。您可能會猜到,有一個用於編輯架構的 DSL,稱為 MPS 中的結構語言。我使用屬於該 DSL 的編輯器編輯架構。這種元循環自舉在語言工作台中很常見。

建立編輯器

現在讓我們來看看如何在 MPS 中定義編輯器。

圖 11:計畫的編輯器定義。

圖 11 顯示計畫的編輯器。一般來說,我們會為模型中的每個概念建立一個編輯器。(並非完全一對一,但這是一個很好的思考起點。)要定義計畫的編輯器,我們會使用編輯器的編輯器(在此處很難避免元循環)。編輯器定義為儲存格的階層。階層的葉節點可以是常數,或對架構中元素的參考。編輯器編輯器(我們現在正在查看的編輯器)使用一些符號來協助界定編輯器的部分。雖然這些符號有點難懂,但重要的是不要擔心語言工作台的符號,因為符號很容易變更。

這個儲存格階層的最上層是整個編輯器的儲存格集合。我透過選取「[/」儲存格來選取此集合。

圖 12:選取儲存格階層的最上層。

當您使用編輯器編輯器時,檢閱員框架(左下角)會變得重要,我們之前沒有使用過它。檢閱員的使用方式與屬性編輯器在 GUI 建構器中的使用方式相同。在此處,檢閱員顯示我們有一個垂直儲存格集合。子儲存格為

  • [> plan 開頭的列
  • 空白列
  • 包含 % value % 及其後續列的列。
  • 另一個空白列
  • 包含 % event % 及其後續列的列。

正如您所見,這個投影的問題之一是,很難找出實際的儲存格階層。使用空白儲存格來顯示縮排和空白也值得懷疑。我預期未來會針對如何讓編輯器編輯器更易於使用進行更多工作。

三個非空白列對應於計畫編輯器中命名計畫的列、值列和事件列。

圖 1:這裡再次顯示一般計畫的範例。請注意,計畫編輯器中的三個非空白列如何對應於計畫中的三個內容區域:名稱、值和事件。

現在,我將深入探討這些內容區域中的第一個,即計畫名稱。這是有助於深入探討的最簡單區域,但即使如此,也很難在像這樣的文章中描述它,因為編輯器編輯器使用檢閱員提供許多資訊,因此我需要使用許多螢幕截圖。

圖 14:計畫列的儲存格集合。

計畫名稱出現在計畫編輯器的整體儲存格集合中的一個單一儲存格內。此儲存格為儲存格集合,這次是兩個子儲存格的水平集合:常數和屬性。(編輯器編輯器以 [/ 表示垂直儲存格集合,並以 [> 表示水平集合。)

圖 15:計畫行中「計畫」字詞的常數。

常數僅為工作計畫。您可以使用常數儲存格將任何標記或提示放入編輯器中。您也可以使用空白常數儲存格來進行配置,例如空白行和縮排。計畫編輯器中的分隔符號(例如 [/[>)也是編輯器中為編輯器定義的常數。

圖 16:計畫行中名稱的屬性。

我們使用屬性儲存格顯示計畫名稱。屬性可以是編輯器定義概念上的任何屬性。在此,我顯示我在檢查器上編輯屬性欄位,並有一個快顯視窗顯示計畫概念上的所有屬性 - 在此情況下只有一個。

編輯器中的空白行是簡單的常數儲存格。值和事件行涉及子編輯器。我將略過值列並深入探討事件。

事件列是垂直儲存格集合中的儲存格,本身是具有兩個子儲存格的水平儲存格集合:空白常數儲存格和標記為 (> 的 ref 節點清單儲存格。

圖 17:事件的連結儲存格。

連結節點的檢查器比我們迄今所見的其他節點複雜許多,但我們在此感興趣的是兩則資訊。正如您從名稱中猜測的那樣,ref 節點清單儲存格會根據遵循架構中的連結來列出元素。編輯器會告知要遵循哪個連結,以及清單應垂直建立。在 圖 17 中,我顯示編輯器窗格本身中用於選擇連結(這次實際上有一個選項)的快顯視窗。我也可以在檢查器中執行此操作。編輯器窗格會在 % 分隔符號內顯示連結名稱。

這個小範例在您定義編輯器時提出了一個有趣的問題:您應該使用單獨的檢查器進行編輯,還是直接在編輯器窗格中進行編輯?將內容保留在編輯器窗格之外,讓您可以在編輯器窗格中取得整體結構,並更清楚地看到編輯器定義與編輯器最終使用方式之間的關係。但是,如果您將所有內容都放入檢查器中,您會不斷地深入探究,以查看單元格是什麼。這是簡潔標記(例如 [/ 和 [>)的理由之一。您可以按一下標記,在檢查器中查看它們是什麼,但當您習慣了那個特定的編輯器時,您會習慣直接閱讀編輯器窗格。當您習慣後,簡潔會有所幫助,因為它讓您的眼睛可以在較小的空間中看到更多內容。

您還可以想像針對不同目的有多個編輯器,有些適合使用者的經驗,有些則僅適合使用者的偏好。例如,刻意編輯器通常允許您根據您的偏好,在不同的投影之間快速切換。在編輯像這樣的巢狀表格時,您可以選擇在巢狀表格(稱為 boxy)、Lisp 類似表示法(lispy)或具有屬性的樹狀檢視(沒有可愛的名稱)之間切換。若要編輯條件式邏輯,則有一個類似的 C 程式語言檢視,或表格表示法。在投影之間快速切換很有用,因為您通常可以在不同的投影中看到問題的不同面向,因此您通常可以從簡單投影之間的簡單變更中了解更多資訊。

但讓我們回到範例。我們已經看到計畫編輯器有一個垂直列出事件的單元格。我們如何編輯那些事件?在這個時候,我們切換到事件編輯器。我們的最終工具會將這些事件編輯器嵌入在計畫編輯器中。

在我們檢視編輯器之前,讓我們複習一下事件的架構。

圖 6:事件架構

以下是事件編輯器的定義,僅使用編輯平面中的內容。

圖 19:事件編輯器的編輯器窗格定義。

如果你的眼睛已經習慣簡潔的符號,你應該可以在不使用檢查器的情況下理解大部分內容。基本上我們有一個垂直儲存格集合,包含兩個元素。兩個元素中的底部是一個 ref 節點清單儲存格,用於列出會使用該概念所定義的編輯器的張貼規則時間屬性。然而,頂部的儲存格顯示了一些我們尚未看過的東西。

頂部的儲存格是一個水平儲存格集合。它有兩個子儲存格。左邊的子儲存格是一個常數儲存格,包含單字「事件」,這沒有什麼新意。新的元素是第二個儲存格,它是一個 ref 節點儲存格。Ref 節點儲存格類似於 ref 節點清單儲存格,但適用於所引用的連結為單值的情況,就像這裡的事件類型一樣。

Ref 節點儲存格本身有兩個部分。第一個部分指出要遵循哪個連結,在本例中為 type。第二個部分指出要顯示目標的哪個屬性。這是一個可選部分,如果我們省略它,事件類型會使用其常規編輯器來呈現。這裡我們指出,我們不想這麼做,我們只想呈現一個單一屬性:類型的名稱。

現在讓我們看看張貼規則的編輯器定義。在範例計畫中(圖 1),我們看到編輯器顯示規則的生效日期,後面接著規則本身的詳細資料。以下是編輯器定義

圖 20:張貼規則時間屬性的編輯器

這次,根儲存格是一個水平儲存格集合,包含三個子儲存格:一個日期的 ref 節點儲存格、一個冒號的常數儲存格,以及另一個張貼規則本身的 ref 節點儲存格。日期和張貼規則都使用自己的編輯器來呈現。

我要展示的最後一個編輯器是張貼規則編輯器。

圖 21:張貼規則的編輯器

希望到現在為止,這已經很熟悉了。根是一個垂直儲存格集合,包含兩個水平儲存格集合作為子儲存格。頂部的儲存格包含常數「金額:」和表達式的 ref 節點。表達式由表達式的編輯器呈現,它是公式語言的一部分。底部的儲存格包含常數「帳戶:」,後面接著帳戶的 ref 節點,顯示帳戶的名稱屬性。

以文字描述此編輯器很奇怪,在某個時間點,使用編輯器的螢幕錄製可能會更容易理解。編輯器編輯器有點難以習慣。這部分是因為我不習慣定義編輯器,部分是因為需要更多工作才能讓編輯器編輯器可用。這是新的領域,因此 JetBrains 仍在學習這種東西應該如何運作。

這項工作中重要的部分是,您需要很大的靈活性來定義編輯器,以便它們像最終計畫編輯器一樣乾淨。為了提供這種靈活性,您最終會得到一個複雜的編輯器編輯器。儘管可能有很多方法可以讓它們更可用,但我懷疑定義一個對語言有用的編輯器仍然需要一些努力。然而,由於編輯器與 DSL 的其他元素緊密整合,因此嘗試並變更編輯器定義以探索最佳編輯器相對容易。

主編輯視窗和檢查器之間的交互作用揭示了關於更複雜 DSL(例如編輯器語言)的編輯器的另一個重點。與其嘗試透過單一投影取得所有編輯,通常最好使用顯示不同事物的多個投影。在這裡,我們在主編輯器窗格中看到編輯器的整體結構,以及檢查器中的許多詳細資訊。在設計編輯器時,您可以在不同的窗格之間移動不同的元素。

在這種情況下,我可以看到顯示儲存格層級的第三個窗格會提供一個有用的第三個投影,它會補充檢查器和所見即所得的主編輯器窗格。

定義產生器

此三人組的最後一部分是撰寫產生器。在這種情況下,我們將產生一個 Java 類別,它將使用我們目前擁有的架構建立適當的物件。此計畫建構器類別將為我們使用 DSL 定義的每個計畫建立服務合約類別的執行個體。

我們將產生的程式碼看起來會與我們稍早看到的 Java 等效程式碼有點不同。這是因為我們將處理計算公式的方式。在純 Java 版本中,我使用參數化但有限的公式類別來設定公式。在此版本中,公式由公式語言提供。

以下是產生器定義的編輯窗格投影

圖 22:產生定義。

以下是它產生的程式碼:(我加入了一些換行符號,以協助為網頁格式化它。)

package postingrules;

/*Generated by MPS*/


import postingrules.AgreementRegistry;
import postingrules.ServiceAgreement;
import postingrules.EventType;
import postingrules.AccountType;
import jetbrains.mps.formulaLanguage.api.MultiplyOperation;
import jetbrains.mps.formulaLanguage.api.DoubleConstant;
import jetbrains.mps.formulaLanguage.api.IfFunction;
import formulaAdapter.*;
import mf.*;

public class AgreementRegistryBuilder {
  public void setUp(AgreementRegistry registry) {
    registry.register("regular", this.setUpRegular());
    registry.register("lowPay", this.setUpLowPay());
  }
  public ServiceAgreement setUpRegular() {
    ServiceAgreement result = new ServiceAgreement();
    result.registerValue("BASE_RATE");
    result.setValue("BASE_RATE", 10.0, MfDate.PAST);
    result.setValue("BASE_RATE", 12.0, new MfDate(1999, 12, 1));
    result.addPostingRule(
      EventType.USAGE, 
      new PostingRule_Formula(AccountType.BASE_USAGE, true, 
        new MoneyAdapter(new MultiplyOperation(
          new ValueDouble("BASE_RATE"), new UsageDouble()),
          Currency.USD)), 
      new MfDate(1999, 10, 1));
    result.addPostingRule(
      EventType.SERVICE_CALL, 
      new PostingRule_Formula(AccountType.SERVICE, true, 
        new MoneyAddOperation(
          new MoneyMultiplyOperation(new FeeMoney(), new DoubleConstant(0.5)), 
          new MoneyConstant(10.0, Currency.USD))), 
      new MfDate(1999, 10, 1));
    result.addPostingRule(
      EventType.SERVICE_CALL, 
      new PostingRule_Formula(AccountType.SERVICE, true, 
        new MoneyAddOperation(
          new MoneyMultiplyOperation(new FeeMoney(), new DoubleConstant(0.5)), 
          new MoneyConstant(15.0, Currency.USD))), 
      new MfDate(1999, 12, 1));
    result.addPostingRule(
      EventType.TAX, 
      new PostingRule_Formula(AccountType.TAX, false, 
        new MoneyMultiplyOperation(new FeeMoney(), new
        DoubleConstant(0.055))), 
      new MfDate(1999, 10, 1));
    return result;
  }
  public ServiceAgreement setUpLowPay() {
    ServiceAgreement result = new ServiceAgreement();
    result.registerValue("BASE_RATE");
    result.registerValue("REDUCED_RATE");
    result.registerValue("CAP");
    result.setValue("BASE_RATE", 10.0, MfDate.PAST);
    result.setValue("REDUCED_RATE", 5.0, MfDate.PAST);
    result.setValue("CAP", new Quantity(50.0, Unit.KWH), MfDate.PAST);
    result.setValue("CAP", new Quantity(60.0, Unit.KWH), new MfDate(1999, 12, 1));
    result.addPostingRule(
      EventType.USAGE, 
      new PostingRule_Formula(AccountType.BASE_USAGE, true, 
        new IfFunction<Money>(
          new QuantityGreaterThenOperation(new UsageQuantity(), new ValueQuantity("CAP")), 
          new MoneyAdapter(
            new MultiplyOperation(new ValueDouble("BASE_RATE"), new UsageDouble()), 
            Currency.USD), 
          new MoneyAdapter(
            new MultiplyOperation(new ValueDouble("REDUCED_RATE"), new
            UsageDouble()), 
            Currency.USD))), 
      new MfDate(1999, 10, 1));
    result.addPostingRule(
      EventType.SERVICE_CALL, 
      new PostingRule_Formula(AccountType.SERVICE, true, 
        new MoneyConstant(10.0, Currency.USD)), 
      new MfDate(1999, 10, 1));
    result.addPostingRule(
      EventType.TAX, 
      new PostingRule_Formula(AccountType.TAX, false, 
        new MoneyMultiplyOperation(new FeeMoney(), new
        DoubleConstant(0.055))), 
      new MfDate(1999, 10, 1));
    return result;
  }
}

像往常一樣,我會挑選一些產生部分來逐步說明,而不會深入探討所有部分。特別是,從公式產生的程式碼是一個相當醜陋的解釋器公式。這需要清理,我們希望在不久的將來完成這項工作。

與任何範本語言一樣,MPS 的產生器語言允許您以範本形式撰寫類別,並包含參數參考。語言工作台的一項重大差異在於,您使用投影編輯器來定義範本。因此,我們可以為 Java 類別產生建立一個投影編輯器,它瞭解 Java 語法,並使用此資訊來協助您進行範本產生。在此,您會看到產生器編輯器已針對 Java 程式中看到的各種元素類型提供標記。此範例僅有方法,因此其他部分未被使用。

MPS 的產生器語言使用兩種參數參考:屬性巨集(標記為 $)和節點巨集($$)。屬性巨集會詢問抽象表示,並傳回要插入到範本輸出的字串。節點巨集會詢問抽象表示,並傳回更多節點以進行更多處理。通常,您會使用節點巨集來處理其他範本系統中迴圈的等效項。

這兩種巨集類型都是由支援 Java 類別中的 Java 方法實作的。隨著時間推移,MPS 團隊希望用一個專門設計用於查詢抽象語法以進行產生之 DSL 來取代 Java,但目前他們使用 Java 程式碼。

屬性巨集以 $[_registryBuilder_] 等參考顯示。選取 $ 可讓您在檢查器中看到巨集呼叫哪個 Java 方法。

圖 23:連結到 Java 屬性巨集

MPS 與 JetBrains 的 IntelliJ Java IDE 整合,允許我按下傳統的 IntelliJ <CTRL>-B,並轉到 Java 中的巨集定義

  public static String propertyMacro_RegistryBuilder_ClassName(SemanticNode sourceNode,
     SemanticNode templateNode, PropertyDeclaration property, 
     ITemplateGenerator generator) 
  {
    return NameUtil.capitalize(generator.getSourceModel().getName()) +
      "RegistryBuilder";
  }

如您所見,這是一個相當簡單的方法。它所做的基本上就是將我們正在處理的模型名稱與「RegistryBuilder」串接,以綜合類別名稱。

這種類型的事項允許您綜合各種字串以插入到產生的程式碼中。當您在方法中時,您可以存取抽象表示的各個部分:合約 DSL 和產生器 DSL。

  • sourceNode 是原始語言中的目前節點 - 在此情況下為合約語言。
  • templateNode 是產生器語言中的當前節點,在本例中是產生器定義中來自建構函式的當前節點
  • property 是我們套用巨集的當前屬性
  • property declaration 是此屬性的宣告(來自架構)。
  • generator 是當前的產生器執行個體 - 這會連結到當前的專案和模型。

您可以在編輯器投影中看到,此參數參考在編輯器投影中有一個名稱:_registryBuilder_。這是一個標籤,允許在編輯器中有多個參考。您可以在稍後的範本中看到此範例。每個合約都是用一個獨立的方法建立的(setUpRegular()setUpLowPay())。這些需要從整體設定方法中呼叫。因此,這些方法的名稱必須從方法定義和呼叫中參考。標籤 _setUp_plan_ 允許我們這樣做。在 圖 22 中,您可以在 setUp 方法的重複列中和在為每個方法產生的範本中的方法名稱中看到標籤。的確,由於範本編輯器是一個投影編輯器,我們可以在需要時取得彈出式選單來協助我們選擇這些標籤。由於編輯器知道我們正在為 Java 程式建立範本,它可以使用這些資訊來協助我們在投影中編輯。

我們可以看到的第二種巨集是節點巨集。節點巨集在編輯器中顯示為 $$[更多範本程式碼]。括號中所包含的範本程式碼會套用至巨集中傳回的每個節點。以下是我們合約建立方法的畫面。

圖 24:連結至節點巨集

這會連結至下列 Java 程式碼。

  public static List<SemanticNode> templateSourceQuery_Plans(SemanticNode parentSourceNode,
     ITemplateGenerator generator) 
  {
    List<SemanticNode> list = new LinkedList<SemanticNode>();
    List<SemanticNode> roots = generator.getSourceModel().getRoots();
    for (SemanticNode node : roots) {
      if (node instanceof Plan) {
        list.add(node);
      }
    }
    return list;
  }

您會看到,當屬性巨集傳回字串時,節點查詢會傳回語意節點的清單 - 在本例中,它會瀏覽抽象表示的根節點,並傳回那裏的所有計畫節點。然後,產生器會為每個計畫產生封閉的已定義程式碼。(以這種方式,它的作用很像 VTL 中的 迴圈指令)。

當您在節點巨集內部時,封閉的範本會針對巨集傳回的每個節點套用一次,將 sourceNode 參數設定為該節點。因此,當我們稍後命名方法時,可以使用下列 Java 片段。

  public static String propertyMacro_Plan_SetUpMethod_Name(SemanticNode sourceNode, 
      SemanticNode templateNode, PropertyDeclaration property, 
      ITemplateGenerator generator) 
  {
    Plan plan = (Plan) sourceNode;
    return "setUp" + plan.getName();
  }

由於來源節點是計畫節點,而計畫的架構具有字串名稱,因此我們可以使用名稱來產生方法名稱。

範本的其餘部分基本上以相同的方式運作。您可以從目前的來源節點取得屬性,或使用節點巨集來取得另一個節點進行作業。

在 MPS 中定義範本與傳統的 基於範本的方法 非常類似。我們再次擁有要查詢的抽象表示,將結果插入到產生的程式碼中。從這個範例中可見的主要差異,在於我們能夠為不同類型的範本輸出建置投影編輯器,在本例中為 Java 類別。

總結

我希望這個範例讓您感受到使用語言工作台的感覺,即使它仍然有點像胚胎。在許多方面,這個範例大多缺乏傳統文字 DSL 般的尊重。正如我在 語言工作台 中所建議的,我認為真正有趣的 DSL 實際上會相當不同。但這項工作的性質之一,就是我們還無法真正看到它們會是什麼樣子。


重大修訂

2005 年 6 月 12 日:首次出版。