Schemeライブラリ用の特殊なインデントをVimでやりたかったメモ

4 min読了の目安(約4300字IDEAアイデア記事

個人的にSchemeのライブラリは特殊なインデントで書いてたけど、ちょっとどうかということでやめたメモ。

Schemeのライブラリ形式

Schemeのライブラリは4種類程度のフォーマットが存在する:

  1. ライブラリシステムなんて無いから load で何とかしろ派 -- s7 のような組込み処理系など
  2. 処理系独自のモジュールシステムがある派 -- BiglooとかGambitのような昔からある有力な処理系に多い
  3. R6RS -- (library (lib name) ...) でライブラリを書く派。ChezScheme、Racket(のR6RSサポート)、GNU Guile等
  4. R7RS -- (define-library (lib name) ...) でライブラリを書く派。chibi-schemeやGauche等

R6RSやR7RSは処理系独自のモジュールシステムと一緒にサポートされていることも多い。

個人的に制作している yuni ではR6RS形式のライブラリフォーマットを採用していて、BiglooだろうがGaucheだろうが処理系を選ばずR6RS形式のライブラリを書かせることにしている。(yuniは非R6RS処理系向けに専用のローダを提供する)

標準的なインデントルールを無視したい

R6RS形式のライブラリは、ライブラリの宣言と本体を単一のS式の中に書かなければならないという制約がある。読み易さのためには、

(library (hello)
         (export hello)
         (import (yuni scheme))

;; ↓ トップレベルの define が (library ...) と同じ高さ(インデントなし)になる
(define (hello)
  (display "Hello.\n"))
) ;; ← library の閉じカッコ
  ;;   ※ R6RS形式では、 (library ...) に全ての式を入れる必要がある

のように記述して、 (hello) が宣言される define の高さを下げたいが、通常のエディタでは、

(library (hello)
         (export hello)
         (import (yuni scheme))
	 ;; ↓ define が (import ...) と同じ高さになる
         (define (hello)
           (display "Hello.\n"))

)

となってしまい、ちょっと見た目がわるい。

個人的にはvimとslimv( https://github.com/kovisoft/slimv )でSchemeのコードを書いているので、vimスクリプトでなんとかすることにした。

(ちなみに、R7RSではC言語の #include に相当する機能が追加されて、ライブラリの本体を別のファイルに書けるようになったのでインデントの高さ問題は軽減された。)

書いたスクリプト

vimにはLisp専用のindent機能として set lisp で有効化できる lispindent があるが、上記のような特殊なルールを記述できる程の柔軟性はない。

そこで、.vimrc に専用のインデント取得関数を宣言し、それを indentexpr に指定してやることで求めるインデントルールを実装してみた。

function YuniLispIndentLib(lnum)
  " lispモードに戻す (indentexprが呼ばれるようにするために、普段はnolispする必要がある)
  set lisp
  let ind = lispindent(a:lnum) " lispindentを取得
  " 元に戻す
  set nolisp
  " 以降の処理でカーソルを動かすのでカーソル位置を保存
  let curpos = getpos('.')
  call cursor(1,1)
  " (import ...) の行を探し、(importのインデント高さを取得する
  let headpos = search('^ *(import') 
  if headpos
    call search('(import')
    let headpos2 = getpos('.')
    " import 宣言の終端を取得する
    call searchpair( '(', '', ')', 'W')
    let headln = headpos2[1]
    let headind = headpos2[2]
    if headln < curpos[1]
      " 現在行が import 宣言以降であればインデントの高さを下げる
      ind = ind - headind
    endif
  endif
  " カーソルを元に戻す
  call cursor(curpos[1], curpos[2])
  return ind
endfunction
function YuniSchemeLibraryInit()
  set indentexpr=YuniLispIndentLib
endfunction

augroup yuniconfig
  " 拡張子 .sls についてだけindentexprをセットアップ
  au VimEnter *.sls call YuniSchemeLibraryInit()
augroup END

ポイントは、インデントの取得中に lispindent を呼べる点。slimvもこの方法でvim内蔵のLispインデントをカスタマイズしているようだ。

R6RSライブラリのインデント習慣

と、言うわけでエディタで特殊なインデントルールを実装することができたのでコーディングルールとして取り込もうかと一瞬思ったけど、どうやらこういうインデントをしているのは 非常にレア っぽく、他のR6RSコードでは exportimport を下げて、トップレベルは1段だけインデントするというスタイルが一般的なようだ。

(library (rnrs records syntactic (6))
  (export define-record-type
	  record-type-descriptor
	  record-constructor-descriptor
          fields mutable immutable parent parent-rtd protocol 
          sealed opaque nongenerative)
  (import (for (rnrs base (6)) run expand)
          (for (rnrs syntax-case (6)) expand)
	  (rename (r6rs private records-explicit)
		  (define-record-type define-record-type/explicit)))
  ;; ★ ↓ export や import と同じ高さになっている
  (define-syntax define-record-type
    ;; Just check syntax, then send off to reference implementation

Schemeのライブラリにとって、トップレベルはライブラリから export する変数を宣言できる唯一の場所で、それが画面端に来ていると視覚的に判りやすいというメリットはある。

ただ、特殊な設定をしないと編集に参画できないというのも非常に大きなdisadvantageなので、ここは2文字のindentは甘んじて受けるべきなのではないかと考えている。

... そもそもR7RSの include を使ってライブラリの export import 宣言と本体を別ファイルに分ければ良いじゃんというのも有るかもしれないが、yuniでは意図的にR7RSライブラリ形式を避けてR6RS形式を採用している。

このようなインデントを行うには、単に lispwordslibrary を足せば良い。

  set lispwords+=library

はしっこは特等席か?

すみっこぐらしでなくても、端がUX上重要なロケーションであることはコンセンサスと言える。例えばApple Newtonは画面端をクリップボードとして利用できた( https://youtu.be/KFtgonf1KpA?t=796 )。

このため、top-level宣言が通常のプログラムでもライブラリでも画面端に来るのは重要な形質で、yuniではこれを守るため だけ に専用のライブラリフォーマットを真剣に検討している時期があった。 ...逆に言うと、これと天秤にかかるくらい、「通常のエディタで編集できる」と「標準(R6RS)と互換性がある」という性質も重要なものと言える。

もっとも、C言語やJavaScriptを除くと重要なシンボルを行頭で常に宣言できるプログラミング言語は実はそれほど多くない。例えば、C++では class 宣言に囲まれる形でAPIを宣言することが多いため、宣言の多くはインデントされることになる。