S式でプログラミング言語つくろ!!

プログラミング言語の構文にS式を使うということ

  1. 構文に悩まされない
    • カッコがすべてを包み込んでくれます
  2. パーサの実装が簡単
    • JSONのパーサを作ることができるならいける
    • S式言語の実装が一つもない言語なんて存在しないので、既存の実装を参考にすればいい
  3. shift/reduce conflictに悩まされない
    • 24 shift/reduce conflictとか出ない
    • 6 reduce/reduce conflictとか出て絶望しない
  4. 構文なんてただの飾りです

構文を作りたいのか、言語を作りたいのか

プログラマの3大欲求は「OSを作る」「エディタを作る」「言語を作る」と古から伝わっています。※諸説あり

プログラマを拗らせた人なら一度は言語を作りたいと思ったことでしょう。

そんな人が、今どき本やネットで「プログラミング言語 作り方」を調べた時に最初に出てくるものはパーサの作り方です。
そしてみんな電卓を作らされるのです。

ちゃうやん。

電卓作りたいわけじゃないんよ。

文字列をASTに分解してキャッキャしたいわけでもないんよ。

俺が考えた最強にイケてる機能を持った言語を作りたいんよ。


開発がマジで楽になる

NaviというS式でNot Lispな言語を作っているわけだけど、パーサにあたるread関数は一番最初のコミットで作った。

で、ずっとread関数への大きな修正をせずに開発は進んでるし、機能追加(構文追加)のコストはすこぶる低い。

これは本当にすごいことで、何かしらの構文解析アルゴリズムを使いながら言語を作ってると一つの機能を追加しようと思うとたくさんのことを考えないといけない。

新しく追加しようとした機能のための構文が、既存の構文とバッティングしたときが本当に辛い。
なんかもう機能追加自体が面倒になってくる悪循環。




構文を作ることから解放されよう

カッコに包まれよう

そして言語を作ろう




マジで一度やってみ

人生変わっから

みんなもっと安易に自分が考えた最強の言語を作ろうよ




S式のread実装はC言語ですがGaucheのread.cがシンプルにまとまっていて非常にわかりやすいです。

Let's enjoy the S-Expression!!

ニート終わりにS式でErlangライクなプログラミング言語作った

はじめに

会社を辞めてからそこそこの期間ニートでいたのですが、そろそろ社会復帰が必要になってしまいました。
貯蓄的な意味で。

だけれども何の成果もなくニートを終えるのは寂しいので、プログラミング言語を作ることにしました。

で、実装が一段落している訳ではないですが、ニートイムリミットに合わせて紹介記事を書いているところです。

プログラミング言語 Navi

Naviという名前の言語を作っています。
名前の由来は秘密です。
Githubにあります。

f:id:aharisu:20220124174122p:plain:w150

Hey!!



特徴

  • Rustで実装。
  • 構文はS式を採用。
  • 機能はだいたいErlang
  • オブジェクトの概念がある。
  • 暗黙的なFutureを持つ。

サンプルコード

だいたいこんな感じ

(let obj (spawn))

(object-switch obj)
(def-recv "Hey" "What's up?")
(return-object-switch)

(let result (send obj "Hey"))
(print result) ; => What's up?



Rustで実装

まとまった時間が取れるので勉強も兼ねて、一切触れたことがなかったRustで実装しました。

勉強期間は3日ほど。
書いたコードの量0でいきなり言語の実装に取り掛かってしまったので、最初の方はボロボロだったと思います。
そんな訳で苦労したところは多かったですが、Rustはとてもいい言語ですね。

ある程度アプリケーションとしての下地が整ってくると、rust-analyzerが提示してくれる型から自分が目的とする型へ、関数を通して変換を繰り返すと機能の追加が完了しています。
なかなかにHaskell味も感じられて書き心地が良いです。

Rustといえばの部分で、ボローチェッカとライフタイムによるエラーは今でも頭が痛くなることが多いです。
ですが基本的にはコンパイラの指摘が正しい訳で、素直に従った方が良いということで落ち着きました。
最悪unsafeで逃げられますし :-)

構文はS式を採用

いわゆるLispの構文です。

言わずもがなカッコを多用しています。

構文としてはLispのS式を採用しましたが、機能的にはLisp的要素はほとんどありません。

Lispに飢え、Lispを求めている方には満足いただけないと思います。

Not Lisp yeah.

機能はだいたいErlang

Erlangは並列・分散指向のプログラミング言語です。

多数のプロセスがメッセージのやり取りで協調しながら動作するところが非常にかわいらしい。

そんな言語を作りたかったので、NaviはErlangを参考にしている機能が多いです。

オブジェクトの概念がある

Erlangでいうところのプロセスを、Naviではオブジェクトと呼んでいます。

Erlangのプロセス同様、Naviのオブジェクトはそれぞれ完全に独立しています。
全てのオブジェクトは非同期的に動作して、メモリ領域(ヒープ・グローバル変数)もそれぞれのオブジェクト固有のものです。

独立しているオブジェクトはメッセージングによってやり取りするわけですが、
他のオブジェクト指向言語でのメソッド呼び出しのことをメッセージングと呼んでいると思ってもらえればほぼ間違いありません。

メッセージングとメソッド呼び出しとを同一視するためにNaviのsend関数は戻り値を持ちます。
ここがErlangと大きく違うところです。

; サンプルコードのこの部分。
(let result (send obj "Hey"))
(print result) ; => "What's up?"

もちろんsend関数はブロッキングなしで処理を返します。
この仕掛けは次の特徴によって成り立っています。

暗黙的なFutureを持つ

Naviで一番特徴的な機能だと思います。
ほかの言語では、FutrueとかPromissとかDelayとか呼ばれる機能です。
send関数がFuture値を返します。

Future自体は珍しくはありませんが、一般的なFutureと違いNaviではFutureに対して処理が完了したかどうかを確認する必要はありません。
値を使用するとき自動的に確認され、完了していなければ結果が取得できるまで待ちます。

(let result (send obj "Hey")) ;; ← ここはブロックなしですぐに終了
(print result) ;; ← print関数にresultが渡される前に、完了チェック。まだ完了していなければここで処理がブロックされる

この動作はすべて暗黙的に行われるためソースコード中にFutureに関する記述は必要なく、これらが非同期で動作していることを気にする必要もありません。

普通にオブジェクトを定義して、メソッド呼び出しをしながら実行するだけで、全て非同期で動作するようになります。

オブジェクトの生成と複製

; 空のオブジェクトを作成
(let obj1 (spawn))

; obj1の中に移動
(object-switch obj1)
; グローバル変数xを定義
(let x 1)
; メッセージレシーバーを定義
(def-recv {:add-x @n} (+ x n))
; obj1から最初のオブジェクトに戻る
(return-object-switch)

; obj1を複製して新しいオブジェクトを作成
(let obj2 (spawn obj1))
(print (send obj1 {:add-x 1})) ; => 2
(print (send obj2 {:add-x 1})) ; => 2

; obj2の中に移動
(object-switch obj2)
; グローバル変数xを変更
(let x 2)
; obj2から最初のオブジェクトに戻る
(return-object-switch)

(print (send obj1 {:add-x 1})) ; => 2
(print (send obj2 {:add-x 1})) ; => 3



ざっくりとその他の特徴

  • 組込み型一覧

    • 整数 1, 2, 3
    • 実数 3.14
    • 文字列 "Hello!!"
    • Bool true / false
    • シンボル a b symbol
    • キーワード :a :b :keyword
    • 関数 / クロージャ (fun (arg1 arg2) ...)
    • Object
    • リスト '(+ 1 2 3)
      • ペアではない。
      • Lispでcdrに当たる部分は必ずリストになっている。
    • 配列 [+ 1 2 3]
    • タプル {+ 1 2 3}

  • パターンマッチを持つ

    • 実のところオブジェクト間のメッセージsend/recvは単なるパターンマッチ
(match x
  (1         :the-one)
  ((1 2 3) :list)
  ({}        :empty-tuple)
  ([@one @two] :array-bind-one-two)
  (else      :other))

(def-recv {:add-one @n} (+ n 1))
(def-recv {:add-two @n} (+ n 2))
;;上二つのメッセージレシーバーは以下のmatch式と同義
(match msg
  ({:add-one @n} (+ n 1))
  ({:add-two @n} (+ n 2)))
  • GCはCompactionとCopying

  • 完了していないFuture値かどうかの判定処理が超速い

    • ポインタの最下位ビットを見るだけ

  • プリエンプティブ方式のマルチタスク

  • 値はすべてイミュータブル

おわりに

紹介としては雑ですが疲れたので終わり。

まだ開発は続きますが、久しぶりの言語実装は非常に楽しかったです。

ニート最高!!

Androidでモーダルダイアログ

今更だがAndroidではモーダルなダイアログが存在しない。
ダイアログ自体は存在するがそれはモードレスダイアログだけだ。


過去にはこちらのサイトにあるように、非UIスレッド上という制限付きだがモーダルダイアログが実現できていた。
先ほどのサイト内でも書かれているがAndroidではUIスレッドを止めることがご法度なので
非UIスレッド上という制限がついている。


それでもどうしてもUIスレッド上でモーダルダイアログを実現したいので
無茶な方法だが実現してみた。


以下がそのコード。
※後述しますがこのダイアログにも判明している制限があります。
 また、無茶をしているので予想外の影響を及ぼす可能性があります。
 使用する場合は個人の責任で、よくテストを行ってから使用してください。

private static boolean showModalDialog(Context context, String text) {
    final Handler handler = new Handler() {
        @Override
        public void dispatchMessage(Message msg) {
            if (msg.what == 1192) {
                try {
                    msg.recycle();
                } catch (IllegalStateException ex) {
                    this.removeMessages(msg.what);
                }
                //大域脱出のための例外を発生させる
                throw new BreakMainLoopException();
            }
            super.dispatchMessage(msg);
        }
    };

    final Runnable finalize = new Runnable() {
        @Override
        public void run() {
            //停止しているUIスレッドを起動させるためにメッセージを送る
            handler.sendEmptyMessage(1192);
        }
    };

    final Object[] result = new Object[1];
    result[0] = false;

    // 表示するダイアログを作成する
    final AlertDialog.Builder builder = new AlertDialog.Builder(context);
    builder.setMessage(text);

    //YESボタンを設定
    builder.setPositiveButton("YES", new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface arg0, int arg1) {
            result[0] = true;
            finalize.run();
        }
    });

    //NOボタンを設定
    builder.setNegativeButton("NO", new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface arg0, int arg1) {
            result[0] = false;
            finalize.run();
        }
    });

    //キャンセルされたときの動作を設定
    builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
        @Override
        public void onCancel(DialogInterface dialogInterface) {
            result[0] = false;
            finalize.run();
        }
    });

    //まずダイアログを表示する
    builder.show();

    //メッセージループは止めずにプログラムの実行を現時点で停止させたいので、
    //本来はAndroidのmain関数内で実行されているメッセージループメソッドを
    //ここでも実行してメッセージを処理させる。
    try {
        Looper.loop();
        //メッセージループから強制的に脱出するために専用の例外が投げられてくるためそれを捕まえる。
    } catch (BreakMainLoopException e) {
    }

    return (boolean)result[0];
}
private static final class BreakMainLoopException extends RuntimeException {}



上記のコードをトーストで囲むようにして呼び出すと、ちゃんとプログラムが停止していることがわかる。

 private void testModalDialog() {
      Toast.makeText(this, "Before", Toast.LENGTH_SHORT).show();

      boolean result = showModalDialog2(this, "modal dialog!");

      Toast.makeText(this, "After:" + result, Toast.LENGTH_SHORT).show();
  }



このモーダルダイアログの実現方法だが、先ほども書いたようにAndroidではUIスレッドを止めることはご法度だ。
これを実際の処理に近い単語で言い換えると、メインのメッセージループを止めることがご法度なのだ。


なら、「止めたい場所でメッセージループをもう一度呼び出せばいい」というのがこの方法の肝である。
あとは本来無限ループになっているメッセージループから抜け出すために、Handlerと例外を利用している。
この二つの無茶でモーダルダイアログを実現した。


私が作っているアプリでは昔からこの方法を採用していて、一応Androidのバージョン2.3の頃から8.0までは動作が確認できている。
ただし、Fragmentとの相性は未知数で私自身は試したことがない。
また必ずエラーとなる場合もあり、それは以下のようにViewクラスのonTouchEventメソッドのコンテキストで呼び出す場合である。

new View(context) {
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        showModalDialog(context, "error error error!");
        return super.onTouchEvent(event);
    }
};



この場合、間違いなくエラーになるので注意して欲しい。
もちろんクリックイベント内で今回のダイアログを呼び出すのは問題ない。
他にも私が気づいていないだけで使用できないケースが他にもあると思うので本当に注意してほしい。


最後に、前述のサイトで実現されていた
非UIスレッド用モーダルダイアログと、今回のUIスレッド用モーダルダイアログを混ぜたメソッドを紹介する。
私はずっとこれを利用していて、UIスレッドにいるのか非UIスレッドにいるのかあまり気にせずに呼び出している。

private static boolean showModalDialog(Context context, String text) {
    //UIメッセージループインスタンスを取得します
    Looper mainLooper = Looper.getMainLooper();
    //現在のスレッドとメッセージループのスレッドが同じであれば、現在はUIスレッドにいる
    final boolean isInUIThread = Thread.currentThread() == mainLooper.getThread();

    //カウントダウンシグナルはUIスレッド以外で実行されたときだけ参照される。
    final CountDownLatch signal = isInUIThread == false ?  new CountDownLatch(1) : null;

    //ハンドラはUIスレッドで実行されたときだけ参照される
    final Handler handler = isInUIThread ? new Handler() {
        @Override public void dispatchMessage(Message msg) {
            if(msg.what == 1192) {
                try {
                    msg.recycle();
                } catch (IllegalStateException ex) {
                    this.removeMessages(msg.what);
                }
                throw new BreakMainLoopException();
            }
            super.dispatchMessage(msg);
        }
    } : null;

    final Runnable finalize = new Runnable() {
        @Override
        public void run() {
            if (isInUIThread) {
                //停止しているUIスレッドを起動させるためにメッセージを送る
                handler.sendEmptyMessage(1192);
            } else {
                //停止スレッドを起動させるためにシグナルを送る
                signal.countDown();
            }
        }
    };

    //戻り値にする値(イベントリスナ内で値を操作する必要があるので配列にする)
    final Object[] result = new Object[1];
    result[0] = false;

    // 表示するダイアログを作成する
    final AlertDialog.Builder builder = new AlertDialog.Builder(context);
    builder.setMessage(text);

    //YESボタンを設定
    builder.setPositiveButton("YES", new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface arg0, int arg1) {
            result[0] = true;
            finalize.run();
        }
    });

    //NOボタンを設定
    builder.setNegativeButton("NO", new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface arg0, int arg1) {
            result[0] = false;
            finalize.run();
        }
    });

    //キャンセルされたときの動作を設定
    builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
        @Override
        public void onCancel(DialogInterface dialogInterface) {
            result[0] = false;
            finalize.run();
        }
    });

    if(isInUIThread) {
        //UIスレッドから実行された場合

        //まずダイアログを表示する
        builder.show();

        //メッセージループは止めずにプログラムの実行を現時点で停止させたいので、
        //本来はAndroidのmain関数内で実行されているメッセージループメソッドを
        //ここでも実行してメッセージを処理させる。
        try {
            Looper.loop();
            //メッセージループから強制的に脱出するために専用の例外が投げられてくるためそれを捕まえる。
        } catch(BreakMainLoopException e) { }

    } else {
        //UIスレッド以外から実行されたので、呼び出しスレッドをとめてダイアログを表示

        //ダイアログはUIスレッドで表示させないといけない
        new Handler(mainLooper).post(new Runnable() {
            @Override
            public void run() {
                builder.show();
            }
        });

        try {
            //スレッド停止
            signal.await();
            //スレッドはユーザがダイアログのボタンを押した時に再開される
        } catch (InterruptedException e) {
        }
    }

    return (boolean)result[0];
}

private static final class BreakMainLoopException extends RuntimeException {}



個人的には、なかなか無茶な実現方法で、とても気に入っている。

リーダーマクロと行カバレッジとステップ実行を作った

これはKPF#x09のネタとして作成して発表したものです台風の影響で勉強会は延期になってしまいましたが、これ以上公開を先延ばししたくないので予定通り公開します。
リポジトリこちらgithub上にあります。
今回はGaucheにリーダーマクロ、行カバレッジ計測、ステップ実行の三つを実装しました。
それぞれ説明しているためかなり長くなっています。
目次代わりにそれぞれの章へのジャンプを作っておくので、気になるところだけ見ていってください。


はじめに

今回使っているGaucheのバージョンは0.9.4です。
他のバージョンで動作する保障はありません。
また、リーダーマクロについての説明の関係上COMMON LISPのリーダーマクロについて触れていたり、その知識を前提として説明している部分があります。ですが、COMMON LISPのリーダーマクロ自体の解説は行っていません。ごめんなさい。


今回三つの機能を実装していますが、すべてGaucheレベルのスクリプトで実装しています。
ただし、行カバレッジやステップ実行のスクリプトを実行するためには、一箇所だけGauche本体のソースを修正しなければなりません。(※追記あり。現在はこのパッチを当てる必要はありません。)

この修正ではload関数から呼ばれるread関数を上書きできるようにしています。
Gaucheレベルでreadに対してset!したとしてもload関数内からは呼ばれないため、このような修正が必要になります。
上記のdiffはgit diff --no-prefixで出力したものなので、Gaucheのルートディレクトリで以下のようにパッチを当てることが出来ます。

diff -p0 < patch

ちなみにですが、diffを適用させた後に再コンパイルする必要があります。

※追記 2014年8月13日時点のGauche開発レベルの最新版では上記のパッチを当てる必要はありません。パッチの様なその場しのぎではない方法でちゃんと上書きしたread関数が呼ばれるようになりました。


Gaucheにリーダーマクロを導入する

リーダーマクロはCOMMON LISPにある機能です。
通常のマクロはevalされる前(ほとんどの場合はコンパイルタイム)に評価されますが
リーダーマクロはさらにその前、read時に評価されます。
GaucheSchemeの実装ですからリーダーマクロは存在しません。
といっても通常のマクロは存在するわけで、リーダーマクロもあっていいだろうということで実装してみました。
github上のリポジトリではsrc/ya/以下のファイルがリーダーマクロを実装するために必要なファイルです。


さて、リーダーマクロをGaucheに導入する方法を考えます。
リーダーマクロはread関数の動作と密接に結びついているのですが
GaucheのreadはC言語で実装されているのでカスタマイズする余地がありません。
なので今回はread関数そのものを再実装しています。


実装したread関数の特徴
  • 構文は全てリーダーマクロで実装
  • ' (クオート)も" (文字列) ( (リスト)も#u8( (各ユニフォームVector)など全てリーダマクロとして定義。
  • トライ木を使ってリーダマクロ のテーブルを表現している
  • 一文字ずつトライ木から検索できるように、標準ライブラリのトライ木モジュールを拡張しています。
  • Gaucheの標準ライブラリは全て読込み可能
  • 対応していない構文はsrfi-38の共有データ参照読込みだけです。
  • Gauche本体のread関数より5倍くらい遅い


こんな感じでリーダーマクロをベースとしたread関数を実装しました。
では本題のリーダーマクロの説明に入ります。


実装したリーダーマクロの仕様
  • COMMON LISP風のterminatingとnon-terminatingのマクロを持つ
  • 独自のリーダーマクロタイプ、right-terminatingを持つ
  • COMMON LISP風の文字種類操作ができる

    (whitespace, constituent, illegalと独自のskipという種類を指定できる)
  • ディスパッチマクロは存在しない
  • マクロ名の長さに制限が無い


COMMON LISPのリーダーマクロをベースにしつつ
かなり大胆に独自仕様を取り入れています。
一番大きな点はディスパッチマクロが無い代わりに、そもそもマクロ名の長さに制限が無いことでしょう。
なので、文字単位にマクロを紐付けるという考え方ではなく
文字列に対してリーダーマクロを紐付けるという考え方になります。


マクロ種類
今回の実装で重要になるのがterminatingとnon-terminating、さらに独自仕様のright-terminatingの動作の違いです。
(以下、それぞれをterm、non-term、right-termと書きます)
  • term
  • termに指定したマクロ文字列はシンボルの途中に存在しても認識されます。
    たとえば、' (クオート)や" (文字列の始まり)や( (リストの始まり)などがtermマクロです。

    a'bc
    

    という区切り文字を含まない文字列でも、aというシンボル、(quote bc)というリストの二つとしてreadされます。


  • non-term

  • non-termマクロの場合は読み取ったシンボルと完全一致した時だけ認識します。

    これは通常の関数やマクロの呼び出しと同じような考え方です。

    読み取ったシンボルに対応するリーダマクロが存在すれば、それが実行されます。

    #tや#fをnon-termマクロとして定義しています。


  • right-term

  • right-termマクロは、termとnon-termの中間の性質を持ちます。

    マクロ文字列は読み取ったシンボルの開始から一致する必要がありますが、シンボルの途中でもリーダマクロ文字列が完成した段階で、認識されます。

    right-termマクロは、本来はディスパッチマクロとして定義されていたリーダーマクロのために導入しました。

    そのため、Vectorのための#( などほとんどのディスパッチマクロはright-termとして定義しています。


それぞれのマクロ種類で動作がどのように違うのか具体例を示して説明します。
abcというリーダーマクロが定義された環境でreadしたとして、どのような結果になるかコメントで示します。
コメントの先頭が○ならabcリーダマクロの認識可能、×なら認識不可能です。

  • termの場合
  • abc   ;○
    abcd  ;○ abcリーダマクロが返した値とシンボルdの二つとして読み込まれます
    zabcd ;○ zシンボル、abcリーダマクロが返した値、シンボルdの三つとして読み込まれます
    

  • non-termの場合
  • abc   ;○
    abcd  ;× abcと完全に一致した時だけ、non-termリーダマクロは認識されます
    zabcd ;× 同上
    

  • right-termの場合
  • abc   ;○
    abcd  ;○ abcリーダマクロが返した値とシンボルdの二つとして読み込まれます
    zabcd ;× right-termマクロはシンボルがabcから開始している必要があるため、この場合は認識されません。
    


一致するマクロの探索
termとright-termは、シンボルの途中でも部分一致するマクロが見つかれば認識されます。
なので異なるリーダーマクロが同じプレフィックスを持つ場合、複数のマクロが一致することがあります。
今回の実装では、最長一致するマクロを認識するようにしています。
たとえば、abとabcというtermマクロが定義された環境で以下の文字列をreadすると、

zabcd

zabcdはabとabcのどちらも含んでいますが、この場合はより長い文字列のabcが採用されます。
結果、シンボルz、abcリーダマクロが返した値、シンボルdの三つとしてreadされます。


文字種類操作
COMMON LISPに習い、読み込んだ文字をどのように扱うかを指定することが出来ます。
whitespace, constituent, illegalはCOMMON LISPにもあり、同じ意味を持ちます。
skipは独自の種類で、単にその文字を飛ばし、readを継続します。whitespaceと違い区切り文字にはなりません。


今回の実装では、COMMON LISPにあるsingle escapeとmulti escapeがありません。
これはディスパッチマクロが無いことと関連しています。
Gaucheでは#| はブロックコメントの始まりという意味を持つのですが、この構文を認識するためにはmulti escape文字の| をconstituentとして扱う必要がありました。
single escape文字のバックスラッシュも同じ理由でconstituentにする必要があります。
別の文字をsingle escapeやmulti escape文字にすることも考えました。
ですが、single escapeとmulti escapeの動作はGaucheにとってあまり重要ではないのかな?と考え、実装をしないという判断をしています。


リーダーマクロを使ったサンプルプログラム
リーダーマクロを使ったサンプルとして、Whitespace言語をリーダーマクロベースで実装してみました。
プロジェクト内のsrc/whitespace.scmがそのソースコードです。
まず、文字種類の操作を利用して本来区切り文字になるスペースとタブと改行(LF)を構成文字として読込み、それ以外の文字を無視するように設定しています。
次に、Whitespace言語の命令(IMP)とコマンドの組をリーダーマクロ文字列にして、リーダマクロ内では単に対応するGaucheの式を返しています。
ただし、Whitespace言語のgoto命令は後方にあるラベルにもジャンプできるため、単純に各命令をreadでS式に変換してevalするだけ、というわけにはいきませんでした。
その点だけ良い方法が思いつかず、不本意な実装になっています。


実際に動かすには、Whitespace言語のExamplesページにある各サンプルをダウンロードして、
ya-readのルートディレクトリに移動して、以下のようにコマンドを実行してください。

$ gosh -I src/ src/whitespace.scm hworld.ws

すると、「Hello, world of spaces!」と表示されると思います。


カバレッジ計測

リーダーマクロを導入する過程で、read関数そのものを作成しました。
このread関数にフックを仕込めるようにすれば、さまざまなタイミングでより柔軟な式への操作を行えるようになります。
その応用の一つとして、評価される式の全てを

(begin
  (line 1 "test.scm")
  any-expression)

のように囲むことで、式が評価された時に同時にline関数を実行することが出来ます。
line関数内で式が実行されたことを記録するようにして、最後に統計を出せばカバレッジ計測機能の完成です。


評価される式はどこか?
「評価される式」と簡単に書きましたが、さて、その式の区別が簡単に付くでしょうか?
たとえば以下の式では、どこが構文的な意味を持つ式で、どこが実行時に評価される式でしょう。

(let ([a (+ 1 2)]
      [b 3])
  (* a b))

(if-let1 a (memq a l)
  (print "true")
  (print "false"))

(and-let* ([num (string->number str)]
           [(exact? num)]
           [(integer? num)])
  (print num))

Gaucheのプログラムに慣れている人であれば、一目でわかるかもしれません。
ですがそれ以前に、Gaucheはdefine-syntaxやdefine-macroによって、ユーザが自由に構文を定義できます。
さらに、letが上書きされている場合など、環境によって結果が変わります。
構文は無限に存在して、環境により結果も変わる。そんな状況では事前に構文に対するパース処理はかけません。
そんなわけで、無限の構文を有限にするため、以下の様な流れで処理を行います。


評価される式を特定する流れ
青字はGaucheの機能を利用
赤字は自前実装
  1. ソース
  2. テキストが次の処理に渡ります。
  3. read
  4. S式が次の処理に渡ります。
  5. Gaucheコンパイラ Pass1
  6. iform(Gaucheコンパイラで使用する内部表現)が次の処理に渡ります。
    マクロ、拡張構文はコンパイラによって全て展開されます。
  7. コンパイル
  8. S式が次の処理に渡ります。
    この時に、評価される式への操作が出来ます。
  9. Gaucheコンパイラ
  10. コンパイル済みのコードが次の処理に渡ります。
  11. eval
無限の構文を有限の構文に変換するため、処理の三番目でGaucheコンパイラのPass1だけ実行します。
Pass1では全てのマクロと拡張構文が展開され、iformと呼ばれる構造に変換されます。
iformはタグで区別され、そのタグは現状25個しかありません。言いかえると、25個しか構文が存在しないプログラムに変換されるわけです。
25個しかないのであれば一つ一つの解析をすることができるので、タグ別の処理を書いてiformの中から評価される式を抽出しています。
このいったんコンパイラのPass1を通すというアイデアは、koguroさんのglitを参考にしました。


抽出する作業と、次にまたコンパイラを通さないといけないためiformからS式に戻すという作業を四番目で行っています。
この逆コンパイルGaucheの内部処理にべったりと依存してしまっているので、バージョンが変わると動かなくなるでしょう。
しょーがないです。


Gaucheのテストコードでカバレッジを計測してみる
せっかくなのでGauche本体のテストを実行してみて、標準ライブラリに対するテストカバレッジを出してみましょう。
その前にGaucheのテストコードには自前実装のreadで読み込むことが出来ない参照読込み(#0#や#0=など)が含まれているため、そこの部分だけコメントアウトする必要があります。

それ以外は読み込んで実行することができるのですが、自前のread関数で実装をサボっているところや、カバレッジ計測のために式をインジェクションしているため、テストが失敗することがあります。ですが今回は気にしない方針で行きましょう。
※実はio2.scmにも参照読込みを使用したコードが存在しますが、io2.scmは他にもちゃんと対応できていない構文がたくさんあるためテスト対象から外しています。Gaucheのread関数のコンパチを名乗るためには、このio2.scmをちゃんとread出来るようならないといけないですね。


それでは結果ですが、Gaucheカバレッジは52%でした。
実際にプログラムから出力された各ファイルの明細をこちらからダウンロード出来るようにしています。ぜひ見ていってください。
フォルダ内のindex.htmlにサマリ、サマリ内の各ファイル名をクリックすると詳細を見ることが出来ます。


実際にGaucheのテストカバレッジを図るためには、ya-readディレクトリのルートで以下のように実行する必要があります。

gosh -I src/ -I /hoge/Gauche/test/ src/coverage.scm --basedir=/hoge/Gauche/src/ --tests=GaucheTests

注意点として、/hoge/Gauche/test/と/hoge/Gauche/src/の二つの部分は実際にGaucheソースコードがあるパスに書き換えてください。
また、なぜかsystemのテストの動作が不安定です。完全に止まってしまいどうしようもない時はGaucheTestsファイル内のsystem行の先頭に;を入れてコメントアウトしてください。
テストには普通に実行するのと比べてかなり時間がかかります。辛抱強く待つと急にテストが終わるので、そうすると現在のディレクトリに.coverageというディレクトリが出来ていると思います。その中に上でダウンロードできる内容と同じ明細が作成されています。


ステップ実行

やっと三つ目。Gaucheスクリプトのステップ実行です。
ここまで来ると特に難しいことはやっていません。
カバレッジ計測の時と同じ要領で関数呼出を行う式を全て以下のように修正します

(begin
  (break ...)
  func-call-expression)

break関数は対象の式がブレイクポイントになっているかを確認し、ブレイクポイントであればプログラムの実行を一時中断させます。


すごいぞcall/cc
言語実装者やライブラリ実装者にとっては厄介でしかない(と、僕は思っている)、そしてユーザから見ても使いどころがわからない(と、僕は思っている)。
それがcall/cc。
ですが今回のように、プログラムの環境を保存しておいて大域脱出、その後もとの位置に復帰して実行を継続する。こういったことをしようと思ったときにはcall/ccがあってよかったと思います。
そんなわけで一時中断と復帰してからの継続にはcall/ccを使っています。おかげでかなり簡単に実装することが出来ました。


機能一覧
続いてこのスクリプトが行えることを説明したいと思います。
  • breakで実行を止めることができる
  • 停止中にstep, next, continueができる
  • 停止中の環境で式を評価できる(watch)
  • ただし手抜きをしているのでset!による破壊的代入後の値を参照できません。
  • ステップアウトはできない
  • バックトレースを出力できない
  • いろいろできない
  • 正直、時間がたらんかった。実装適当。テストも適当。
こんな感じです。
breakによる停止とstep等の順次実行はもちろんできます。ですが一度関数内にステップインしてしまったら最後。ステップアウトが出来ないため、順次実行していき関数から抜ける必要があります。
停止した位置のローカルフレームを保持しているため、watchができます。これはがんばった点ですが、プログラム内でset!された場合は上書きした値を参照できません。片手落ちですね。


最後にこのスクリプトを実行する方法を説明します。
ya-readディレクトリのルートで以下のコマンドを実行してください

gosh -I src/ src/debug.scm step.scm

最後のstep.scmはステップ実行の対象にしたいスクリプトです。
step.scmの内容は以下の様なものです。

好きなファイルを指定すればステップ実行ができると思います(本当に思うだけで、落ちる可能性も大いにあります)。
起動に少し時間がかかりますが、しばらくすると警告がいくつか出た後にgdb> というプロンプトが表示されコマンド入力が可能になります。
それではstep.scmを引数に渡した場合の、ステップ実行の操作例を紹介します。
ちなみに;以降はコマンドを説明するためのコメントです。実際には出力されません。

gdb> run
gdb> break 4      ;step.scmの4行目add関数内の(print "before"の出力前)で一時停止するように指定しています
gdb> exec (add 2) ;exec eでeを評価することが出来ます。今回はstep.scm内のadd関数にを評価します。
gdb> stop: /hoge/huga/ya-read/step.scm:4    ;step.scmの4行目で一時停止した、というメッセージが出力されます。
gdb> next    ;nextコマンドで、同一関数内の次の行へ移ります。
gdb> before  ;beforeはstep.scmのプログラムによって出力された内容です。
stop: /hoge/huga/ya-read/step.scm:5
gdb> next    ;nextの代わりにstepを実行するとstring-split関数にステップインできます。(先述のとおりステップアウトできないので覚悟してください)
gdb> (hoge huga)
stop: /hoge/huga/ya-read/step.scm:7
gdb> print (+ a b c)    ;7行目の実行時環境内で(+ a b c)を評価して、その結果を表示します。
gdb> 10      ;print (+ a b c)の結果が表示されます。
gdb> step    ;stepコマンドでmul関数内に入ります。
gdb> stop: /hoge/huga/ya-read/step.scm:11
gdb> step
gdb> 13
stop: /hoge/huga/ya-read/step.scm:8
gdb> next
gdb> after

書いた本人が言うのもあれですが、かなりごちゃっとしていてわかりにくい説明ですね。
ごめんなさい。


こんな中途半端なステップ実行機能ですが、
言語自体による特別なサポートがなくてもread時の式操作とcall/ccがあればここまでデキルんだぜ(どやぁ)
というのが書きたかっただけです。
なのでこれ以上進展させるつもりはありません。


おわりに

最初に書いたとおり、かなり内容が長くなってしまいました。
単にリーダーマクロを導入するだけの予定でしたが、好き勝手いじることができるread関数を手に入れてしまうと、なかなか夢が広がりますね。
単にプログラムを実行するだけでは足りない人はマクロを利用しますが、それでも足りない場合はリーダーマクロやread時フックです。
まぁ強力な分、実際に書いたプログラムとはかけ離れた形としてreadされてしまう可能性があるため、デバッグがさらにさらに複雑になってしまうんですけど。
そんなこともLISPならではということで、LISPerの皆さんなら楽しむことができますよね?

Erlang R17 の名前つきローカル関数の速度

2014年4月9日にErlangの17.0がリリースされました。
17.0では新しいプリミティブタイプのMapsと専用のシンタックスが追加されて、そこそこ古い言語なのに意欲的だなぁという風に見ていました。


で、もう一つの大きな変更として、こちらのサイトでも紹介されているようにローカル関数に名前を付けられるようになりました。
先ほどのサイトの例でもあるように、これによってローカル関数の自己再帰呼出が書きやすくなります。
ローカル関数で自己再帰呼出が出来るということは関数内のループ処理が書きやすくなるので、既存コードのループ部分を書き換えるかどうかを判断するためにテストをしてみました。


テストは単純なループにどれぐらい時間がかかるかを計測したものです。
メモリ使用量は測っていないので、本来ならメモリ使用量も測るべきかな。
以下、テストに使用したコード。

loop_test(C) ->
  io:format("loop1:~p~n", [timer:tc(?MODULE, loop1, [C, 0])]),
  io:format("loop2:~p~n", [timer:tc(?MODULE, loop2, [C, 0])]),
  io:format("loop3:~p~n", [timer:tc(?MODULE, loop3, [C, 0])]),
  ok.

loop1(0, N) ->
  N;
loop1(C, N) ->
  loop1(C-1, N+1).

loop2(C, N) ->
  (fun Loop(0, NN) ->
      NN;
    Loop(CC, NN) ->
      Loop(CC-1, NN+1)
  end)(C, N).

loop3(C, N) ->
  loop(fun(_, 0, NN) ->
        NN;
      (Loop, CC, NN) ->
        Loop(Loop, CC-1, NN+1)
    end, C, N).

-spec loop(fun((fun(), any(), any()) -> any()), any(), any()) -> any().
loop(Fun, Arg1, Arg2) ->
  Fun(Fun, Arg1, Arg2).

loop1はグローバルに定義した関数の再帰呼出。一番速いはずなので基準として使用します。
loop2は新しい書き方を利用した関数。今回はこれの速度を計りたい。
loop3は既存コードで書いているやり方。ユーティリティ関数経由して無名関数に名前を与えている。


で、以下が実行して出力された結果。

loop_test(10000000).
loop1:{178750,10000000}
loop2:{764379,10000000}
loop3:{306386,10000000}
ok

新しい書き方をした場合、ユーティリティ関数を経由するやり方と比べても2倍以上かかっている。
そして、既存コードのやり方がそんなに遅くないということも今回初めて知った。


比べると速度の差はあるが、一回当たりの差にしてしまうと微々たるものなので気にするほどでもないかもしれない。
しかしせっかく新しい機能なのになんとなくがっくり。
configure時の引数を間違えたんじゃないかとも思ってしまう。
気長に待ってたら最適化されるようになるのかな?

Andrew K. Wrightのmatchを読む


いくつかのScheme処理系のパターンマッチライブラリとして採用されているAndrew K. Wrightのmatchライブラリがある。

このmatchライブラリがどのようなものかは、日本語だとGaucheutil.matchモジュールのドキュメントがわかりやすい。

このmatchライブラリのソースを手に入れて(@SaitoAtsushiさんにいただきました。感謝!)、読んでみようとしているのだけどなかなかに手ごわそうで、途中で心が折れてしまわないように、途中経過の自分用メモ内容をネットにあげながら自分を追い込もうと思う。



基本的には関数単位で、役割や内容を呼んで行く。

手元のソースコード上にもコメントとしてメモを書いているため、全ての説明メモをここに記述することは出来ないけどある程度わかるようには書くように心がける。



先頭に記述されている、matchの動作を制御するための変数と関数群は後回し。



  • genmatch
  • 後回し。

  • genletrec
  • 後回し。

  • gendefine
  • 後回し。

  • pattern-var? (λ (x) ...)
  • xがシンボル以外なら#f。
    (dot-dot-k? x)が#t なら #f。
    シンタックスシンボル(quasiquoteやquote、and, orなど他いろいろ)なら#f。
    それ以外は#t。
    つまり、パターン中にある任意の式にマッチするシンボルなら#tを返す。

  • dot-dot-k? (λ (s) ...)
  • sが...のシンボルなら0。もしくは..数字なら数字の部分を返す。
    ちなみに...の部分は___でも代用可能。
    それ以外は#f。

  • error-marker (λ (match-expr) ...)
  • gen...の三つの関数から呼ばれている。
    match:error-control変数の値によってエラー時の操作を制御する。
    match:error-controlが
    • 'unspecifiedの時は、単にundefinedな値を返す。undefinedな値は(cond (#f #f))で生成している。
    • 'errorまたは'failは実行時エラーを発生させる。
    • 'matchはマッチ式の追加情報とともに実行時エラーを発生させる。

  • unreachable (λ (plist match-expr) ...)
  • gen...の三つの関数から呼ばれている。
    plistのそれぞれの要素Xについて、(car (cdddr X))が#fなら警告文字列を出力する。
    plistの構造はまだわからないので、詳細は後回し。

  • validate-pattern (λ (pattern) ...)
  • gen...の三つの関数から呼ばれている。
    引数のpatternにはたとえば以下の様なmatch式の場合、

    (match 123
      [(? string? x) (list 'string x)]
      [(? number? x) (list 'number x)])
    

    各節のパターン部である、(? string? x)と(? number? x)がそれぞれ渡されて処理される。



    validate-pattern自体は内部で定義されているordinary関数に引数のpatternを渡すことしかしていない。

    内部にヘルパー関数が定義されているため、別々に説明する。



    • simple? (λ (x) ...)
    • xがstring?、boolean?、char?、number?、null?のいずれかなら#t。それ以外は#f。
      つまり、xがリテラルなら#tを返す。

    • ordinary (λ (p) ...)
    • pがどのような値かによって処理を分岐する巨大な条件分岐を持つ関数。
      処理の内容はpの値を正規化しながら、無効な構造ならエラーを発生させる(たぶん)。
      pがリストで(car p)の値が定められたシンボル(quasiquoteや?、=、andなど)かどうかで処理を分けている。

      【余談だけども】ここの巨大なif式はcondを展開したような形になっている。
      はじめてみた時は若干引いてしまったが慣れてしまえば難しいことをしている式ではなかった。
      だけどこの巨大なif式はcondの偉大さを教えてくれるいい例な気がする。

      pがvector?の場合は、各要素をordinaryに通しながらリストに変換される。
      どの構造にも当てはまらなければシンタックスエラーを発生させる。

    • quasi (λ (p) ...)
    • ordinary関数の中で(quasiquote X)の形式を見つけた時にXの部分を処理するためのヘルパー関数。
      処理の内容はordinary関数と同じようにpを正規化しながら、無効な構造ならエラーを発生させる。
      pがリストなら特定の構造を持っているかテストされ構造ごとの処理に分岐する。
      • pが(unquote X)なら(ordinary X)になる。
      • pが((unquote-splicing X))なら(ordinary X)になる。
      • pが((unquote-splicing X) *)なら(append (ordlist X) (quasi *))になる。(*は(cdr p)を意味する。)
      • 【また余談です】ここのunquote-splicingのチェックでは、unquote-splicingが複数引数を持つようになっているとunquote-splicingの処理と認識しないようになっている。
        これはr5rs(と、たぶんr7rs)では複数引数を取るunquote-splicingの動作が未定義になっているためだと思う。
        処理系によっては独自の解釈で複数引数を受け付けるようになっていた気がするが、その処理系ではここのチェックはどうなってるんだろう?

      • pが(X ...)なら*1( (quasi (cdr p)))
      pがvector?の場合はordinary同様に各要素をquasiに通しながらリストに変換される。
      どの構造にも当てはまらなければシンタックスエラーを発生させる。

    • ordlist (λ (p) ...)
    • pが'()かpair?でなければシンタックスエラー。
      それ以外の動作は、(map ordinary p)と同等。

  • bound (λ (pattern) ...)

  • この関数も内部に複数のヘルパー関数を持っているため別々に説明する。

    内部関数である、bound、boundv、bound*が継続渡しスタイルで処理されるため難しそうに見えるが、処理のパターンが理解できればそんなに複雑なことはしていない。



    • bound (λ (p a k) ...)
    • 外側の関数と同名の関数。これがメインの処理を行っている。
      引数pは処理するパターン式。この式を分解しながら再帰的に降下することで解析している。
      引数aはパターン式内に出現する束縛シンボルのリスト。aはaccumulatorの頭文字か?
      ちなみに束縛シンボルとは、

      (match '(foo bar)
        [('baz x) x] ... パターン1
        [(x y) x]) ... パターン2
      

      パターン1ではx、パターン2ではxとyの様なシンボルのこと。

      引数kはbound関数終了時に実行する関数。おそらくkは継続を表す。



      bound処理本体ではpがどのような値かを判別しながら処理している。

      たとえばpが、

      • '_だった場合
      • 通常シンボル同様全てのパターンを受け付けるが、構造の束縛は行わないため引数aに束縛リストに追加はしない。
      • (or p1 p2 ..)だった場合
      • 各パターン部に出現する束縛シンボルは同じ内容でなければならないためチェックを行っている。(ここで後述するpermutation関数が使われている)
      • (not p1 p2 ..)だった場合
      • 各パターンには束縛シンボルが現れてはいけないためチェックを行っている。(ここでもpermutation関数が使われている)
      • (x ...)だった場合(ちなみにここの...はdot-dot-k?的なシンボルのこと)
      • なにやら複雑な値を生成しているが先を読むまでわからないため後回し。
      • Vectorだった場合
      • リストに変換し処理をboundv関数に委譲する。

    • boundv (λ (plist a k) ...)

    • bound関数の引数pがVectorだった時のヘルパー関数。

      単純にplistの各要素をboundに渡して結果をconsする。

      plistの末尾が(x ...)の様な構造だった時は要素に分解せずplistをそのままboundに渡している。


    • bound* (λ (plist a k) ...)

    • plistの各要素について内部関数であるboundを適用させる。

      boundのk引数には(cdr plist)を自己再帰的に適用させるクロージャを生成して渡している。

      つまり、bound関数の処理終了後に実行すべき継続を渡している模様。


    • find-prefix (λ (b a) ...)

    • リストbの先頭からリストaに含まれない要素だけを取り出す。

      例:

      (find-prefix '(1 2 3) '(2 3)) => '(1)
      


    • permutation (λ (p1 p2) ...)

    • リストp1、p2について、順不同で内容が同じかどうかをチェックする。

      例:

      (permutation '(1 2) '(2 3)) => #f
      (permutation '(3 2 1) '(2 1 3)) => '(1 3)
      

      異なる要素を持っていれば#f。

      同じであればリストが返るのだが、呼出元では全てnot関数を通っているので値は使用されていない。


  • inline-let (λ (let-exp) ...)

  • gendefineとgenmatchから呼ばれている。

    引数のlet-expはlet式の構造を持つリスト。letのbodyの部分は単一の式という前提で処理されている。

    この関数は引数として渡されたlet式から無駄な式を削除、置き換えをして最適化を図っている

    たとえば

    (let ([x 1]
          [y (lambda () 2)]
          [z 3])
      (+ x y)
    

    上記のlet式は次のように変換される。

    (+ 1 (lambda () 2))
    

     定数の置き換えと参照されていない変数の削除が行われ、最後にletの束縛部が一つもなければ実行部分だけに変換される。



    この関数も内部にヘルパー関数を持っているため、別々に説明する。

    • occ (λ (x e) ...)
    • 引数xはlet束縛部の変数名シンボル。
      引数eはletの実行部分の式。
      occはletの実行部分の式eの中で変数xが何回参照されているかをカウントする。

    • const? (λ (sexp) ...)
    • 名前のとおり、引数sexpが定数かどうかを判断する。
      sexpがペアだった場合、(quote x)でxもシンボルだった時だけconst?は#tを返す。

    • isval? (λ (sexp) ...)
    • 引数sexpが即値であれば#tを返す。
      (const? sexp)が#tであればisval?も#tを返す。
      または、sexpがペアで、(car sexp)がlambda、quote、match-lambda、match-lambda*のいずれかであれば#tを返す。

    • small? (λ (sexp) ...)
    • 引数sexpが単純な式なら#tを返す。
      (const? sexp)が#tであればsmall?も#tを返す。
      または、sexpの構造が(lambda _ x)で、(const? x)が#tならsmall?は#tを返す。



  • gen (λ (x sf plist erract length>= eta) ...)

  • bound関数が返した結果などを利用して実際にパターンマッチを行うコードを生成する関数。

    コード生成には多くの関数がかかわっていて、しかもパターンマッチに成功した時の継続と失敗した時の継続を複数の関数間で引き渡しながら処理を進めている。

    読む人にとっては地獄以外の何者でもない。ただ、手法としては参考になった。



    引数xには、パターンマッチさせる値そのものが入る。たとえば、

    (match '(1 2) [...] [...])
    

    の様なマッチ式の場合、xには'(1 2)が渡される。



    引数sfは、現在処理中のパターン以前ですでに実行されている値へのパターンマッチテストのリストが渡される。

    実際には、(string? x)や(equal? x pattern)、(not (null? x))などテスト関数のリストになっている。

    この引数は、すでにパターンマッチが確認されている値に関しては重複してテストを行わないように最適化するために利用されている。

    たとえば以下のマッチ式の場合、

    (match '((1 . 2) 3)
      [((_ . 2) (? string?)) 'string] ;;パターン1
      [((_ . 2) (? number?)) 'number]);;パターン2
    

    パターン1とパターン2では、

    • 最素に(_ . 2)というパターン
    • マッチして欲しい構造は長さ2のリスト
    ということは一致しているため以下のように展開される。

    (式の展開にはGaucheを使用して、gensymの部分は適当な名称に変更しました。)

    (let ((x '((1 . 2) 3)))
      (let ((pat2-body (lambda () 'number))
            (pat1-body (lambda () 'string)))
        ;;パターン1とパターン2の共通の構造テストで、((_ . 2) _ ...)という構造になっているとこまでチェックしている。
        (if (and (pair? x) (pair? (car x)) (equal? (cdar x) 2) (pair? (cdr x)))
          (if (string? (cadr x))
            (if (null? (cddr x))
              (pat1-body)
              (match:error x))
            (if (and (number? (cadr x)) (null? (cddr x)))
              (pat2-body)
              (match:error x)))
          (match:error x))))
    

    コメントに書いているように、最初の長いifで共通部分を一気にテストしている。

    しかし共通部分のテストでは長さが2以上のリストであるというところまでしかテストしていないため、パターンのそれぞれで長さが2かどうかのテストを行っている。



    引数plistには、bound関数がパターン部分を解析した結果を含んだデータのリストが渡される。

    リストの一要素は一つのパターンに対応している。



    引数erractは、パターンマッチに失敗した際に実行する式を生成する関数が渡される。

    この関数を実行すると、(match:error x)の様な式が生成される。



    引数length>=とetaには、(gensym)で生成されたユニークなシンボルが渡される。



以下、まだ未調査。出来次第追記します。

*1:quasi X) ...)になる。

  • それ以外は)((quasi (car p

  • Vimはもっと外の世界につながろうプラグイン作りました

    無駄に壮大なタイトルの記事ですが、内容は単純。
    (それなりに)汎用な外部プロセスとの対話プラグインを作りましたというお話。


    以前この記事ではVim上からGaucheのREPLといちゃいちゃするためのプラグイを紹介しました。
    今回紹介するプラグインはGoshREPLプラグインを汎用化したものです。
    このプラグインを利用してGaucheだけにとどまらずいろんなプログラムといちゃいちゃしましょう。


    いろいろなプログラムといちゃいちゃしている例


    上から順に

    必要なもの

    このプラグインは最初に紹介したGoshREPLを改造したものです。
    リポジトリvim_goshreplのままですが、Gaucheがインストールされていなくても問題なく動作することが出来ます。
    外部プロセスと通信するためにvimprocを利用しているので、かならずvimprocをインストールしてください。

    設定方法


    GaucheのREPLはプラグインをインストールすると自動的に使えるようになりますが
    ほかのプログラムと対話するためには少し設定を書く必要があります。

    ghciの設定例

    "ghciとの対話環境を開く関数定義
    function! Open_ghci()
      call ieie#open_interactive({
          \ 'caption'  : 'ghci',
          \ 'filetype' : 'haskell',
          \ 'buffer-open' : '12:sp',
          \ 'proc'     : 'ghci',
          \})
    endfunction
    
    "ghciを開くコマンド定義
    command! -nargs=0 Ghci :call Open_ghci()
    
    "おまけ
    "ghciに対してテキストを送るコマンド定義
    command! -nargs=1 GhciSend :call ieie#send_text(function('Open_ghci'), 'ghci', <q-args>)
    "選択しているテキストをghciに送るキーマップ定義
    vmap <F12> :call ieie#send_text_block(function('Open_ghci'), 'ghci')<CR>

    設定例の中で重要なのはOpen_ghci関数の中のieie#open_interactive(...)です。
    ieie#open_interactive関数が、引数で渡された設定を元に外部プロセスを起動し、別バッファを開き、読み込み、書き込み、バッファ出力...などいろいろと行ってくれます。
    引数の設定を変えることで全体の動作をカスタマイズすることできます。


    ついでにRuby(irb)とPython用の設定も書いておきます。

    "Ruby(irb)と対話するための設定
    command! -nargs=0 Irb :call ieie#open_interactive({
          \ 'caption' : 'ruby',
          \ 'filetype' : 'ruby',
          \ 'buffer-open' : '10:sp',
          \ 'proc' : 'irb',
          \ 'pty'  : 1,
          \})
    
    "Pythonと対話するための設定
    command! -nargs=0 Python :call ieie#open_interactive({
          \ 'caption' : 'python',
          \ 'filetype' : 'python',
          \ 'buffer-open' : '10:sp',
          \ 'proc' : 'python',
          \ 'pty'  : 1,
          \})

    設定を見てみると、caption/filetype/procがそれぞれの環境向けに変更されているのがわかると思います。
    実際、別な環境向けの新しい設定を作成するときに変更するのはこの三つぐらいでしょう。
    ですが一つだけptyという設定が増えています。irbpythonに関してはpty設定がないと正しく動作しません。
    新しい設定を作成してみるときはまずはptyなしで確認してみて、起動後何も表示されないようならptyを追加して試してみてください。


    最後にちょっとだけ

    このプラグインは前回紹介したviseで書いています(もともとVimScriptだったのを書き直しました)。
    viseでどのようにプログラムが組めるのかという例としてもちょこっと見てやってください。