JetBrains MPS は JetBrains が開発した言語ワークベンチです。

MPS は公式の Document が充実しており、Getting startedMPS User's Guide などのこれからはじめる人向けの Document も多くあります。これらの Document を読めば MPS について理解することができますが、Document が膨大であるが故にはじめる前から何をすればいいのか悩んでしまう人も多いと思います。これはそのような人向けの「とりあえず使ってみる」というところに重点を置いた Codelab です。

この Codelab では、MPS の使い方がなんとなく分かるレベルを目指します。

難易度としては公式の Tutorial としてある MPS Calculator Language Tutorial よりも簡単なくらいだと思います。
完了までにかかる時間は 1 時間程度です。

具体的には以下に触れていきます。

以下については触れません。

各役割についてはやっているうちに分かってくると思いますが、気になる場合は公式の Document を参照してください。

はじめる前に作るものも軽く紹介しておきます。作るのは URL builder です。

url https

example.com

pathSegment api
pathSegment v2

value = a
value = b

このように書くと、

https://example.com/api/v2?value=a&value=b

のような URL を build し出力する Java code を生成するために structure の定義、editor の設計、generator の作成を行います。

完成したものは以下の Repository にありますので、困ったらこちらも参照してみてください。

それでは始めていきましょう。

Project の作成

まずは Project を作成しましょう。

‘Create New Project' から ‘Language project' を選択します。Project name, Language name は任意の値を入れてください。
‘Create Runtime Solution' と ‘Create Sandbox Solution' に check を入れて ‘OK' を押します。

依存関係の整理

次に依存関係を整理します。

まず runtime の依存関係を見ていきましょう。runtime (四角の 'S') を右クリックし、'Module Properties' を選択します。
‘Solution Properties' という画面が表示されるので、Dependencies に jetbrains.mps.baseLanguage を追加します (JDK でも大丈夫です)。
jetbrains.mps.baseLanguage は MPS において Java に相当します。すでに追加されているかもしれません。

続いて、その中の runtime (フォルダの 'M') を右クリックし、'Model Properties' を選択します (先ほどの ‘Module Properties' ではありません)。
Used Languages に jetbrains.mps.baseLanguage を追加します。

Dependencies に java.net@java_stubjava.io@java_stub を追加します。これは、後ほど java.net.URI を使うからです。

次に Language (四角の ‘L') を右クリックして ‘Module Properties' を選択し、'Language Properties' を表示します。
Dependencies に jetbrains.mps.execution.util を追加します。これは後ほど IMainClass を実装するのに必要です。

Generator にも必要です。
generator (四角の ‘G') を右クリックし、'Module Properties' を選択します。
‘Generator Properties' の Dependencies に jetbrains.mps.baseLanguage を追加します。

最後に main@generator です。
generator では後ほど runtime を参照するため、runtime を Dependencies に追加します。

この先困ったときは一度依存関係を見直してみてください。

これで準備は完了です。次からは structure 及び editor を設計しましょう。

まずは Query 部分を設計しましょう。

structure 部分を右クリックし、'Concept' を選択します。名前は QueryConcept としましょう。

INamedConcept を実装してください。これは中身を見ると分かりますが property として name : string を定義します。
Query は value = a のように namevalue が必要になるので、value : stringproperties: に定義しましょう。

次は editor に移ります。editor 部分を右クリックし、'Concept Editor' を選択します。
concept 名は先ほどの QueryConcept を入れてください。

続いて、cell layout を定義します。
[> と入力すると自動的に Horizontal Collection が入力されます。
入力した後、下部に ‘name Property' というボタンが表示されるはずです、これをクリックしましょう。{name} が入力されたら、その状態で Enter を押します。

今回は value = a のような入力をさせたいので、間に = が必要です。= を入力しましょう。入力して Enter を押すと ‘value Property' というボタンが表示されますので、これをクリックします。

これで QueryConcept は終わりです。続いて Path segment 部分を設計しましょう。

それでは Path segment の設計を始めます。
structure 部分を右クリックし、'Concept' を選択します。名前を PathSegmentConcept とします。

PathSegmentConcept では INamedConcept は実装しません。意味のある名前を別でつけましょう。
properties:pathSegment : string を定義します。

次に editor に移ります。editor 部分を右クリックし、'Concept Editor' を選択します。
concept 名は PathSegmentConcept です。

ここでは pathSegment api のような形での入力にしたいと考えています。
なので、cell layout としては Horizontal Collection を入力し、pathSegment{pathSegment} というように書いていきます。

これで Path segment の設計は以上です。次は Query と Path segment を複数持つような構造を定義しましょう。

ここでは、先ほど作った Path segment と Query を複数持つような structure の定義及び editor の設計をします。

まずは Path segment から設計します。structure 部分を右クリックし、'Concept' を選択します。
名前は PathSegmentContainerConcept としましょう。

この Concept では子要素として PathSegmentConcept を複数持ちますので、children:pathSegments : PathSegmentConcept[1..n] というように書きます。1..n とすることで、最低一つ以上 Path segment が入力されていないとエラーが出るようにできます。

続いて editor を作成します。
cell layout は先ほどまで見てきたものとは少し異なりますが、補完に頼ればほぼ困ることはありません。
Horizontal Collection を入力すると、'pathSegments Link' というボタンが表示されるはずですので、これをクリックします。
pathSegments:というのはただの文字列で今回は要らないので削ります。残しておいても構いません。

次に Query を見ていきます。
といっても、作りは PathSegmentContainerConcept と全く同じです。
children:queries : QueryConcept[0..n] とします。

editor は全く同じなので良き感じに作ってください。

以上で Path segment と Query 部分の設計が終わりました。次はいよいよ structure / editor の最後、Url の設計に入ります。

ここでは、大本となる Url を設計します。

structure 部分を右クリックし、'Concept' を選択します。名前は UrlConcept としましょう。

全体の構文は以下のような形でした。

url https

example.com

pathSegment api

value = a

これにおける構成要素としては、scheme, host, pathSegments, queries があるでしょう。
大本の structure の name はそのまま後に説明する sandbox の名前として扱われますので、host を name として使いましょう。
UrlConceptINamedConcept を実装し、加えて properties に scheme : string を定義します。

children: には pathSegmentContainer : PathSegmentContainerConcept[1] を定義します。これによって先ほど定義したとおり、Path segment を複数持つことができます。
同じように queryContainer も定義しましょう。

UrlConcept ではもう少しやることが残っています。
まず、UrlConcept は大本となる structure なので instance can be roottrue にしてください。
加えて、IMainClass を実装します。これがないと sandbox で作成した Node が実行可能になりません。

できたら UrlConcept の editor を作成します。

Vertical Collection を作成し、さらに Horizontal Collection を作成します。ラベルとして url を入力し、scheme property をセットします。補完が出てこない場合は { を入力してから Enter を押すと出てきます。
続いて、一つ <constant> を追加して name property をセットします。<constant> を入れたあと ‘New Cell' をクリックするとスムーズです。

また一つ <constant> を追加し、'pathSegmentContainer Link' をクリックします。pathSegmentContainer: というラベルは今回不要ですが、つけておいても良いでしょう。
その後 <constant> を追加して今度は ‘queryContainer Link' をクリックします。

これで Url の設計は終わりです。

先ほどまで設計していた URL Builder の構文を一度確認してみましょう。

まずは ‘Build' -> ‘Make Project' (または Rebuild) を選択し Make した後、sandbox を右クリックし ‘N' のマークがついた ‘UrlConcept' をクリックします。

すると、未入力状態のエディタが開くはずです。
設計したとおり、url の後に scheme, pathSegment の後に pathSegment ... が入力できます。

ただ、これでは実際に Java code が生成される部分がなく、ただ構文を設計したに過ぎません。次からはいよいよその生成部分に移ります。

まず Main class の作成しましょう。

main@generator を右クリックし、'New' -> ‘j.mps.baseLanguage' -> ‘class' を選択します。すると Java なのかなんなのかよくわからない class が生成されます。
input には Concept を設定します。今回の場合は Root Concept である UrlConcept を設定します。

class 名は実のところなんでも良いのですが、Main にしておきましょう。
その次に #main を用意しますが、これは class 内で ‘psvm' と入力すると勝手に作ってくれます。便利ですね。
まだ生成部分で用意すべきことが色々ありますが、ここで一度 "Hello"println して動作を見てみましょう。

さて、これで実行すると print されるかというとそういうわけでもありません。もう一つ設定が必要です。

main@generator にある ‘main' を見てください。これは generator の rule を設定する mapping configuration です。
先ほど作った Main を紐付けます。

root mapping rules: の空欄にフォーカスを当て、Enter を押すと rule の雛形が作られます。applicable concept は UrlConept、template は Main です。
これで Make して sandbox に先ほど作った Node を右クリックしてみてください。'Run Node null' (null の場合はなにか入力していれば変わっているはずです) と表示されるはずです。
実行してみましょう。

Hello と表示されましたか?

どのような Java code が生成されたか見てみましょう。Node を右クリックして ‘Preview Generated Text' を選択します。
すると、先ほど書いた Main class に package が付加された実行可能な Java code が表示されます。

次は URL の生成部分を書いていきます。

それでは根幹となる URL Builder を実装しましょう。
ここは普通の Java です。

runtime を右クリックし、'New' -> ‘class' を選択します。
先ほどとは違って、冒頭に何もついていない空の class が生成されます。ここに URL を build するための builder を作りましょう。

使い方としては以下のようなものを考えています。

UrlBuilder builder = new UrlBuilder("https", "example.com");

builder.appendPathSegment("path")

builder.appendQuery("key", "value")

// https://example.com/path?key=value
String url = builder.build();

ここまで書けば作り方はなんとなく思い浮かぶと思います。私の実装例を示しますが、実装はこの通りにする必要はありません。

なお、import 文を書く必要はありません。
「Query の設計」で触れたように MPS 内部では XML で表現されており、そもそも Dependencies にない依存解決できない Code は書くことができません (書いても無視されます)。

public class UrlBuilder {
  private final string scheme;
  private final string host;

  private string path = "";
  private string query = "";

  public UrlBuilder(string scheme, string host) {
    this.scheme = scheme;
    this.host = host;
  }

  public void appendPathSegment(string pathSegment) {
    if (path.endsWith("/") && pathSegment.startsWith("/")) {
      path += pathSegment.substring(1);
    } else if (path.endsWith("/") || pathSegment.startsWith("/")) {
      path += pathSegment;
    } else {
      path += "/" + pathSegment;
    }
  }

  public void appendQuery(string key, string value) {
    string encodedValue;
    try {
      encodedValue = URLEncoder.encode(value, "UTF-8");
    } catch (UnsupportedEncodingException e) {
      e.printStackTrace();
      return;
    }
    if (query.isEmpty) {
      query = key + "=" + encodedValue;
    } else {
      query += "&" + key + "=" + encodedValue;
    }
  }

  public string build() {
    URI uri = null;
    try {
      uri = new URI(scheme, null, host, -1, path, query, null);
    } catch (URISyntaxException e) {
      e.printStackTrace();
    }
    return uri == null ? "" : uri.toString();
  }
}

以上が実装例です。もっと簡単にするのも、もっと安全にするのもお任せします。

一つ注意点として、ここでも基本的に自分で書くことは避けましょう。
Constructor や Method の呼び出し部分は特に、少し打ってから補完を出して書くことをおすすめします。自分で全部書いても認識されない場合があります。

次はこれを使用する generator の template を作成しましょう。

それではここからは template を作成します。

はじめのうちは何をしているか分からないかもしれませんが、この Chapter を乗り越えればどういうものかがなんとなく理解できると思います。

まずは appendPathSegments です。
Main class に #appendPathSegments(UrlBuilder builder) を定義し、それを main から呼び出します。
#main で直接 UrlBuilder#appendPathSegment を呼び出しても良いのですが、今回は template の使い方を見るために多少冗長な書き方をしています。

それでは、main@generator を右クリックし、'template declaration' を選択します。
template 名は include_appendPathSegments としましょう。input には対応する Concept を指定します。今回の場合は PathSegmentContainerConcept です。

Content node には実際の template を記述していきます。
Intentions (Alt + Enter) を表示し、'Replace with instance of ClassConcept concept' を選択します。すると、空の Class が補完されるはずです。

実際に使用するのはこの class ではなく、中の method です。そのためこの class 名はなんでも構いません。ここでは _class_ とします。

#appendPathSegments(UrlBuilder builder) を定義します。
まずは

private static void appendPathSegments(UrlBuilder builder) {

}

ここまでを普通に記述します。実は書く順番がそれなりに大事なのでこのとおりに書いていって下さい。

private から } まで全体を選択し Intentions を表示して、'Create Template Fragment' を選択します。
これが後ほど Main class に差し込まれる部分になります。

次にその中に

builder.appendPathSegment();

と記述します。
引数が足りないので赤くなっていると思いますが、そのままで大丈夫です。

書けたらこの builder.appendPathSegment(); 全面を選択した状態で Intentions を表示します。
‘Add LOOP macro over node.pathSegments' というのがあると思うので、これを選択します。builder...; の部分が $LOOP[...] で囲われたと思います。

この LOOP Macro は node.pathSegments の要素分だけ内部の文を生成します。

次は引数をセットします。引数は string ですので、appendPathSegment()("") というように空文字を入力し、その状態で Intentions を表示します。
‘Add Property Macro: node.pathSegment (property)' を選択します。"$[]" というような表示になったはずです。
この中は何を書いても値は変わらないので、わかりやすい名前をつけましょう。ここでは PathSegment とします。
ここでの Property Macro は LOOP Macro より渡された PathSegmentConcept から、 property として定義した pathSegment を string として受け取り、さらに builder#appendPathSegment に渡しています。

さて、今回も Main でやったのと同じ mapping が必要になります。main を開いてください。

この include_appendPathSegments は reduction rules として定義します。
recduction rules: の部分で Enter を押し、Concept を PathSegmentContainerConcept、Consequence を `include_appendpathSegments とします。

これらを実際に Main class で生成する必要もあります。Main class を開いてください。

生成されるのは static method ですから、class の直下に生成してほしいです。
main の下辺りを空けて Intentions を開きます。'Apply COPY_SRC for node.pathSegmentContainer' を選択してください。

この COPY_SRC Macro は Node を Inspector の mapped node に書いた Node に置き換えます。今回の場合は PathSegmentContainerConcept です。
そして先ほどの reduction rule はこれらの Node を変換します。

では、実際に生成された Java code を見てみましょう。
Make (または Rebuild) し、sandbox で ‘Preview Generated Code' を選択します。
何か Path segment を書いている場合は、以下のようになっているはずです。

private static void appendPathSegments(UrlBuilder builder) {
    builder.appendPathSegment("api");
    builder.appendPathSegment("v2");
}

appendPathSegments の呼び出しは後で作りますので、この Chapter は以上です。
次はこれとは少し異なる方法で Query の構築を行いましょう。

次は appendQuery です。
appendQuery は先ほどとは異なり、各 builder#appendQuery の呼び出しに対してそれぞれユニークな名前を持つ method を割り当てます。

同じように template を作成します。template 名は include_appendQuery とし、input は QueryConcept とします。
QueryContainerConcept ではないことに注意してください。

空の class を作成し、その中に #appendQuery を定義します。

private static void appendQuery(UrlBuilder builder) {
}

続いてこれらを選択し、Template Fragment を作成します。

作成できたら builder#appendQuery を呼び出しましょう。

... {
    builder.appendQuery();
}

続いて Property Macro を追加します。第 1 引数に name property、第 2 引数に value property を割り当ててください。名前はなんでも良いですが、Key と Value にしましょう。

次に appendQuery の method 名をユニークにします。appendQuery の部分にフォーカスを当て、Intentions を表示して ‘Add Property Macro' を選択します。

$[ の部分にフォーカスを当て、Inspector を確認してください。value の中身がなにも入っていないはずです。

この <no statements>genContext.unique name from (templateValue) in context (<no node>) というように書きます。
genContext を入力した後に ‘un' くらいまで入力して Enter を押すと勝手に入力されます。

これでこの templateValue に従ってユニークな名前が自動的に生成されるようになります。

さて、これをまた reduction rule に追加します。先ほど include_appendPathSegments を定義した下に書きましょう。
concept は QueryConcept、consequence は include_appendQuery とします。

次に Main class を開きます。
先ほど COPY_SRC Macro を定義した下に新たに COPY_SRC Macro を定義します。
今回は対象の Node は後で記述するので空の Macro を定義します。Intentions から、'Add Node Macro' を選択し、中身に COPY_SRC と書きます。

続いて、これを LOOP Macro で囲みます。COPY_SRC Macro を選択した状態で ‘Add Node Macro' を選択肢、中身を LOOP とします。

Inspector を確認してください。iteration sequence の中身がない状態になっているので、これを node.queryContainer.queries と指定し、QueryConcept の配列を返します。

そして COPY_SRC Macro の Inspector を確認し、mapped node で node を指定します (空の状態で Enter を押すと Function は生成されます)。

ここまでできたら生成される Code を確認してみましょう。
Make し、sandbox で ‘Preview Generated Code' を選択します。何か Query を書いている場合は、以下のようになっているはずです。

private static void appendQuery_a0(UrlBuilder builder) {
    builder.appendQuery("value", "a");
}
private static void appendQuery_b0(UrlBuilder builder) {
    builder.appendQuery("value", "b");
}

それぞれユニークな名前が生成されていることが分かりますね。
次はこれらを呼び出す #appendQueries を定義します。

ここでは #appendQueries の template を作成します。
template 名を include_appendQueries とし、input は QueryContainerConcept とします。

#appendQueries#appendQuery を定義します。

private static void appendQueries(UrlBuilder builder) {
}

private static void appendQuery(UrlBuilder builder) {
}

#appendQuery は template として定義する際の method 呼び出しにおいてエラーを抑制するために定義しており、実際には無視されます。
#appendQueries の全体を選択して Template Fragment を作成します。

できたら #appendQuery を呼び出しましょう。

... {
    appendQuery(builder);
}

次にこの appendQuery(builder); を LOOP Macro で囲います。'Add LOOP Macro over node.queries' を選択します。

さて、このままの状態で rule と Main class への追加を行うとエラーになります。この状態では node.queries の要素分だけ #appendQuery を呼び出すからです。先に書いたとおり、#appendQuery は実際には生成されない method であり、前の Chapter で見た生成される method は appendQuery_a0 のような名前でした。
ということは、なんらかの方法で生成される method を呼び出す必要があります。こういう時に使えるのが label と Reference Macro です。

まず、main に移動し、mapping labels: に label を定義します。名前を appendQuery、Output concept を MethodDeclaration とします。
Input concept は <no input concept> のままで大丈夫です。

次に include_appendQuery を選択し、Template Fragment の Inspector を確認してください。mapping label:appendQuery と記述します。

include_appendQueries に戻りましょう。
#appendQuery の呼び出し部分、appendQuery の名前の部分にフォーカスを当て、Intentions を表示します。'Add Reference Macro' を選択してください。

->$ の部分にフォーカスを当てて Inspector を確認すると、referent : という部分が確認できるはずです。ここに、genContext.get output appendQuery for (node); というように記述します。
‘genContext.get' まで入力して Enter を押し、'get output by label and input' を選択すると良いでしょう。

準備は完了したので、reduction rules に追加しましょう。concept を QueryContainerCocnept、consequence を include_appendQueries として定義します。

最後におなじみの COPY_SRC Macro です。Main class に移動し、class 配下の良さげなところで Intentions を表示し、'Apply COPY_SRC for node.queryContainer' を選択してください。

また生成結果を確認してみましょう。以下のような生成結果を確認できるはずです。

private static void appendQueries(UrlBuilder builder) {
    appendQuery_a0(builder);
    appendQuery_b0(builder);
}

残りの Chapter も (設計としては) あと一つとなりました。
次は #appendPathSegmnets#appendQueries を呼び出しを定義します。

ここでは #main 内で #appendPathSegments#appendQueries を呼び出すための template を作成します。
template 名を include_main とします。input は <any node> のままで大丈夫です。

空の class を content node: に定義し、#appendPathSegments#appendQueries を定義します。

private void appendPathSegments(UrlBuilder builder) {
}

private void appendQueries(UrlBuilder builder) {
}

これらは実際には無視されます。

今回は #main で呼び出すための template ですので、method 全体ではなく、中の呼び出しだけを Template Fragment とします。そのため、class 名と同様に method 名もなんでも構いません (修飾子もなんでも良い)。

public void _method_(UrlBuilder builder) {
}

さて、独立した 2 つの文を Template Fragment とすることはできないため、{} で囲います。

public void _method_(UrlBuilder builder) {
    {
        appendPathSegments(builder);
        appendQueries(builder);
    }
}

{ にフォーカスを当てて Template Fragment を作成します。

続けて main を選択し、reduction rules に追加しましょう。concept を UrlConcept、consequence を include_main として定義します。

以上でこの Chapter は終了です。次で実際に URL を build します。

いよいよ最後です。
ここでは先ほどまで作っていた URL Builder、Template を使用して URL を出力します。

それでは Main class を選択してください。まずは #main で print している "Hello" を builder#build() の結果に置き換えましょう。

public static void main(string[] args) {
    final UrlBuilder builder = new UrlBuilder();
    System.out.println(builder.build());
}

Constructor に Scheme と Host を渡しましょう。
これはおなじみの Property Macro です。Scheme には ‘Add Property Macro: node.scheme (property)' を、Host には ‘Add Property Macro: node.name (property)' を選択します。

Path segment と Query の追加も必要になります。これは先ほど template を作成しましたのでこれを使います。

‘Add Node Macro' で COPY_SRC Macro を配置し、mapped node で node をそのまま返します。

これで生成された Code を確認してみましょう。

public static void main(String[] args) {
    final UrlBuilder builder = new UrlBuilder("https", "example.com");

    appendPathSegments(builder);
    appendQueries(builder);

    System.out.println(builder.build());
}

生成された Code は問題なさそうです。Node を右クリックして実行してみましょう。

https://example.com/api/v2?value=a&value=b

正常に出力されましたね。

以上でこの Codelab は終わりです。なんとなく MPS の使い方が分かってもらえたなら幸いです。

なお、付録として typesystem による入力規則の設定を行います。興味があれば見てみてください。

ここでは typesystem の Checking Rule を適用し、入力できる値に制限を設け、警告を出すような設定を行いましょう。

typesystem を右クリックし、'New' -> ‘Checking Rule' を選択します。
名前は check_UrlConcept とします。

今回は scheme が http の場合に https にするように警告する rule を作成しようと思います。そのため、適用する対象の Concept は UrlConcept です。

applicable for に続いて、concept = UrlConcept as urlConcept と入力します。conc くらいまで入力して Enter を押すとスムーズです。

あとは do {} の中に rule を記述していきます。

do {
    if (urlConcept.scheme.equals("http")) {
        warning "Should use https." -> urlConcept;
    }
}

これで一致していない場合に警告を表示できます。

ただこれだけだと警告が出る位置が url の部分になってしまいます。warning の部分にフォーカスを当て、Inspector を確認します。

node feature to highlight(optional) という項目がありますので、property scheme にします。これで警告の位置が scheme になります。

ついでに Intentions で https に置き換えられるような Quick fix を作成しましょう。

typesystem を右クリックし、'New' -> ‘Quick Fix' を選択します。名前は UseHttps とします。

arguments には node<UrlConcept> を渡し、中で参照できるようにします。arguments:node<UrlConcept> urlConcept と定義します。
続いて、description を記述します。Enter を押すと Function が補完されるので、中に説明を記載します。"Replace with https"; とかで良いでしょう。

最後に実行された場合の処理を記述します。
scheme を https に変更すれば良いので、以下のように書きます。

execute(node)->void {
    urlConcept.scheme = "https";
}

これで sandbox を開き、http と入力すると警告が出るようになりました。また、Intentions により https に変換することもできます。

このように、typesystem などを使用することで様々な規則や機能を付加することができます。