LISP風VimScript「vise」

LISPが好きですか?ときかれたら「Yes」と答えるでしょう。
VimScriptが好きですか?ときかれたら「No」と答えるでしょう。
だけど、Vimが好きですか?ときかれたら「Yes」と答えるでしょう。


そんなわけで今回作ったvise(ビーズ)です。


これはKPF#x08のネタとして作成して発表したものです。
リポジトリこちらgithubです。


インストール方法や細かな構文はリポジトリ内のdoc/vise.jaxにドキュメントがあるのでそちらを参照してください。
ここでは簡単なあらましと特徴的な機能に絞って紹介したいと思います。


viseはLISP構文を持つVimScript

LISP風VimScriptというタイトルのとおりviseはLISPの構文を持ちVimScriptに変換して実行するプログラミング言語です。
いわゆるトランスレータという分類になる言語ではないでしょうか。
最近ではJavaScriptやhtmlに変換するもののようにトランスレータと呼べる代物が増えてきました。
それらと並べるとは思っていませんが言語のくくりとしては同じです。


このようなトランスレータ系の言語は、
「元言語(変換後の言語)に不満はあるが何らかの理由によって使い続けなければならない」
というときに、元言語の限界を超えてより使いやすくしましょうということを目指すものです。


ではviseを作る理由となったVimScriptの不満とは、、、ただ一つです。


「VimScriptってLISPじゃないじゃん?」
ということでviseはLISP風な言語になったのでした。


実装はGaucheで行ってるんですか?

はい。そうなんです。
viseはGaucheで実装されており、viseコンパイラを動作させるにはGauche0.9.3以上が必要です。


viseの書き方

viseはたとえば次のような書き方でVimScriptを記述します。

;;もちろん全てLISP風記法
(echo "hohoho")
(echo (+ 1 2 (* 3 4 (- 5 6))))

;;文字列は二通りの記述方法
(echo "double quote string")
;;シングルクオート文字列は少し特殊な書き方をします
(echo #/single quote string/)

;;関数定義はこんな感じ
(defun square (num)
  (* num num))
(echo (square 10))

;;マクロもかけるよ
(defmacro zero? (num)
  `(== 0 ,num))
;;マクロはパターンマッチングで展開結果を分岐できます
(defmacro inc!
  (match
    ((_ sym) `(+= ,sym 1))		;(inc! num)の様に1引数のときマッチ
    ((_ sym delta) `(+= ,sym ,delta))));(inc! num 2)の様に2引数のときにマッチ

;;tryなどのVimコマンドも使用できます
(try
  (edit)
  (#/^Vim(edit):E\d\+/ (echo "error"))
  (else (echo "else"))
  (finally (echo "finally")))

;;VimScriptですが入子の同名変数を扱えます
(let ((val "outer"))
  (echo val) ;outerと出力される
  (let ((val "inner"))
    (echo val))) ;innerと出力される

;;foldも自前で定義できます
(defun fold (proc seed list)
  (rlet1 acc seed
    (dolist (x list)
      (set! acc (proc x acc)))))
(echo (fold + 0 '(1 2 3 4)))
(echo (fold (lambda (num acc) (+ acc (* num num))) 0 '(1 2 3 4)))


viseのマクロ

書き方例の中にしれっと書きましたが、viseはマクロを扱えます。
このマクロはviseのコンパイル段階で全て実行され展開されるため、変換後のVimScriptにはマクロに関するプログラムが残りません。
マクロを利用すればコンパイルタイムに処理を行えるため大変便利なのですが、一つ弊害が生まれます。
マクロの本体(つまりコンパイルタイムに実行される式)にviseの式は記述できません。
viseはあくまでコンパイラであり評価器を持っていません。そのためコンパイルタイムにvise自体の式を実行できないのです。


viseではマクロの本体にはGaucheの式を記述する必要があります。
viseを書くためにはGaucheの知識が必要になるということは短所ではありますが、
多くライブラリがそろっているGaucheの式を記述できるというのは長所にもなると考えています。


λ ...

LISP風を銘打っている以上ラムダ(lambda)は扱えるべきだろう、と。
そんなわけでviseではラムダを使用できます。

(defvar proc (let ((a 10))
               (lambda (b) (+ a b))))
(echo (proc 1))   ;11が出力される
(echo (proc 100)) ;110が出力される

簡単な例ですが、
procにはlambdaによって作られた手続きが束縛されています。
その手続きはaという変数を自由変数として参照しています。
実際にprocを呼んでいる位置からはaはもちろん参照できませんが、手続きは(静的スコープ上)正しい結果を返します。


もちろんVimScriptにラムダはないので、いろいろな処理を組み合わせてこのような動作になるようにしています。
処理の内容、トリックの秘密は変換後のVimScriptのコードを見れば理解できる人もいるのではないでしょうか。

function! s:SID_PREFIX_421()
  return matchstr(expand('<sfile>'),'<SNR>\d\+_\zeSID_PREFIX_421$')
endfunction

function! s:display420(b)dict
  return self['a:a'] + a:b
endfunction

let s:dict_type_419 = type({})

function! s:let_func418(a)
  return {'func':function(s:SID_PREFIX_421() . 'display420'),'a:a':a:a}
endfunction

let s:Proc = s:let_func418(10)

echo (type(s:Proc)==s:dict_type_419) ? s:Proc.func(1) : s:Proc(1)

echo (type(s:Proc)==s:dict_type_419) ? s:Proc.func(100) : s:Proc(100)

vise上では簡単なプログラムだったのですが、VimScriptへ変換した後は結構な量のコードになっています。
変換後コードに関してここでは詳細な解説は行いません。
ですが一つだけ、ラムダで作成した手続きは辞書型インスタンスとして扱います。実際の手続きは決まったキーとして設定され、自由変数も値として辞書内に取り込みます。


TIPS

そろそろ長くなってきたのでここからはサンプルソースと変換後コードだけ。
どんな処理かはなんとなく察してください。


末尾再帰呼出しの最適化

・before

;;末尾再帰呼出を使って階乗を求める関数を定義する
(defun fact (n)
  (let loop ((acc 1)
             (n n))
    (if (== n 0)
      acc
      (loop (* acc n) (- n 1)))))

・after

function! s:fact(n)
  let acc_418 = 1
  let n_419 = a:n
  let recursion_420 = 1
  while recursion_420
    let recursion_420 = 0
    if n_419 == 0
      return acc_418
    else
      let recursion_420 = 1
      let acc_418 = acc_418 * n_419
      let n_419 = n_419 - 1
    endif
  endwhile
endfunction


不要式の削除

・before

;;再設定(set!)がされていないdebug変数を定義
(defvar debug #f)

(defun hoge (arg)
  ;;debug変数の値によってデバッグ出力を行うかどうかが変わる
  (when debug
    (echo (string-append "argument:" (string arg))))
  arg)

・after

let s:debug = 0

function! s:hoge(arg)
  return a:arg
endfunction

これらの詳細はdoc/vise.jaxにあるので気になる人は参照してください。


viseのサンプル

viseはVim上で動作するREPLプラグインを持っています。
このviseREPL自体viseで書かれています。なのでこのソースを読めばviseの書き方や、viseで出来ること・出来ないことがわかると思います。
ソースはautoloadディレクトリ以下の
  • vise_repl.vise
  • repl_mapping.vise
  • common_macro.vise
です。


まとめ

ここまでざっとviseについ書いてきましたが、
正直なところ現状のviseの機能では実用には耐えられないと思っています。
トリッキーなVimScriptの記述方法はほとんどサポートできていませんし、
たまに使うようなコマンドでも実装されていないものがあります。(たとえばキーマップを定義するコマンドは全てサポートできていません。その理由はdoc/vise.jaxの最後の章、UNSUPPORTで。)


今後どうするかもあまり考えていないホビー用言語なので、
マクロだったりラムダがあるVimScriptとして面白がってください。