octahedron

LemとSKKとCommon Lispでたたかうプログラマのブログ

インタラクティブシなREPLをWebページ上に実装する

 自分で言語を実装したとき、できればWebページ上でさっと試せるとカッコいいなと思いますよね。たとえばこんな感じで:

 今回ぼくは自作の言語に対して、同じようなことを実験したので、その成果を記します。

どんなものを作ったか

f:id:t-sin:20180202232900p:plain

github.com

どうやって実装したか

 ここではテキストエリアのonChangeなどの更新をさっと取得・描画できるVue.jsを用いたリアクティブなスタイルで実装していきます。

REPLのデータモデル

 まず、REPLに必要なものを考えてみましょう。REPLは三つの要素から出きていると考えました。それすなわち

  1. 読み込み行
  2. 出力行
  3. 過去の読み込み行

の三つです。

 読み込み行は、プロンプトを表示し、ユーザの入力を促す行です。
 出力行は、ユーザの入力を評価した結果を表示する行です。
 過去の読み込み行は、プロンプトと、過去にユーザが入力した文字列から成ります。これは出力行と考えてもよいのですが、なんとなく分けています。

 各行をJavaScriptのオブジェクトで表現し、配列で保持しましょう。すなわち、こうです:

[
  { type: 'old-prompt', msg: '$ ', input: 'aaaaa'},
  { type: 'output', msg: 'aaaaa'},
  { type: 'old-prompt', msg: '$ ', input: 'bbbbb'},
  { type: 'output', msg: 'bbbbb'},
  { type: 'prompt', msg: '$ ' },
]

REPLのView

 そして、あとはそれを描画するViewの部分です。Vue.jsの描画対象を#appとすると、その要素の中に、データモデルの各typeに対応する要素を書き、v-forで配列全体を描画する、という流れになります。

<div id="app">
  <!-- ここで、各行を描画-->
  <div v-for="line in lines">

    <!-- 出力行 -->
    <div v-if="line.type == 'output'">{{ line.msg }}</div>

    <!-- 読み込み行 -->
    <!-- 入力中の内容はv-modelで逐次取得する -->
    <div v-if="line.type == 'prompt'">
      <div>{{ line.msg }}</div>
      <input type="text"
             v-model="readline"
             v-on:keydown.enter="rep(line.id)">
    </div>

    <!-- 過去の読み込み行 -->
    <div v-if="line.type == 'old-prompt'">
      <div >{{ line.msg }}</div>
      <span>{{ line.input }}</span>
    </div>
  </div>
</div>

REPLのcontroller

 表示の制御は、REPLのinputでエンターキーが押されたときに駆動するようにします。REPLなので。

 すなわち、JavaScriptのコード全体としてはこんな感じになります。

<script>
  const app = new Vue({
    el: '#app',
    data: {
      readline: "",
      lineCount: 0,
      lines: [],
    },
    methods: {
      // 現在の読み込み行の内容でreadとevalをし、
      // 過去の読み込み行に更新し、結果と次の読み込み行を追加する
      rep: function (id) {
        this.lines[id].input = this.readline.toString()
        this.lines[id].type = 'old-prompt'
        // これらはもちろん、あなたの言語のreadとevalですよ!
        let result = eval(read(this.lines[id].input))
        this.print(result)
        this.prompt()
      },
      // 読み込み行を追加する
      prompt: function () {
        let prompt = getCurrentPackageName() + '> '
        this.readline = ''
        this.lines.push(
          { id: ++this.lineCount, type: 'prompt', msg: prompt })
      },
      // 出力行を追加する
      print: function (str) {
        this.lines.push(
          { id: ++this.lineCount, type: 'output', msg: str })
      }
    },
    created: function () {
      this.print('welcome to my cool REPL.')
      this.prompt()
    }
  }
</script>

 あとはCSSでカッコいい見た目や色を設定してあげるだけである。

おわりに

 これにより、自作言語ができたときあなたはすぐにWeb REPLを追加することができるようになりました。

 まさかLisp以外の記事が増えるとは…。