koturnの日記

普通の人です.ブログ上のコードはコピペ自由です.

CodeIQのスクエア・カルテット問題を解いた

はじめに

CodeIQで@riverplus氏による「 スクエア・カルテット」問題 という,初等整数論を絡めた面白いプログラミングの問題があったので,それについての記事を書いた. 高校生の数学を思い出す良い問題であり,楽しかった.

問題

2つの自然数の組 $(a, b)$ が与えられたとき,自然数 $x, y$ に関する次の方程式を考えます.

\begin{equation} x^2 + a^2 = y^2 + b^2 \label{eq:given-equation} \end{equation}

例えば, $(a, b) = (3, 10)$ のとき,方程式(\ref{eq:given-equation})の解は $(x, y) = (10, 3), (46, 45)$ の2組です.

自然数の組 $(a, b)$ に対し,方程式(\ref{eq:given-equation})の全ての解の $x + y$ の和を $F(a, b)$ と定義します. 例えば $F(3, 10) = 10 + 3 + 46 + 45 = 104$ です. 同様に, $F(10, 50) = 3500$ , $F(20, 100) = 15022$ となることが確かめられます.

標準入力から,半角空白区切りで 2つの自然数 $a, b$ ( $1 \leq a < b \leq 10^5$ )が与えられます. 標準出力に $F(a, b)$ の値を出力するプログラムを書いてください.

考え方

ここでは,$0 \notin \mathbb{N}$ とする.

与式(\ref{eq:given-equation})を変形して,

\begin{equation} (x + y)(x - y) = b^2 - a^2 = n \label{eq:converted} \end{equation}

とおく($a, b \in \mathbb{N}$ かつ $a > b$ より $n \in \mathbb{N}$). よって, $x + y \in \mathbb{N}$ , $x - y \in \mathbb{N}$ . 式(\ref{eq:converted})より,$x$, $y$ に関する連立方程式

\begin{eqnarray} \begin{cases} x + y = p & \\ x - y = q & \end{cases} \label{eq:xypq} \end{eqnarray}

を得る($p, q \in \mathbb{N}$ かつ $n = pq$). $i$ 番目の解 $(x_i, y_i)$ に $p_i, q_i$ が対応すると考えると,出力すべき値は,

\begin{equation} F(a, b) = \sum_i (x_i + y_i) = \sum_i p_i = \sum_i \dfrac{n}{q_i} \end{equation}

である. 連立方程式(\ref{eq:xypq})を解くと,

\begin{equation} (x, y) = \left( \dfrac{p + q}{2}, \dfrac{p - q}{2} \right) \end{equation}

を得る. $x \in \mathbb{N}$ なので,

\begin{equation} (p + q) \bmod 2 = 0 \label{eq:pq-constrain} \end{equation}

また,$x, y \in \mathbb{N}$ なので,

\begin{equation} x - y = q < p = x + y \end{equation}

$p, q$ は $n$ を2つの自然数積に分解したもの,すなわち,$n$ の約数のペアであることを踏まえると,

\begin{equation} (1 \leq) \:\: q < \sqrt{n} < p \:\: (\leq n) \end{equation}

以上より, $\sqrt{n}$ より小さく(「以下」ではない),かつ条件(\ref{eq:pq-constrain})を満たす $n$ の約数 $q_i$ を全て見つけ出し,対になる約数 $p_i = \dfrac{n}{q_i}$ の総和 $\sum_i p_i$ を計算して,出力すると良い.

おまけ

$a < b$ という制約が無く,

  1. $a = b$ , すなわち $n = 0$
  2. $a > b$ , すなわち $n < 0$

である場合も,思考実験として考えてみる.

$n = 0$ の場合, $(x, y)$ は $x = y$ なる任意の自然数

$n < 0$ の場合,$x + y > 0$ なので,

\begin{equation} x - y = q < 0 \end{equation}

となり, $q$ がマイナス符号を担当する($p \in \mathbb{N}, q \in \mathbb{Z}$). 連立方程式(\ref{eq:converted})を

\begin{eqnarray} \begin{cases} y + x = p & \\ y - x = -q & \end{cases} \end{eqnarray}

と変形し, $y \rightarrow x'$ , $x \rightarrow y'$ , $p \rightarrow p'$ , $-q \rightarrow q' (> 0)$ と置き直すことで, $n > 0$ のときと同様に処理できる.

\begin{eqnarray} \begin{cases} x' + y' = p' & \\ x' - y' = q' & \end{cases} \end{eqnarray}

$-n (> 0)$ (マイナス符号が無い場合)と比較して, $(x, y)$ の組み合わせは逆転しているが,出力すべき値はその和

\begin{equation} F(a, b) = \sum_i (y'_i + x'_i) = \sum_i p'_i = \sum_i \dfrac{-n}{q'_i} \end{equation}

なので, $n$ と $-n$ のときの $F(a, b)$ は等しい. すなわち, $F(a, b) = F(b, a)$ .

まとめ

  1. $n = b^2 - a^2$ ( $n = | b^2 - a^2 |$ ) とし, $\sqrt{n}$ より小さく,$\left( q + \dfrac{n}{q} \right) \bmod 2 = 0$ となる $n$ の約数 $q_i$ を全て求める
  2. $\sum_i \dfrac{n}{q_i}$ を出力する

本番入力値と考察

以下の6ケースが本番での入力値であった.

10 26
11 389
123 456
35672 61243
71200 82321
19126 98765

1つ目のケース $(a, b) = (10, 26)$ は, $b^2 - a^2 = 576 = 24^2$ となり,コーナーケースであった($p = q = 24$ ,すなわち $(x, y) = (24, 0)$ を含めてしまうのは誤り). また,問題文の例にあった $(a, b) = (10, 50)$ は, $b^2 - a^2 = 576 = 2400$ となり,$2400$ は $\lfloor \sqrt{2400} \rfloor = 48$ を約数に持つので,(ある意味,前述のものと対になる)コーナーケースであった.

この2つのケースを考えると, $n = b^2 - a^2$ の約数を単純に $1, 2, \ldots, \lfloor \sqrt{n} \rfloor - 1$ から見つけ出すのは,本番ケースに限るならばうまくいくが誤りである. 解決策としては,$n$ が2乗数であるかどうかを判定し,範囲を調製しなければならないが,コードで書くと汚くなる上に面倒である. そこで,$1, 2, \ldots, \lfloor \sqrt{n - \epsilon} \rfloor$ ($\epsilon$ は十分に小さな正の実数)から約数を探索するようにすると単純に処理できるはずだ.

解答例

bashJavaC++で解答での解答例を紹介する. $ \epsilon = 1.0 \times 10^{-10}$ とした.

ちなみに,入力は各ケースにつき1行のみだったので,それぞれの解答例のように,whileでEOFまで読み込みを行う必要はない.

bashでの解答

あえて,bashで解くというのも面白い.

#!/bin/bash -eu

declare -i a b
while read a b; do
  declare -i n=$((b ** 2 - a ** 2))
  declare -i qMax=`echo "sqrt($n - 0.00000000001)" | bc | sed 's/\.[0-9]*$//g'`
  declare -i q answer=0
  for q in `seq 1 $qMax`; do
    (( n % q == 0 )) && (( (q + n / q) % 2 == 0 )) && (( answer += n / q ))
  done
  echo $answer
done

bashだと,下手に組むとTLEになるので,やや難易度は高かった(1秒の壁は大きい). expr コマンドは時間がかかるので,基本的にbashの算術式で計算し,平方根などの算術式では計算できないものは bc コマンドに投げて計算するだけだ.

なお,以下のようなbashの算術式のfor文

for ((i = 0; i < 100; i++)) {
  # 処理
}

だと時間がかかるので, seq コマンドで $1$ から $\lfloor \sqrt{n - \epsilon} \rfloor$ までの連続する整数のリストを生成し,通常のシェルのforを用いるとよい. また,ifのパースは時間がかかると予想できるので,短絡評価を利用し,bashの算術式を繋げるとよいだろう.

Javaでの解答

まともな言語,例えばJavaでは以下のように率直に書けばよいだろう.

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.stream.LongStream;

public class Main {
    public static void main(String[] args) throws Exception {
        try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in))) {
            br.lines()
                .map(line -> Arrays.stream(line.split(" "))
                        .mapToLong(Long::parseLong)
                        .map(x -> x * x)
                        .toArray())
                .map(inputs -> inputs[1] - inputs[0])
                .map(n -> LongStream.rangeClosed(1, (long) Math.sqrt(n - 1e-10))
                        .filter(q -> n % q == 0 && ((q + n / q) & 0x01) == 0)
                        .map(q -> n / q)
                        .sum())
                .forEach(System.out::println);
        }
    }
}

Javaでは2の累乗の定数の乗算,除算,剰余はビット演算に置き換えた方が速い. C/C++ならば,2の累乗の定数の乗算,除算,剰余は最適化オプションを付与しなくてもビット演算に置き換えられるが,JavaコンパイラJITコンパイルのためにバイトコードを最適化しないようになっているため,2の累乗の定数の乗算,除算,剰余は,コンパイル後のバイトコードにおいても乗算,除算,剰余のままである. これは生成されるバイトコードを見れば一目瞭然である. しかし,JITコンパイルにより最適化されるかといえばそうではなく,手動でビット演算に置き換えた方が高速に動作する.

なお, $b^2 < 10^{10}$ なので,64bit整数型を用いないとオーバーフローする. この点は気をつけないといけない.

C++での解答

みんな大好きC++で書くと,以下のようになる. for文で逐次的に処理するので,連続する整数の配列やリストを生成する必要がなく, $\lfloor \sqrt{n - \epsilon} \rfloor$ の議論が不要になる. 大半の人はこういった解答をしていると思う. 試していないが,ループ毎に $q^2$ を計算するので, $\lfloor \sqrt{n - \epsilon} \rfloor$ を計算しておく場合と比較すると遅い気がするのだが,実際はどうなのだろうか?

#include <cstdlib>
#include <iostream>

typedef long long  llint;


int
main()
{
  std::cin.tie(0);
  std::ios::sync_with_stdio(false);

  llint a, b;
  while (std::cin >> a >> b) {
    llint n = b * b - a * a;
    llint answer = 0;
    for (llint q = 1; q * q < n; q++) {
      if (n % q != 0) continue;
      llint p = n / q;
      if ((p + q) % 2 == 1) continue;
      answer += p;
    }
    std::cout << answer << std::endl;
  }

  return EXIT_SUCCESS;
}

候補絞り込み型インターフェースを提供するプラグインについて

この記事は Vim Advent calendar 2015 の16日目の記事です.

僕は普段「だ・である」調でブログを書いていますが,今回の記事は多数の人に見ていただくことを考慮して,多くの人がブログで採用している「です・ます」調,語り口調で書いていくことにしましょう.

さて本題です. 候補を絞り込むインターフェースというのは人気で,Vimではunite.vimctrlp.vimなどが有名ですね. そして,コマンドラインツールfzfVimからも利用できるように,本体にVimプラグインが付属しています. また,ctrlp.vimにインスパイアされて開発されたプラグインとして,LeafCageさんによるalti.vimやkamichiduさんによるvim-milqiがあります. この記事では,前述の5つの候補絞り込み型インタフェースを提供するVimプラグイン

について,簡単な解説と比較をしたいと思います. FuzzyFindervim-kuについては取り上げません.

基本操作の比較

unite.vim

unite.vimでは,本体にたくさんの拡張が付属しており,第三者(特に日本人に多い)によって開発された拡張も多数公開されているので,自分で拡張を書く必要は無いですね. 基本的に, :Unite [unite source名] とすることで,unite.vimを起動できます.

unite.vim上でのキーマッピングは以下のようになっています. unite.vimは他の候補絞り込み型プラグインと異なり,ノーマルモードとインサートモードがあり,それぞれのモードで操作が異なります.

キーマッピング

ノーマルモード
キー 動作
i / I インサートモードに
a 候補選択時はアクションを選択,そうでなければ A と同じ
A 入力欄の末尾にカーソル移動をしてインサートモード
q or <C-g> unite.vim を終了し,1つ前のuntieバッファメニューを復元する
Q or g<C-g> unite.vim を終了する
<C-r> uniteを再起動する
<Space> / <S-Space> 候補の選択状態をトグル(複数選択できるsourceのみ)
* 全ての候補選択をトグルする(選択状態が反転する)
M 表示候補数を制限する
<Tab> アクションを選択する
<C-p> / <C-n> sourceを切り替える
<C-a> / <C-k> ログ / 候補を echo する
<C-l> 再描画する
<C-h> 1つ前のパスを削除する
gg or <C-Home> カーソルを1番上に
G or <C-End> カーソルを1番下に
j or <Down> カーソルを1つ下に
k or <Up> カーソルを1つ上に
J / K マッチしない候補をスキップしてカーソルを上 / 下に移動
g? 簡単なuniteのキーマッピングのヘルプを表示
N 候補を追加する(action_tableunite__new_candidate が定義されている場合のみ)
. ドットを絞り込み欄に入力し,インサートモードに
<CR> デフォルトアクションを行う
b ブックマークのアクションを行う
d 削除アクションを行う
e narrowアクションを行う
t 別のタブで開く
yy ヤンクアクションを行う
o オープンアクションを行う
x クイックマッチを用いて,デフォルトのアクションを行う(マークされた候補があるとダメ)
インサートモード
キー 動作
<Esc> ノーマルモード
<Tab> アクションを選択する
<C-n> or <Down> 次の行を選択
<C-p> or <Up> 前の行を選択
<C-f> / <C-b> 前 / 次の行を選択
<C-h> or <BS> カーソルの1つ前の文字を削除
<C-u> カーソルの前の入力を全て削除
<C-w> カーソルの前の単語を削除
<C-a> or <Home> カーソルを先頭に
<Left> / <Right> カーソルを右 / 左に移動
<C-l> 再描画する
<C-g> uniteを終了する
ビジュアルモード
キー 動作
<Space> 選択された範囲の候補全ての選択状態をトグル

本体付属のunite source

以下の表は,unite.vim本体から提供されているunite sourceの一部です. デフォルトのアクションについても併記しています.

他にもたくさんのunite sourceが付属しているので,調べてみると面白いと思います.

unite source名 機能
bookmark UniteBookmarkAdd でブックマークされたファイルを表示し,選択されたファイルを開く
buffer リストされているバッファ一覧を表示し,選択されたバッファを開く
change ファイルの変更履歴(:changeの結果)を表示し,選択された位置に移動
command 利用できるコマンドを表示し,選択されたコマンドをコマンドウィンドウに入力(引数のヒントも表示)
directory カレントディレクトリ以下のディレクトリを表示し,ファイラのように利用できる
file カレントディレクトリ以下のファイルとディレクトリを表示し,ファイラのように利用できる
file_rec カレントディレクトリ以下のファイルを再帰的に検索して表示し,ファイラのように利用できる
find 対象ディレクトリと名前を入力させて,findコマンドを実行した結果を表示
function 利用できる関数を表示し,選択された関数をコマンドウィンドウに入力(引数のヒントも提示)
grep 外部コマンド grep を行うように促し,grepを行った結果を一覧として表示.選択された位置に移動する
history/unite 過去に利用したunite sourceの履歴を表示
jump ジャンプリスト(:jumpsの結果)を表示し,選択された位置に移動
launcher パスが通っているところから,実行可能ファイルを候補として表示する
line 現在のバッファの全ての行を候補として表示し,選択された行に移動
mapping 全てのキーマッピングを表示し,選択されたものを実行する
output Vimコマンドを入力を促し,入力されたコマンドの出力を候補とする
process 起動中のプロセス一覧を表示し,選択した候補にTERMシグナルを送る(確認あり)
register Vimレジスタを表示し,選択されたレジスタの内容を現在位置に挿入
runtimepath runtimepath に追加されているディレクトリ一覧を表示し,選択されたディレクトリに移動
source unite sourceの一覧を表示し,選択されたsourceをもとにUniteを起動
tab 現在利用中のVimのタブ一覧(とタブ内のバッファをヒントとして)を表示し,選択されたタブに移動
vimgrep :vimgrep を行うように促し,vimgrepを行った結果を一覧として表示.選択された位置に移動する
window 現在のタブのウィンドウ一覧を表示し,選択されたウィンドウに移動

ctrlp.vim

プラグイン名になっているように,デフォルトでは <C-p> でCtrlPを起動します. あるいは, :CtrlP というコマンドで起動してもよいでしょう.

unite.vimと同様,ctrlp.vimの本体からも多数の拡張が提供されており,それぞれの拡張は :CtrlPXXXX のようなコマンドを実行することで利用可能です.

CtrlPバッファでのキーマッピングは以下のようになっています.

キー 動作
<C-c> or <Esc> CtrlPを終了する
<C-f> / <C-b> エクステンションを切り替える
<C-a> / <C-e> 入力ウィンドウのカーソルを先頭 / 最後に
<C-j> / <C-k> 候補ウィンドウのカーソルの上下移動
<C-d> フルパス検索モードとファイル名のみの検索モードを切り替え
<C-r> 正規表現検索モードと通常の検索モードのトグル
<CR> 同じタブに開く
<C-t> 新しいタブに開く
<C-v> 垂直分割で開く
<C-x> or <C-s> or <C-CR> 水平分割で開く
<C-p> 1つ前の入力履歴を入力欄に入れる
<C-n> 1つ後の入力履歴を入力欄に入れる
<F5> キャッシュを更新する
<F7> 選択した(マークを付けた)候補を候補から除外する
<C-z> 対象ファイルにマークを付ける(選択状態にする)

また,ctrlp.vimでは,:CtrlP 以外にもコマンドが提供されています. これらは, autoload/ctrlp.vim で定義されている組み込みの拡張とも言えるでしょう.

コマンド 機能
:CtrlPMRUFiles [dir] 最近利用したファイルを表示し,選択されたファイルを開く
:CtrlPBuffer リストされているバッファを表示し,選択されたバッファに移動する
:CtrlPLastMode [args] 最後に利用したCtrlP拡張を実行する,[args]--dir が指定されたとき,最後のワーキングディレクトリも用いる
:CtrlPClearCache or :ClearCtrlPCache CtrlPのキャッシュを削除する
:CtrlPAllClearCache or :ClearAllCtrlPCaches CtrlPのキャッシュを全て削除する
:CtrlPCurWD カレントディレクトリ以下のファイルを再帰的に表示
:CtrlPCurFile 現在編集中のファイルのディレクトリ以下のファイルを再帰的に表示
:CtrlPRoot わからない....

先に述べたように,ctrlp.vimの本体からは多数の拡張が提供されています. 先に述べた組み込みの拡張とは違い,多数の拡張は autoload/ctrlp/xxx.vim で定義されており,本体から分離されております.

拡張名 対応するコマンド 機能
tag :CtrlPTag 見つかったtagsファイル(オプション tags に依存)の項目を表示し,選択された項目の位置へジャンプする
buffertag :CtrlPBugTag [buffer] カレントバッファ,または指定されたバッファからtagsファイルの項目を表示し,選択された項目の位置へジャンプする
buffertag :CtrlPBugTagAll リストされているバッファ
quickfix :CtrlPQuickfix quickfixにある項目を表示し,選択されたエラー位置に移動する
dir :CtrlPDir [dir] カレントディレクトリ,または指定されたディレクトリ以下のディレクトリを表示し,選択されたディレクトリをワーキングディレクトリにする
rtscript :CtrlPRTS runtimepath 以下にあるファイルを表示し,選択されたファイルを開く
undo :CtrlPUndo Undo履歴を表示し,選択されたところまでundoする
list :CtrlPLine リストされている全てのバッファ,または指定されたバッファの行を表示し.選択された位置にジャンプする
changes :CtrlPChange [buffer] カレントバッファ,または指定されたバッファの変更箇所を表示し,選択された位置にジャンプする
changes :CtrlPChangeAll リストされているバッファ全ての変更履歴を表示し,選択された位置にジャンプする
mixed :CtrlPMixed カレントディレクトリ以下のファイル,バッファ,最近使用したファイルを表示し,選択されたものを開く
bookmarkdir :CtrlPBookmarkDir ブックマークされているディレクトリを表示し,
bookmarkdir :CtrlPBookmarkDirAdd [dir] カレントディレクトリ,または指定されたディレクトリをブックマークする(選択後,ブックマークのタイトルの入力が求められる)

fzf

fzfはコマンドラインツールとして呼び出されるので,Vimとして操作することはできません. 用意されているマッピングは以下のように単純なものです.

キー 動作
<C-c> or <C-g> or <Esc> fzfを終了する
<C-f> / <C-b> カーソルを移動する
<CR> 候補を選択する
<S-Tab> 複数選択可能なとき(オプション -m or --multi が指定されているとき),候補にマークを付ける

本体から拡張は提供されていないので,自分で書くことになるでしょう. 公式のwikiにサンプルが多数ありますので,それらを ~/.vimrc にコピペなどすればよいかと思います.

alti.vim

alti.vimグローバル変数 g:alti_default_mappings_base'standard''ctrlplike' を代入することで,操作のプリセットを変更することができます. このグローバル変数のデフォルト値は 'standard' なので,何もしなかったり,無効な値を入力すると,standardのキーマッピングになるでしょう.

standard

キー 動作
<BS> or <C-h> 入力欄のカーソルの前1文字を削除
<Del> or <C-d> 入力欄のカーソルの後ろ1文字を削除
<C-w> 入力欄のカーソルの前1単語を削除
<C-u> 入力欄をクリアする
<C-r> レジスタ挿入モードに
<C-x><C-n> or <C-_> 1つ後の入力履歴を入力欄に入れる
<C-x><C-p> or <C-s> 1つ前の入力履歴を入力欄に入れる
<C-a> / <C-e> 入力欄の先頭 / 末尾にカーソルを移動
<C-b> or <Left> 入力欄のカーソルを左に
<C-f> or <Right> 入力欄のカーソルを右に
<C-j> or <PageDown> or <kPageDown> 候補欄で次のページを表示する
<C-j> or <PageDown> or <kPageDown> 候補欄で前のページを表示する
<C-n> or <Down> 候補欄のカーソルを1つ下に
<C-p> or <Up> 候補欄のカーソルを1つ上に
<C-g>g or <C-g><C-g> or <Home> or <kHome> 候補窓で先頭にカーソルを移動
<C-g>G or <End> or <kEnd> 候補窓で末尾にカーソルを移動
<Tab> 候補窓のカーソル下の候補を,入力欄に入力する
<C-o> 選択候補についてのアクションメニューを出す
<Esc> or <C-c> alti.vimを終了する
<CR> 入力(あるいはカーソル下の候補)に基づいて,アクションを行う
<C-y> 拡張の定義辞書のキー default_actions に指定されたリストの0番目に指定されたアクションを実行
<C-v> 拡張の定義辞書のキー default_actions に指定されたリストの1番目に指定されたアクションを実行

ctrlplike

キー 動作
<BS> or <C-]> 入力欄のカーソルの前1文字を削除
<Del> or <C-d> 入力欄のカーソルの後ろ1文字を削除
<C-w> 入力欄のカーソルの前1単語を削除
<C-u> 入力欄をクリアする
<C-r> or <C-\> レジスタ挿入モードに
<C-n> 1つ後の入力履歴を入力欄に入れる
<C-p> 1つ前の入力履歴を入力欄に入れる
<C-a> / <C-e> 入力欄の先頭 / 末尾にカーソルを移動
<C-h> or <Left> 入力欄のカーソルを左に
<C-l> or <Right> 入力欄のカーソルを右に
<C-f> or <PageDown> or <kPageDown> 候補欄で次のページを表示する
<C-b> or <PageDown> or <kPageDown> 候補欄で前のページを表示する
<C-j> or <Down> 候補欄のカーソルを1つ下に
<C-k> or <Up> 候補欄のカーソルを1つ上に
<C-g>g or <C-g><C-g> or <Home> or <kHome> 候補窓で先頭にカーソルを移動
<C-g>G or <End> or <kEnd> 候補窓で末尾にカーソルを移動
<Tab> 候補窓のカーソル下の候補を,入力欄に入力する
<C-o> 選択候補についてのアクションメニューを出す
<Esc> or <C-c> alti.vimを終了する
<CR> 入力(あるいはカーソル下の候補)に基づいて,アクションを行う
<C-y> 拡張の定義辞書のキー default_actions に指定されたリストの0番目に指定されたアクションを実行
<C-v> 拡張の定義辞書のキー default_actions に指定されたリストの1番目に指定されたアクションを実行

このプラグインも本体に拡張は付属していませんが,使い勝手はかなり良いので,拡張を自分で作って利用するとよいでしょう.

vim-milqi

vim-milqiはドキュメントが無いので,ソースコードを読んだ結果を書きます. このあたりを見る限り,ctrlp.vimから持ってきてると推測できるので,ほとんどctrlp.vimと同じ操作であると言ってよいでしょう.

vim-milqiの本体にも拡張は付属していませんが, :MilqiFromUnite というコマンドが提供されており,引数にunite source名を指定することで,unite sourceから候補を取得し,アクションを実行することができます. 他のプラグインでは提供されていない機能なので,なかなか面白いと思いました. しかし,unite sourceの候補にkindが指定されていない場合,エラーとなるので注意が必要です.

拡張の作りやすさの比較

各絞り込み検索プラグインの拡張の作りやすさを比較してみましょう. まず,拡張において実現できる特徴的なことを比較すると,以下のようになります.

プラグイン 複数選択 複数のアクションの提供 非同期の候補取得 プロンプトの変更
unite.vim ○(起動オプション)
ctrlp.vim △(候補選択時のキー) × ×
fzf × ×
alti.vim × ◎(動的に変更可)
vim-milqi × × ×

ここでいう「非同期の候補取得」とは,定期的に候補を取得処理を呼び出し,選択可能な候補数を増やす(あるいは減らすこともできるでしょう)ことです. Vimでは非同期処理をする手段がちゃんとした形で提供されていないので,非同期の候補の取得は,Shougo/vimproc.vimと, updatetimefeedkeys() , そして, CursorHoldCursorMoved という autocmd を組み合わせたポーリングによって行われるのが主流です. +clientserver の機能と RemoteReply を組み合わせても非同期処理は実現できますが,+clientserver になっているVimは多くないので,こちらが採用されることはありません.

非同期の候補取得でアニメーションを実現するという面白い使い方もあり,例えばsupermomonga/jazzradio.vimでは音量レベルのようなアニメーションを行うunite sourceが提供されています.

さて,同じ目的を達成するそれぞれの拡張を作ってみましょう. 今回は「apple, banana, cakeという3つの候補を表示し,それぞれ選択されたものを echomsg する」という非常に単純なものを作ることにします.

とにかく,コードを書かないと始まりませんね. 以下,それぞれのコードと解説です. なお,このサンプルの拡張は koturn/vim-exts にあります.

plugin/exts.vim : インタフェース

if exists('g:loaded_exts')
  finish
endif
let g:loaded_exts = 1
let s:save_cpo = &cpo
set cpo&vim

command! CtrlPExts  call ctrlp#init(ctrlp#exts#id())
command! FZFExts  call fzf#run(fzf#exts#option())
command! AltiExts  call alti#init(alti#exts#define())
command! MilqiExts  call milqi#candidate_first(milqi#exts#define())
" command! MilqiExts  call milqi#query_first(milqi#exts#define())

let &cpo = s:save_cpo
unlet s:save_cpo

インターフェースを定義しているだけなので,特に言及することはありません. uniteだけはコマンド定義しなくて良いので楽ですね. vim-milqiを用いるコマンドについて,コメントアウトしているものがありますが,これについてはvim-milqiの項目で解説します.

autoload/unite/sources/exts.vim : unite.vimの拡張

let s:save_cpo = &cpo
set cpo&vim


let s:source = {
      \ 'name': 'exts',
      \ 'description': 'descriptions',
      \ 'action_table': {},
      \ 'default_action': 'my_action'
      \}

let s:source.action_table.my_action = {
      \ 'description': 'my action'
      \}
function! s:source.action_table.my_action.func(candidate) abort
  echomsg a:candidate.word
endfunction

function! s:source.gather_candidates(args, context) abort
  return map(['apple', 'banana', 'cake'], '{
        \ "word": v:val
        \}')
endfunction


function! unite#sources#exts#define() abort
  return s:source
endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

unite#sources#exts#define() でunite sourceに関する辞書を返却するようにします. 今回は word のみ指定していますが, abbrkind など,他にも重要な項目もありますので, :h unite-notation-{candidate} で調べてみるとよいでしょう. この関数はunite.vim本体から呼び出されます.

上記の例では,myaction というアクションのみを source に対して定義し,<CR> で選択したときのアクションである default_action に指定しています.

また,今回はやっていませんが,s:source.action_table.my_actionis_selectable: 1 を追加すると複数選択できるようになり,s:source.action_table.myaction.func() の引数が,選択した候補の辞書ではなく,複数の辞書を含むリストになります.

なお,作成したunite sourceに引数を渡して起動したい場合は, Unite exts:arg1:arg2 のように,コロン区切りで引数を指定する形になります. この引数は, s:source.gather_candidates の第一引数 a:args というリストで受け取ることができます. この引数を補完する関数は, s:source のキー complete に,補完関数の関数参照を指定します.

function! s:source.complete(args, context, arglead, cmdline, cursorpos) abort
  return filter(['foo', 'bar', 'piyo'], 'v:val =~? ^"' . a:arglead . '"')
endfunction

のような形になり, <Tab> によって選択できる候補のリストを返却すればよいだけです. ユーザー定義コマンドの補完関数と似たようなもので,引数が2つほど増えているだけですね.

ちなみに,アクションはunite kindとして分離可能であり,丁寧なunite拡張を書くのであれば,候補の取得はsourceで,アクションの担当はkindにした方がよいでしょう(前述の通り,vim-milqiはKindを利用します). kindを指定しなかった場合,untie.vimではkindにcommonが指定されたものとします.

autoload/ctrlp/exts.vim : ctrlp.vimの拡張

if get(g:, 'loaded_ctrlp_exts', 0)
  finish
endif
let g:loaded_ctrlp_exts = 1
let s:save_cpo = &cpo
set cpo&vim

let s:ctrlp_builtins = ctrlp#getvar('g:ctrlp_builtins')

function! s:get_sid_prefix() abort
  return matchstr(expand('<sfile>'), 'function \zs<SNR>\d\+_\zeget_sid_prefix$')
endfunction
let s:sid_prefix = s:get_sid_prefix()
delfunction s:get_sid_prefix

let g:ctrlp_ext_vars = add(get(g:, 'ctrlp_ext_vars', []), {
      \ 'init': s:sid_prefix . 'init()',
      \ 'accept': s:sid_prefix . 'accept',
      \ 'lname': 'sample extension',
      \ 'sname': 'sample',
      \ 'type': 'line',
      \ 'nolim': 1
      \})
let s:id = s:ctrlp_builtins + len(g:ctrlp_ext_vars)
unlet s:ctrlp_builtins s:sid_prefix


function! ctrlp#exts#id() abort
  return s:id
endfunction


function! s:init() abort
  let candidates = ['apple', 'banana', 'cake']
  return candidates
endfunction

function! s:accept(mode, str) abort
  call ctrlp#exit()
  echomsg a:str
endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

ctrlp.vimの拡張の作り方に関しては,以下の記事に詳しく書いてあるので,そちらを見ていただいた方がよいでしょう.

ざっと説明すると, s:init() は候補の取得を担当する関数で, s:accept() がアクションを担当する関数になります. ctrlp#exts#id() は,この拡張のIDを返却する関数で,ctrlp.vimの本体の関数 ctrlp#init() に渡すことで,ctrlpが起動します. 他の候補選択型インターフェースと比較して,作成した拡張の起動に辞書をそのまま渡せる形にしてほしかったと感じます.

autoload/fzf/exts.vim : fzfの拡張

let s:save_cpo = &cpo
set cpo&vim


function! fzf#sampleoption() abort
  return {
        \ 'down': 20,
        \ 'sink': function('s:sink'),
        \ 'source': s:gather_candidates()
        \}
endfunction

function! s:gather_candidates() abort
  return ['apple', 'banana', 'cake']
endfunction

function! s:sink(candidate) abort
  echo len(readfile(a:candidate))
endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

fzfはファイルを分離する必要は無いのですが,他のプラグインに習って分離してみました. 拡張の作り方については,以前書いた

を参考にするとよいでしょう.

autoload/alti/exts.vim : alti.vimの拡張

let s:save_cpo = &cpo
set cpo&vim

function! s:get_sid_prefix() abort
  return matchstr(expand('<sfile>'), 'function \zs<SNR>\d\+_\zeget_sid_prefix$')
endfunction
let s:sid_prefix = s:get_sid_prefix()
delfunction s:get_sid_prefix

let s:define = {
      \ 'name': 'exts',
      \ 'cmpl': s:sid_prefix . 'cmpl',
      \ 'prompt': s:sid_prefix . 'prompt',
      \ 'submitted': s:sid_prefix . 'submitted'
      \}
unlet s:sid_prefix

function! alti#exts#define() abort
  return s:define
endfunction


function! s:cmpl(context) abort
  return a:context.filtered(self.candidates)
endfunction

function! s:prompt(context) abort
  return 'exts> '
endfunction

function! s:submitted(context, line) abort
  if len(a:context.inputs) == 0
    echomsg a:context.selection
  else
    for input in a:context.inputs
      echomsg input
    endfor
  endif
endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

alti.vimはctrlp.vimに不満があって作成されたプラグインだけあって,拡張の作成はctrlp.vimと似た雰囲気になります. しかし,ctrlp.vimよりパワフルな拡張を作ることが可能になっています. alti.vimは入力することに重きを置いたプラグインという雰囲気です.

上記コードにおいて,候補の取得とフィルタリングは s:cmpl() が,アクションは s:submitted() が担当しています. 他の候補絞り込み型プラグインと異なり,s:cmpl() で入力に対する候補の絞り込みを実装する必要があります. alti.vim本体には4つのフィルタリング関数が(引数 context の辞書関数として)用意されており,それぞれ

  • 前方一致:a:context.filtered()
  • 後方一致:a:context.backward_filtered()
  • 部分一致:a:context.partial_filtered()
  • 曖昧一致:a:context.fuzzy_filtered()

となっています. もちろん,自前でフィルタリングを実装してもよいですが,基本的には上記4つのいずれかを用いることで解決できるでしょう.

s:submitted() において,カーソル下にあった候補は, a:context.selection に格納され,入力された候補は a:context.inputs にリストとして格納されています. alti.vimでは,スペース区切りで候補の入力が認識されるようになっていて,複数の候補を選択する場合は,スペース区切りで入力することになります. <Tab> を入力することで,候補窓のカーソル下の候補が入力欄に挿入されるので,ユーザとしては,

  1. 絞り込みクエリを入力し,候補を絞り込む
  2. 候補窓でカーソル移動をして,候補を <Tab> で完全に入力欄に入力する
  3. <CR> で選択を終え,アクションを実行する

という流れになると思います. alti.vim的には,ユーザが <CR> でカーソル下の単語を選択してアクションを実行するのではなく, <CR> によって,入力された候補を送信することが想定されていると思われます. したがって, a:context.selection は基本的にアクション側で見る必要はないでしょう(ただし,受理できる入力が無かった場合は,カーソル下の候補に対してアクションを行うというのもアリかもしれません). なお,入力された候補は,候補窓からは除外されるようになっているので,重複選択の恐れはありません. 手動で重複した入力を与えたとしても,候補から除外されます.

入力欄に入力されたもののうち,元々の候補群になかったものは除外されて a:context.inputs に格納されます. つまり,ユーザがでたらめな入力をしたとしても,それらは全て除外されるということです.

なお,上記コード中の辞書 s:define の中で,値が関数名の文字列となっているものは,関数参照を取ることも可能です. 関数名を渡すより,関数参照を渡した方がコードとしてはシンプルになるのではないかと思います.

autoload/milqi/exts.vim : vim-milqiの拡張

let s:save_cpo = &cpo
set cpo&vim


let s:define = {'name': 'exts'}

function! milqi#exts#define() abort
  return s:define
endfunction

function! s:define.init(context) abort
  " let context.i = 0
  return ['apple', 'banana', 'cake']
endfunction

function! s:define.accept(context, candidate) abort
  call milqi#exit()
  echomsg a:candidate
endfunction

" function! s:define.lazy_init(context, query) abort
"   let a:context.i += 1
"   if query ==# ''
"     return {
"           \ 'reset': 0,
"           \ 'candidates': map(['lazy_apple', 'lazy_banana', 'lazy_cake'],
"           \   'v:val . " - " . a:context.i')
"           \}
"   else
"     return {
"           \ 'reset': 1,
"           \ 'candidates': ['apple_query', 'banana_query', 'cake_query']
"           \}
"   endif
" endfunction

" function! s:define.async_init(context) abort
"   let a:context.i += 1
"   return {
"         \ 'done': a:context.i < 6 ? 0 : 1,
"         \ 'candidates': map(['async_apple', 'async_banana', 'async_cake'],
"         \   'v:val . " - " . a:context.i')
"         \}
" endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

辞書関数ばかりとなっていますが,コードの雰囲気は,ctrlp.vimと似ていますね. 今回の例では活用していませんが(例が悪いのは申し訳ありません),vim-milqiの一番の特徴は, query-firstcandidate-first という2つのインタラクションモードです. 上記コード中では s:define.lazy_inits:define.async_init が,それらのインタラクションモードでのアクションに関係しています この2つのインタラクションモードは,vim-milqiのセールスポイントを語る上では欠かせない要素です. 以下,2015年3月29日のLingrの過去ログからの引用です.

query-firstは入力に応じて動的に内容が変化する系,candidate-firstは単なる非同期ctrlp系ですね candidate-firstだと,例えば時間のかかる処理で候補の取得に時間のかかるもの向けで考えていて query-firstだと,例えば過去の選択状態に応じて最初に一部の候補をキャッシュから出し,入力があれば入力に対応した候補を動的に生成して出すということができます candidate-firstはuniteやctrlpを考えていただければイメージは掴みやすいと思います query-firstは,こっちが私のやりたいメインですが,例えば初期表示でa, bを表示,hogeと入力したときにc, d, eを表示,hogeを消してfugaと入力したときにf, g, h, iを表示,ということができます

candidate-first

candidate-first は,unite.vimにもある「非同期の候補の取得ができるもの」といえるでしょう. candidate-firstモードとして利用する場合, milqi#candidate_first() に拡張の定義辞書を渡します. この辞書のキー async_init に関数参照が指定されていれば, updatetime ハックにより,定期的にその関数がコールされます. async_init に指定された関数は,donecandidates をキーに持つ辞書を返却する必要があります. キーの名前の通り, done は非同期処理が終了したかどうかを, candidates は非同期で取得した候補を表します. candidate に指定したリストは既にある候補群に追加されるので,拡張を作成する側が候補リストの要素に追加するといった処理は不要です. done1 を指定したとき,非同期処理は完了したものとして,関数を定期的なコールが終了します.

query-first

query-first は,入力に応じて,候補を動的に生成する,といったものらしいです. query-firstモードとして利用する場合, milqi#candidate_first() に拡張の定義辞書を渡します. この辞書のキー lazy_init に関数参照が指定されていれば, updatetime ハックにより,定期的にその関数がコールされます. ここまでは,candidate-firstモードと変わらないですが, lazy_init に指定した関数は第二引数に入力クエリを受け取ることができます. そして,返り値は resetcandidates をキーに持つ辞書を返却しなくてはなりません. candidates に指定したリストが候補群に追加されていくのは,candidate-firstモードの async_init と同様ですが, reset1 を指定することで,候補群を(ただの init で返却した候補リストも含めて)全て除去することができます. 入力クエリと reset を活用して,候補を動的に変更するのが,query-firstモードの使い方といえるでしょう.

単純な例の場合

今回のような単純な例(拡張辞書に lazy_initasync_init のどちらも持たない例)では,どちらのモードでも(すなわち, milqi#query_first()milqi#candidate_first のどちらに拡張の定義辞書を渡しても),動作に違いは無いでしょう.

なお,上記コード中のコメントアウト部分を解除し, plugin/exts.vimvim-milqiに関する部分を適切に変更することで,candidate-first , query-firstのそれぞれを体験することができるようにしています. candidate-firstの場合は,関数が5回コールされるまで,候補が増えるようにしています. query-first の場合,何か入力すると,候補が apple_querybanana_querycake_query の3つだけになるようにしています.

感想

この記事では5つの候補絞り込み型インターフェースを提供するプラグインについて,簡単に説明しました. どれもざっと見た限りでは「候補絞り込み型」という風にまとめられますが,それぞれが実現したいことや,目指しているものが少しずつ違っていて面白いですね.

unite.vimは初めから拡張を見越して作られた分,第三者にとってはかなり自由かつパワフルに拡張を作ることができるようになっています. また,作成したsourceの呼び出しは, :Unite のサブコマンド,すなわち第一引数にsource名を与える形になっているので,source作成者が plugin/ 下のファイルにコマンドを定義しなくてよいのも魅力です. :Unite のオプション引数である -auto-preview などは,uniteにしかできないことでしょう.

ctrlp.vimは元々がファイラであり,拡張が後付けのようなものであるため,作ることのできる拡張にやや不自由があるのは否めません. 例えば,複数選択がファイルを開くという操作に限定されるので,第三者にとっては利用しにくいものでしょう. しかし,コンパクトな実装である分,unite.vimより初回の起動時間が短い点は評価できるでしょう.

fzfは外部コマンドとして利用するので,一時的にVimから出る形になります. その点が他の絞り込み検索型のプラグインと比べると異質なところではあります. しかし,tmux上であれば,画面分割を利用し,候補を表示することができたり,neovimであればneovimのターミナルエミュレーター上で起動するようにしていたり,とても面白い機能が実装されています.

alti.vimはctrlp.vimにインスパイアされて開発されたプラグインだけあって,細かい部分での配慮が行きとどいていますね. 本体から前方一致,後方一致,部分一致,ファジーマッチという4つの絞り込み関数が提供されているのも,なかなかよいと感じました. ctrlp.vimはファジーマッチしかできないようになっており,僕としては多数の候補のフィルタリングに苦労することがありました.

vim-milqiは,unite souceから候補を取得できるようにしているのは画期的な機能だと思いました. また,非同期処理を主眼に置いており,他の絞り込み検索プラグインには無い特徴を感じ取ることができました.

おわりに

絞り込み検索型プラグインの拡張を作ることは難しくありません. この記事で紹介したような,たった十数行程度のコード(ほとんどが定型句のようなもので,本質的な部分は数行程度)を書くだけで,自作のプラグインから絞り込み検索型プラグインのUIを利用することができます. もし,これを読んでいるあなたがプラグイン作成の初心者であり,unite sourceやctrlp.vimの拡張を書いたことが無いのであれば,この機会に挑戦してみるのはいかがでしょうか?

なお,unite.vimは日本での知名度がとても高く,ググれば多数の情報が出てくると思います. unite.vimは高機能なため,この記事では紹介しきれていないので,unite.vimの本当の力を知るためにもググることをオススメします.

参考

Cygwinでclipboard機能を有効にしたVimをビルドする

はじめに

昔のCygwinVimではクリップボードが利用できず,Cygwinで用意されているクリップボードデバイス /dev/clipboard に対し,読み書きを行うことや, getclipputclip コマンドを利用することで,クリップボードとのやりとりを行っていた.

kanaさんのkana/vim-fakeclipというプラグインは,Cygwinや他の環境で +clipboard でないVimであっても,外部コマンドなどを用いることによって,クリップボードを何とか利用しようというものであり,かつてのCygwinVimユーザーは,よくこのプラグインを利用していたことだろう.

しかし,Vim 7.3.836以降,k_takataさんにより,CygwinVimでもクリップボード機能が利用できるようになった. 最近では,Cygwinにデフォルトで付属しているVimであっても,クリップボード機能を有効にしてビルドされており,利用することができる. だが,僕がCygwinVimを自前ビルドしたとき, +clipboard となっているのに,クリップボードが利用できなかった. この記事では,そのときにハマったことと,どう解決したかについて述べる.

長い前書きになったが,内容としては「こうやっただけ」というしょうもないものである. 結論を先に言うと, configure--enable-gui=no--without-x を指定しただけである.

CygwinVimをビルドする

最近では,VimソースコードGitHubで管理されているようになっているので,GitHubからソースを落としてくる.

$ git clone https://github.com/vim/vim.git

リポジトリのルートディレクトリに configure が用意されているので,多くのソフトウェアと同じく,

$ ./configure
$ make
$ make install

の順で実行するとよい.

configure については,適当に情報を収集し,以下のようにオプションを追加し,ビルドした.

$ ./configure \
  --prefix=/usr/local/ \
  --enable-fail-if-missing \
  --enable-gui=yes \
  --enable-multibyte=yes \
  --enable-perlinterp=yes \
  --enable-pythoninterp=yes \
  --enable-rubyinterp=yes \
  --enable-tclinterp=yes \
  --enable-luainterp=yes \
  --enable-cscope=yes \
  --enable-gpm \
  --enable-cscope \
  --enable-fontset \
  --with-features=huge \
  --with-x \
  --with-compiledby=koturn && \
  make -j5 && \
  make install

しかし,この設定では +clipboard となったが,クリップボードが利用できなかった. そのときに,ざっと調べたときに見た情報から, --with-x--enable-gui=yes が余計なのではないかと思った. そこで,--with-x の代わりに --without-x を, --enable-gui=yes の代わりに --enable-gui=no を指定して,再度 configure を行いビルドした(1度 configure を行った場合, rm src/auto/config.cache をしておく必要がある).

$ ./configure \
  --prefix=/usr/local/ \
  --enable-fail-if-missing \
  --enable-gui=no \
  --enable-multibyte=yes \
  --enable-perlinterp=yes \
  --enable-pythoninterp=yes \
  --enable-rubyinterp=yes \
  --enable-tclinterp=yes \
  --enable-luainterp=yes \
  --enable-cscope=yes \
  --enable-gpm \
  --enable-cscope \
  --enable-fontset \
  --with-features=huge \
  --without-x \
  --with-compiledby=koturn && \
  make -j5 && \
  make install

これで自前ビルドのCygwinVimであっても,クリップボード機能が利用可能となった. 明示的に --without-x--enable-gui=no を指定しなかった場合でも,クリップボードが有効にならなかったので,ちゃんと指定しなくてはならない.

余談

CFLAGSLDFLAGS に,自分なりの最適化オプションを追加してもビルドできるか?ということが気になったので,

$ ./configure \
  --prefix=/usr/local/ \
  --enable-fail-if-missing \
  --enable-gui=no \
  --enable-multibyte=yes \
  --enable-perlinterp=yes \
  --enable-pythoninterp=yes \
  --enable-rubyinterp=yes \
  --enable-tclinterp=yes \
  --enable-luainterp=yes \
  --enable-cscope=yes \
  --enable-gpm \
  --enable-cscope \
  --enable-fontset \
  --with-features=huge \
  --without-x \
  --with-compiledby=koturn \
  CFLAGS='-Ofast -m64 -march=native -mtune=native -flto -funroll-loops -DNDEBUG' \
  LDFLAGS='-flto' && \
  make -j5 && \
  make install

としてビルドしてみた. しかし,-flto が何かの悪さをしているらしく,出来上がったバイナリを実行すると,Vimが立ち上がった瞬間にエラーが出た. -flto を外すと,エラー無く,無事に実行できた.

まとめ

CygwinVimconfigure--enable-gui=no --without-x を指定しないと,クリップボードが有効にならない.

参考

Vimからfzfを利用する

はじめに

fzfとは,percolpecoと同様,絞り込みの検索を行うことのできるコマンドラインツールである. 日本では,percolやpecoが有名で,fzfはあまり有名ではないが,海外では有名であるらしい. fzfはVimから利用できるように,公式のリポジトリAPIを提供するVimプラグインが付属している. この記事では,fzfをVimから使う方法について述べる.

fzfの特徴

fzfはGoで実装されている. Goで実装されているから,マルチプラットフォームなのかと思ってしまうが,実は Windowsでは利用することができないCygwin上では利用可能).

fzfを利用したプラグインの作り方

正直,fzfのREADME.mdwikiのサンプルを見るのが早いのだが,それではこの記事の意味が無いので,ちゃんと書く. (日本語で書いておくと,日本人が読みやすいという利点もあるだろうし)

まず,fzfをVimから利用するためには,fzfが提供しているプラグインruntimepath を通す必要がある. これはfzfをクローンした場所にもよるが,

$ git clone https://github.com/junegunn/fzf.git ~/.fzf

としたならば,

set rtp+=~/.fzf

とするだけでよい. すなわち,fzfのルートディレクトリを runtimepath に追加するだけでよい. 上記の場合,fzfのVimプラグイン本体は ~/.fzf/plugin/fzf.vim にあるので,Vimからfzfの関数を利用できるようになるわけだ.

fzfをVimから利用するのは簡単で, fzf#run() に渡す適切な辞書を定義するだけだ. 後は,その辞書を第一引数とし, fzf#run() をコールするだけで,Vimからfzfを利用できる. 例えば,wikiには,

nnoremap <silent> <Leader>C :call fzf#run({
    \ 'source': map(split(globpath(&rtp, "colors/*.vim"), "\n"),
    \         "substitute(fnamemodify(v:val, ':t'), '\\..\\{-}$', '', '')"),
    \ 'sink': 'colo',
    \ 'options': '+m',
    \ 'left': 30
    \})<CR>

というfzfを用いるキーマッピングの例が紹介されている. これは,利用可能なcolorschemeを候補として表示し,選択したcolorschemeに変更するというものだ.

この fzf#run() に渡す辞書のキーと値には以下のようなものが有効である. 同じキーであっても,値の型によって挙動が異なる点に注意しよう.

  • source
    • 値の型が 文字列 のとき,その文字列を外部コマンドとして実行し,その結果をfzfに渡す
    • 値の型が リスト のとき,このリストをfzfに渡す
  • sink
    • 値の型が 文字列 のとき,選択した候補に対してその文字列をVimコマンドとして実行する
      • 候補は文字列の末尾に結合される(例:値が 'edit' ならば,実行されるコマンドは 'edit [選択したもの]'
    • 値の型が 関数参照 のとき,選択した候補を指定した関数(への参照)の第一引数として渡し,関数をコール
      • 指定する関数参照の引数は1つでなければならない
    • 複数の候補が選択されたとき,それぞれの候補に対して, sink で指定したアクションを1回1回実行する
  • sink*
    • 値の型は 関数参照 のみで,選択した候補を指定した関数(への参照)の第一引数として渡し,関数をコール
    • 複数選択された候補を リストとして扱う
      • すなわち,選択された候補はリスト化されて,関数の第一引数に渡される
  • options
    • 値の型は 文字列 のみで,fzfの起動オプションを指定する
  • dir
    • 値の型は 文字列 のみで,fzf起動時の作業ディレクトリを指定する
  • updownleftright
    • 値の型は 数値 または 文字列
      • 数値型では行数,または列数を指定できる( 20 など)
      • 文字列型では行,または列の占める割合を指定できる( '50%' など)
      • 文字列型の場合,末尾に % を付けないと,行数,または列数の指定になる( '50' では50行,または50列で50%でない )
    • tmux上でVimを起動している場合のみ
  • window
    • neovimのみ
    • 値の型は 文字列 のみで,fzfのウィンドウを開くコマンドを指定する(例: vertical aboveleft 30new
  • launcher
    • gvimのみ
    • 値の型が 文字列 のとき,fzfを起動する外部のターミナルエミュレータ名を指定する
    • 値の型が 関数参照 のとき,その関数の返り値をfzfを起動する外部のターミナルエミュレータとして用いる

キーとして必ず持つべきものは,候補に対するアクションを担当する sink または sink* である. 候補の取得を担当する source は,後述するが,ほぼ必須の項目である. なお, sinksink* の2つがある場合, sinksink* の順番に処理がなされる.

ちなみに, source を指定しない場合,デフォルトのfzfの動作(再帰的なファイル検索)になるので,単純にファイルを検索したいのであれば, source を敢えて指定しないというのも手だ. 単純なファイラとして用いるのであれば,以下のようにするとよいだろう.

call fzf#run({'sink': 'edit'})

sink も指定しなかった場合,候補を選択しても何のアクションも行われない.

" カレントディレクトリ以下のファイルが再帰的に表示されるが,
" 候補を選択しても,何のアクションも行われない
call fzf#run({})

プラグインに組み込む

僕の場合,fzfをプラグインに導入する場合,実装を別ファイルに分割している. unite.vimctrlp.vimに習い,分割したファイルは autoload/fzf/ に配置することにする.

以下は,カレントディレクトリのファイルを表示し,選択されたファイルの行数を echo するという単純なサンプルである.

  • plugin/sample.vim
if exists('g:loaded_sample')
  finish
endif
let g:loaded_sample = 1
let s:save_cpo = &cpo
set cpo&vim

command! FZFSample  call fzf#run(fzf#sample#option())

let &cpo = s:save_cpo
unlet s:save_cpo
  • autoload/fzf/sample.vim
let s:save_cpo = &cpo
set cpo&vim

function! fzf#sampleoption() abort
  return {
        \ 'down': 20,
        \ 'sink': function('s:sink'),
        \ 'source': s:gather_candidates()
        \}
endfunction

function! s:gather_candidates() abort
  return filter(split(globpath('.', '*', 1), "\n"), 'filereadable(v:val)')
endfunction

function! s:sink(candidate) abort
  echo len(readfile(a:candidate))
endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

autoload/fzf/sample.vimfzf#sample#option() で返却する辞書の一部の項目は,いちいち新しく定義し直す必要は無いので,以下のように記述してもよいだろう.

let s:save_cpo = &cpo
set cpo&vim

let s:option = {
      \ 'down': 20
      \}
function! s:option.sink(candidate) abort
  echo len(readfile(a:candidate))
endfunction


function! fzf#sample#option() abort
  let s:option.source = s:gather_candidates()
  return s:option
endfunction


function! s:gather_candidates() abort
  return filter(split(globpath('.', '*', 1), "\n"), 'filereadable(v:val)')
endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

この例では,候補の取得をVim Script側で行っているが,カレントディレクトリのファイルを検索するのなら,外部コマンドを用いてもよいだろう. fzfがコマンドラインツールであることを考えれば,そちらの方が好ましいかもしれない.

let s:save_cpo = &cpo
set cpo&vim

let s:option = {
      \ 'down': 20,
      \ 'source': 'find -maxdepth 1 -find f'
      \}
function! s:option.sink(candidate) abort
  echo len(readfile(a:candidate))
endfunction


function! fzf#sample#option() abort
  return s:option
endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

複数選択したい場合

先述の例では触れていないが,fzfの拡張は複数選択できるようにすることが可能である. 複数選択はfzf自体の機能にあるので, options にfzfのオプション -m ,または --multi を指定するだけでよい. 先述の例であれば,以下のように s:option を変更するだけだ.

let s:option = {
      \ 'down': 20,
      \ 'options': '-m'
      \}
function! s:option.sink(candidate) abort
  echo len(readfile(a:candidate))
endfunction

このとき, sink に指定したアクションが,選択した各候補に対して実行される. もし,複数選択したものを,Vim Script側ではリストとして扱いたいならば, sink* を利用するとよい.

function! s:sink(candidates) abort
  for candidate in a:candidates
    echo len(readfile(candidate))
  endfor
endfunction

let s:option = {
      \ 'down': 20,
      \ 'options': '-m',
      \ 'sink*': function('s:sink')
      \}

感想

fzfはあくまで外部ツールであって,Vimからfzfに操作を移さなければならない点が残念に思えた. 僕としては,外部ツールはあくまでシームレスに用いることができるようにしてほしいと思っている. ただ,tmux上のVimで用いた場合に,候補選択用の新しいペインを作成する点や,neovimに対応している点など,なかなか面白い試みをしていると感じた.

また,複数選択に対応している点も魅力的だと感じた. 候補選択型インターフェースで,ユーザが作成する拡張で,複数選択ができるものを作ることができるのは,実質unite.vimとfzfぐらいのものではないかと思われる(ctrlp.vimは特殊な場合に限り,複数選択可能).

参考

ctrlp.vimのエクステンションの作り方

はじめに

この記事はctrlpvim/ctrlp.vimのエクステンションの作り方についての記事であり,ctrlpの公式リポジトリのextensionsブランチに書かれているエクステンションの作り方,および以下の3つの記事の内容に個人的な知見を追加したものである.

CtrlPとは

ctrlpvim/ctrlp.vimは候補絞り込み検索型のファイラである. Shougo/unite.vimと双璧を成すプラグインとして,とても有名である.

オリジナルはkien氏のものであるが,kien氏と連絡が取れないことから,それをforkしたctrlpvim/ctrlp.vimが誕生した.

ctrlp.vimは標準機能であるファイラだけではなく,候補絞り込み検索のインタフェースを利用して,ctagsのタグやundo履歴を候補として表示し,選択された場合にそれに応じたアクションが実行される拡張が本体に付属している.

このctrlp.vimの拡張は,第三者が作成することもできる. unite.vimの様々な拡張(unite source)が多数の人により開発され公開されているように,ctrlp.vimの拡張(以後,エクステンションと呼ぶ)も多数の人により開発され公開されている.

ctrlp.vimのエクステンションは,unite.vimのsourceほど自由に作ることはできないが,シンプルかつ簡単に作ることが可能な点が魅力であると思っている.

2015 / 12 / 01 追記

kien氏と連絡が取れ,2015年11月30日にkien氏のリポジトリではメンテナンスされていないということがオリジナルの方のREADME.mdに明記された

エクステンションの作り方

g:ctrlp_ext_vars というリストを保持するグローバル変数があり,このグローバル変数に作成するエクステンションに関する辞書を追加するだけで,エクステンションの作成は終わりである. そして,関数 ctrlp#init() に適切なインデックスを渡すことで,作成したエクステンションを利用することができる.

ただし,作成するエクステンションは,1つのファイルにし, autoload/ctrlp/sample.vim に置く. リスト g:ctrlp_extensions にエクステンションを登録してある場合,ctrlp.vim本体の読み込み時に,ctrlp.vim本体が runtime autoload/ctrlp/sample.vim として読み込みを行うので,エクステンションのファイルは autoload/ctrlp/ 以下に置かなければならない.

何はともあれ,実例を見てもらうとしよう. 以下は,カレントディレクトリのファイルを表示し,選択されたファイルの行数を echo するというシンプル(かつ何の役にも立たない)なエクステンションのサンプルである.

  • plugin/sample.vim
if exists('g:loaded_sample')
  finish
endif
let g:loaded_sample = 1
let s:save_cpo = &cpo
set cpo&vim

command! CtrlPSample  call ctrlp#init(ctrlp#sample#id())

let &cpo = s:save_cpo
unlet s:save_cpo
  • autoload/ctrlp/sample.vim
if exists('g:loaded_ctrlp_sample') && g:loaded_ctrlp_sample
  finish
endif
let g:loaded_ctrlp_sample = 1
let s:save_cpo = &cpo
set cpo&vim

let s:sample_var = {
      \ 'init': 'ctrlp#sample#init()',
      \ 'accept': 'ctrlp#sample#accept',
      \ 'lname': 'sample',
      \ 'sname': 'sample',
      \ 'type': 'path',
      \ 'nolim': 1
      \}
if exists('g:ctrlp_ext_vars') && !empty(g:ctrlp_ext_vars)
  let g:ctrlp_ext_vars = add(g:ctrlp_ext_vars, s:sample_var)
else
  let g:ctrlp_ext_vars = [s:sample_var]
endif
let s:id = g:ctrlp_builtins + len(g:ctrlp_ext_vars)


function! ctrlp#sample#id() abort
  return s:id
endfunction


function! ctrlp#sample#init() abort
  return filter(split(globpath('.', '*', 1), "\n"), 'filereadable(v:val)')
endfunction

function! ctrlp#sample#accept(mode, str) abort
  call ctrlp#exit()
  echo len(readfile(a:str))
endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

先に述べたように,ctrlp.vim本体が runtime autoload/ctrlp/sample.vim として,作成したエクステンションを読みにいくので,autoloadファイルであっても,ファイルの先頭に二重読み込み防止の記述をしておく必要がある.

このサンプルは,koturn/ctrlp-sampleにあるので,インストールして動作を確認したり,改造したりして,ctrlp.vimのエクステンションの作り方の勉強に役立てていただきたい. なお,masterブランチのものは,後述する僕の好みに合わせてコードを洗練してあるので,上記のコード例そのままのものが欲しいなら,not-refiedブランチのものを参照していただきたい.

g:ctrlp_ext_vars に追加する辞書のそれぞれのキー

エクステンションの作成において,要となるのが, g:ctrlp_ext_vars に追加する辞書である. ここでは,その辞書が持つべきキーについて述べる.

必須

  • init : 文字列(evalすると関数呼び出しになる文字列)
    • 評価されるべき関数呼び出しの文字列( 'ctrlp#sample#init()' など)
    • 候補の取得に用い,関数から候補のリストを返却する
    • unite.vimでいうところの gather_candidates
    • CtrlPのバッファに移動した後に呼び出される
  • accept : 文字列(候補が選択されたときに呼び出される関数名)
    • 選択した候補に対するアクションを定義した関数の名前を渡す
    • initenterexit とは異なり, 関数呼び出しの括弧は不要
    • ここで指定した関数のAPIctrlp#sample#accept(mode, str) といった形にすること(仮引数名は何でもよいが,本体付属のエクステンションに従った)
      • mode : 候補の選択に入力したキーに応じた文字列が入っている.対応は以下の通り
        • <CR> : 'mode' は 'e'
        • <C-v> : 'mode' は 'v'
        • <C-t> : 'mode' は 't'
        • <C-x> : 'mode' は 'h'
      • str : 選択した候補
    • 通常,関数の先頭で ctrlp#exit() をコールするとよい
    • mode に応じて処理を変更しているエクステンションは少ない
  • lname : 文字列(省略無しのエクステンション名 - long name)
    • ステータスラインに表示される省略無しのエクステンション名
  • sname : 文字列(省略したエクステンション名 - short name)
    • ステータスラインに表示される省略されたエクステンション名
  • type : 文字列で 'line''path''tabs''tabe' のいずれか
    • 'line'
      • それぞれの候補に対し,末尾まで入力文字列とのマッチを行う
    • 'path'
      • それぞれのファイルパスのような形式の候補に対し,末尾まで入力文字列とのマッチを行う
      • それぞれの候補について,入力にマッチした部分がハイライト される
    • 'tabs' (tab start)
      • 最初のTAB文字までマッチする
      • 候補の文字列の最初のTAB以降を,ユーザに情報として提示したい場合に用いる(候補に関するヒントの提示)
    • 'tabe' (tab end)
      • 最後のタブ文字までマッチする
      • 用途は 'tabs' と同じで,どのTABまでをマッチ対象にするかが異なる

任意

  • enter : 文字列(evalすると関数呼び出しになる文字列)
    • CtrlPが起動する前に呼び出される処理
    • 元のバッファの filetype の取得することなどに用いる
  • exit : 文字列(evalすると関数呼び出しになる文字列)
    • CtrlPが終了する前に呼び出される処理
  • opts : 文字列(evalすると関数呼び出しになる文字列)
    • オプションの設定などに用いるらしい
    • enter で指定した関数より前に呼び出される
  • sort : 数値( 01
    • 候補をソートするか否か
    • 0 なら候補をソートしない(デフォルト), 1 なら候補をソートする
  • specinput : 数値( 01 ) - special input
    • ..@cd といった特殊な入力を可能にする
      • :h ctrlp-input-formats を参照
    • 0 なら特殊入力を不可に(デフォルト), 1 なら特殊入力を可能にする
    • ..../ と等価で,1つドットを増やす度にディレクトリ階層を1つ上にした入力として解釈され,リターンキーを押すと,ワーキングディレクトリを変更し,再度 init に指定した関数が呼び出され,候補が更新される
      • ... なら ../../.... なら ../../../ に移動する
    • @cd はワーキングディレクトリを変更する
      • 候補選択時に @cd ./plugin/ と入力して,リターンキーを押すと,ワーキングディレクトリを ./plugin/ に変更し,再度 init に指定した関数が呼び出され,候補が更新される
    • CtrlP起動中にワーキングディレクトリを変更したい場合に用いる
    • 候補の絞り込みに用いるのではない
    • ワーキングディレクトリが候補の取得に関わっているものに有効

ドキュメント化されていないもの(任意)

  • nolim : 数値( 01 ) - no limit
    • 0 なら制限あり(デフォルト), 1 なら無制限(スクロール可能)に
    • 日本語などの検索しづらい候補があるときに用いると便利かもしれない
    • mattnさんのctrlpのエクステンションに多く用いられているのを見掛ける
  • opmul : 数値( 01 ) - open multiple
    • 0 なら複数選択不可(デフォルト), 1 なら複数選択可能に
    • 掛け算記号のことではない
    • 複数選択は候補上で <C-z> と入力することで候補にマークを付け, <C-o> を入力することで行うことができる
    • ただし, 複数選択のアクションは "基本的に" 定義できず,ファイルを開くという動作のみ である
      • 存在するファイルのパスに限定されるが,autocmdBufReadCmd を用いることで強引にアクションを定義することは可能(危険)
    • 候補はファイルパスとして解釈され,存在しないファイルパスである候補は選択しても除外される
    • 基本的に定義しない.本体付属のプラグインに用いられている
  • wipe : 文字列(関数名)
    • <F7> で候補を除去するときのアクションを指定する
      • opmul1 のとき,<C-z> でマークを付け,<F7> でマークを付けた候補を指定した関数に渡す
      • マークが1つも付いていない場合,<F7> で全ての候補を除去するかどうかのメッセージが表示される
        • o を入力すると,指定した関数に空リストを渡してコールし,その後ctrlpバッファに戻る
        • c を入力すると,候補を除去せず,ctrlpバッファに戻る
    • initenterexit とは異なり, 関数呼び出しの括弧は不要
    • ここで指定した関数のAPIctrlp#sample#wipe(entries) といった形にすること(仮引数名は何でもよいが,本体付属のエクステンションに従った)
    • 新たな候補リスト (引数の除去対象が除去されたリストが想定されている)を関数から返却する
    • 基本的に定義しない.本体付属のプラグインに用いられている

opmulwipe を用い,指定した候補を除去したいのであれば,以下のようにするのがよいだろう.

function! ctrlp#sample#init() abort
  " 候補を他の関数で用いるために,スクリプトローカル変数で保持しておく
  " この s:candidates は ctrlp#sample#exit() でunletするとよい
  let s:candidates = filter(split(globpath('.', '*', 1), "\n"), 'filereadable(v:val)')
  return s:candidates
endfunction

function! ctrlp#sample#wipe(entries) abort
  return empty(a:entries) ? [] : filter(s:candidates, 'index(a:entries, v:val) == -1')
endfunction

上記の ctrlp#sample#wipe() が分かりにくいのであれば,以下のようにforを用いて書くとよい.

function! ctrlp#sample#wipe(entries) abort
  if empty(a:entries)
    return []
  endif
  for entry in a:entries
    let idx = index(s:candidates, entry)
    if idx != -1
      call remove(s:candidates, idx)
    endif
  endfor
  return s:candidates
endfunction

前者の filter() を用いたものと比べると, a:entriess:candidates の捉え方(ループの主体)が異なっているが,候補に重複が無ければ同一の結果が得られるはずだ. (前者は除去候補にマッチしたものを候補から全て取り除き,後者は候補のうち,最初にマッチしたもののみを取り除く)

Vim scriptのパースは時間がかかる(ループ毎にもパースを行っているはず)ため, s:candidates の数が多くないのであれば,前者の方が高速に動作すると思われる.

CtrlP起動時にワーキングディレクトリを変更する

ctrlp#init() に作成したエクステンションのIDを渡すことで,CtrlPを起動するようになっていることは最初に述べた. 実は, ctrlp#init() は,第二引数に辞書を取ることもでき,その辞書のキー dir の値に,ワーキングディレクトリを指定することで,CtrlP起動時のディレクトリを変更することができる. これは相対パスからファイルを簡単に辿りたい場合などに使用できる.

command! -nargs=1 -complete=dir CtrlPSample  call ctrlp#init(ctrlp#sample#id(), {'dir': <q-args>})

候補のシンタックスハイライト

本体付属のエクステンションやこの記事を参考にすると,CtrlPの候補にハイライトを適用する(すなわち,CtrlPバッファ内でハイライトを行う)には, ctrlp#sample#init() 内で,

function! ctrlp#sample#init() abort
  call ctrlp#hicheck('CtrlPSampleTabExtra', 'Comment')
  syntax match CtrlPSampleTabExtra '\zs\t.*$'
  return filter(split(globpath('.', '*', 1), "\n"), 'filereadable(v:val)')
endfunction

などのようにするとよい. 本体付属のエクステンションでは,シンタックスハイライト用の設定を行う部分を関数として切り出し,

function! ctrlp#sample#init() abort
  call s:syntax()
  return filter(split(globpath('.', '*', 1), "\n"), 'filereadable(v:val)')
endfunction

function! s:syntax() abort
  if ctrlp#nosy()
    return
  endif
  call ctrlp#hicheck('CtrlPSampleTabExtra', 'Comment')
  syntax match CtrlPSampleTabExtra '\zs\t.*$'
endfunction

としている( ctrlp#nosy() については後述する).

ctrlp#hicheck() はハイライトのリンクをチェックする関数であり,リンクが存在しないならば,リンクを行う. 実装は非常に単純で,

" ctrlpvim/ctrlp.vim の autoload/ctrlp.vim より引用
fu! ctrlp#hicheck(grp, defgrp)
    if !hlexists(a:grp)
        exe 'hi link' a:grp a:defgrp
    en
endf

となっている.

また, ctrlp#nosy() (no syntax) はシンタックスハイライトが利用できる環境であるかどうかを判別する関数である. シンタックス機能が利用できないVimや, :syntax enable をしていないVimならば 1 を返却し,シンタックスハイライトが可能ならば, 0 を返却する.

" ctrlpvim/ctrlp.vim の autoload/ctrlp.vim より引用
fu! ctrlp#nosy()
    retu !( has('syntax') && exists('g:syntax_on') )
endf

グローバル変数 g:syntax_on は, :syntax enable とコマンドを実行する(大抵,.vimrcに書かれているだろう)ことで定義される変数で, :syntax off とコマンドを実行すると unlet される. 大抵のユーザはシンタックス機能をオンにしているので,わざわざチェックする必要も無いが,本体付属のプラグインに沿った形に記述しておくと,雰囲気は良いだろう.

シンタックスハイライトの用途としては, typetabs を指定した場合,最初のタブ文字以降を目立たない色にするといったものが考えられる. 実は,先述のハイライトの例は,本体付属の typetabs であるエクステンションにも書かれている「タブ文字以降のハイライトをコメントと同じものにして,目立たなくする」ものである.

改良案

ここまではctrlp.vimのエクステンションの作り方について述べたが,ここからは「エクステンションのコードはこう書くのが好きだ」という個人の意見を書く.

ctrlp.vim本体のグローバル変数の参照

ctrlp.vimの拡張はプラグインとしてオプショナルであり,NeoBundle等で依存関係を明示したくない場合があるかもしれない(そんなユーザがいるのかは疑問であるが). このことについて考えているときに,以下の記事を見て,なるほど,と感じた.

ctrlp.vimのExtensionを書くときに:NeoBundleLazy autoloadを考慮してg:ctrlp_builtinsをそのまま使わない - cafegale

ctrlp.vimの拡張はあくまでオプショナルとしてプラグインに付属させており,NeoBundleに依存関係が記述されておらず,ctrlp.vimが遅延読み込みされるように設定してある場合,先述の記事にあるように,作成したプラグインを読み込みしている段階で g:ctrlp_builtins が定義されていないという問題が起こることもあるだろう. 従って,ctrlpが提供しているautoload関数 ctrlp#getvar() を用いて,ctrlp.vim本体の読み込みを発生させ,ctrlpのグローバル変数が読み込まれるようにした方が望ましいはずだ. そのことを踏まえ,作成するエクステンションのファイルの先頭に,

let s:ctrlp_builtins = ctrlp#getvar('g:ctrlp_builtins')

と記述し, g:ctrlp_builtins の代わりに, s:ctrlp_builtins を用いるようにするとよさそうだ.

ただ,プラグインマネージャである neobundle.vim のためだけの記述をするというのは,本質的ではなく,少し嫌な気持ちになる. この g:ctrlp_builtins の取得方法に関しては,個人の好みが大きいだろう. 正直,ユーザがNeoBundleの依存関係をキッチリ書けば問題の無い話でもある.

ちなみに, ctrlp#getvar() は以下のような実装になっている.

" ctrlpvim/ctrlp.vim の autoload/ctrlp.vim より引用
fu! ctrlp#getvar(var)
    retu {a:var}
endf

a:var は文字列と想定されるので,{a:var} は, eval(a:var) と読み替えてよく, a:val を評価した結果に置き換えられる. すなわち, ctrlp#getvar() は, autoload/ctrlp.vim 内で引数を評価した結果を返す関数であり,autoload/ctrlp.vimスクリプトローカル変数を取得することもできる.

g:ctrlp_ext_vars へのエクステンションの辞書の追加

Vimプラグインの拡張機能プラグインを作ってVimをさらに使いやすくしよう - 29th Sta. という記事の例では,

let s:sample_var = {
      \ 'init': 'ctrlp#sample#init()',
      \ 'accept': 'ctrlp#sample#accept',
      \ 'lname': 'sample extension',
      \ 'sname': 'sample',
      \ 'type': 'path',
      \ 'nolim': 1
      \}
if exists('g:ctrlp_ext_vars') && !empty(g:ctrlp_ext_vars)
  let g:ctrlp_ext_vars = add(g:ctrlp_ext_vars, s:sample_var)
else
  let g:ctrlp_ext_vars = [s:sample_var]
endif

としているが, add() は第一引数のリストに対して破壊的変更を加えるので,返却値を再び第一引数のリストに代入する必要はない(どちらが読みやすいかは置いておくとして). また,スクリプトローカル変数 s:sample_var を保持する必要もない. これらのことから,上記コードは以下のように記述することができる.

let s:sample_var = {
      \ 'init': 'ctrlp#sample#init()',
      \ 'accept': 'ctrlp#sample#accept',
      \ 'lname': 'sample extension',
      \ 'sname': 'sample',
      \ 'type': 'path',
      \ 'nolim': 1
      \}
if exists('g:ctrlp_ext_vars') && !empty(g:ctrlp_ext_vars)
  call add(g:ctrlp_ext_vars, s:sample_var)
else
  let g:ctrlp_ext_vars = [s:sample_var]
endif
unlet s:sample_var

個人的にはif文を用いずにシンプルに書きたいので,以下のように記述することにしている.

let g:ctrlp_ext_vars = add(get(g:, 'ctrlp_ext_vars', []), {
      \ 'init': 'ctrlp#sample#init()',
      \ 'accept': 'ctrlp#sample#accept',
      \ 'lname': 'sample extension',
      \ 'sname': 'sample',
      \ 'type': 'path',
      \ 'nolim': 1
      \})

add() の第一引数が即値 [] となることもあるので,この場合は add() の返却値を利用しなければならない.

可能な限り,グローバル関数を使用しないようにする

エクステンション側にグローバル関数を定義し,ctrlp.vimに渡す辞書にそのグローバル関数名を渡しているが,必ずしもそうする必要はない. スクリプトローカル関数であっても,スクリプト番号付きであれば,グローバルに参照することが可能だからだ.

function! s:hoge() abort
  echomsg 'Hello, World'
endfunction
" :call <SNR>18_hoge() などという形で,グローバルに呼び出すことが可能

このことを踏まえ, g:ctrlp_ext_vars に辞書を追加する記述を,以下のような記述に変更した.

function! s:get_sid_prefix() abort
  return matchstr(expand('<sfile>'), '\zs<SNR>\d\+_\zeget_sid_prefix$')
endfunction
let s:sid_prefix = s:get_sid_prefix()
delfunction s:get_sid_prefix

let g:ctrlp_ext_vars = add(get(g:, 'ctrlp_ext_vars', []), {
      \ 'init': s:sid_prefix . 'init()',
      \ 'accept': s:sid_prefix . 'accept',
      \ 'lname': 'sample extension',
      \ 'sname': 'sample',
      \ 'type': 'path',
      \ 'nolim': 1
      \})
let s:id = s:ctrlp_builtins + len(g:ctrlp_ext_vars)
unlet s:sid_prefix

僕は,不特定多数の相手(プラグインであったり,.vimrcであったり)にAPIを提供する場面では,グローバル関数を用い,今回のように作成したプラグインから 能動的に 他のプラグインに自身の関数を渡す場合では,スクリプトローカル関数を用いるとよいだろうと考えている. ただ,ここまでしてグローバル関数を嫌う必要があるのかと問われれば,どうなんだろうと思ってしまう. また,スクリプト番号の取得は,コードとしてややごちゃごちゃしており(5行は必要になる),グローバル関数を用いる方がスマートという気もするが,この場では気にしないことにする.

ここまでのことを踏まえ,最初に紹介したカレントディレクトリのファイルを表示し,選択されたファイルの行数を表示するエクステンションを以下のように書き直した.

if exists('g:loaded_ctrlp_sample') && g:loaded_ctrlp_sample
  finish
endif
let g:loaded_ctrlp_sample = 1
let s:save_cpo = &cpo
set cpo&vim

let s:ctrlp_builtins = ctrlp#getvar('g:ctrlp_builtins')

function! s:get_sid_prefix() abort
  return matchstr(expand('<sfile>'), '\zs<SNR>\d\+_\zeget_sid_prefix$')
endfunction
let s:sid_prefix = s:get_sid_prefix()
delfunction s:get_sid_prefix

let g:ctrlp_ext_var = add(get(g:, 'ctrlp_ext_vars', []), {
      \ 'init': s:sid_prefix . 'init()',
      \ 'accept': s:sid_prefix . 'accept',
      \ 'lname': 'sample',
      \ 'sname': 'sample',
      \ 'type': 'path',
      \ 'nolim': 1
      \})
let s:id = s:ctrlp_builtins + len(g:ctrlp_ext_vars)
unlet s:ctrlp_builtins s:sid_prefix


function! ctrlp#sample#id() abort
  return s:id
endfunction


function! s:init() abort
  return filter(split(globpath('.', '*', 1), "\n"), 'filereadable(v:val)')
endfunction

function! s:accept(mode, str) abort
  call ctrlp#exit()
  echomsg len(readfile(a:str))
endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

ctrlp.vimのエクステンションのテンプレート

僕はctrlp.vimのエクステンションを作成する際に,以下のテンプレートを用いている. <+FILEBASE+> を適宜,エクステンションのファイル名からディレクトリ部と,拡張子を取り除いたものにすればよい( :%s/<+FILEBASE+>/\=fnamemodify(expand('%'), ':t:r') ). これを thinca/vim-template などのテンプレートプラグインと組み合わせると,サクッとエクステンションを作ることができるはずだ.

なお,以下のテンプレートには,CtrlPバッファでハイライトするコードは含まれない. 僕の場合,ハイライトが必要なエクステンションを書くことがあまり無いので,必要に応じて追加するようにしているためだ.

if exists('g:loaded_ctrlp_<+FILEBASE+>') && g:loaded_ctrlp_<+FILEBASE+>
  finish
endif
let g:loaded_ctrlp_<+FILEBASE+> = 1
let s:save_cpo = &cpo
set cpo&vim

let s:ctrlp_builtins = ctrlp#getvar('g:ctrlp_builtins')

function! s:get_sid_prefix() abort
  return matchstr(expand('<sfile>'), '\zs<SNR>\d\+_\zeget_sid_prefix$')
endfunction
let s:sid_prefix = s:get_sid_prefix()
delfunction s:get_sid_prefix

let g:ctrlp_ext_var = add(get(g:, 'ctrlp_ext_vars', []), {
      \ 'init': s:sid_prefix . 'init()',
      \ 'accept': s:sid_prefix . 'accept',
      \ 'lname': '<+FILEBASE+>',
      \ 'sname': '<+FILEBASE+>',
      \ 'type': 'line',
      \ 'enter': s:sid_prefix . 'enter()',
      \ 'exit': s:sid_prefix . 'exit()',
      \ 'opts': s:sid_prefix . 'opts()',
      \ 'sort': 0,
      \ 'specinput': 0,
      \ 'nolim': 1
      \})
let s:id = s:ctrlp_builtins + len(g:ctrlp_ext_vars)
unlet s:ctrlp_builtins s:sid_prefix


function! ctrlp#<+FILEBASE+>#id() abort
  return s:id
endfunction


function! s:init() abort
  " Gather candidates
  let candidates = []
  return candidates
endfunction

function! s:accept(mode, str) abort
  call ctrlp#exit()
  " Write actions
endfunction

function! s:enter() abort
  " Called before s:init()
  " For example: get filetype
endfunction

function! s:exit() abort
  " Called when exit ctrlp
endfunction

function! s:opts() abort
  " Called before s:enter()
  " Set options etc...
endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

最小限の機能に絞ったテンプレートならば,以下のようになるだろう.

if exists('g:loaded_ctrlp_<+FILEBASE+>') && g:loaded_ctrlp_<+FILEBASE+>
  finish
endif
let g:loaded_ctrlp_<+FILEBASE+> = 1
let s:save_cpo = &cpo
set cpo&vim

let s:ctrlp_builtins = ctrlp#getvar('g:ctrlp_builtins')

function! s:get_sid_prefix() abort
  return matchstr(expand('<sfile>'), '\zs<SNR>\d\+_\zeget_sid_prefix$')
endfunction
let s:sid_prefix = s:get_sid_prefix()
delfunction s:get_sid_prefix

let g:ctrlp_ext_vars = add(get(g:, 'ctrlp_ext_vars', []), {
      \ 'init': s:sid_prefix . 'init()',
      \ 'accept': s:sid_prefix . 'accept',
      \ 'lname': '<+FILEBASE+>',
      \ 'sname': '<+FILEBASE+>',
      \ 'type': 'line',
      \ 'nolim': 1
      \})
let s:id = s:ctrlp_builtins + len(g:ctrlp_ext_vars)
unlet s:ctrlp_builtins s:sid_prefix


function! ctrlp#<+FILEBASE+>#id() abort
  return s:id
endfunction


function! s:init() abort
  " Gather candidates
  let candidates = []
  return candidates
endfunction

function! s:accept(mode, str) abort
  call ctrlp#exit()
  " Write actions
endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

基本的にこれらのテンプレートの s:init()s:accept() を中心に実装するだけでよいはずだ.

最後に

ctrlp.vimのエクステンションを作るにあたって覚えなければならないことは,unite.vimより少なく,unite sourceより簡単に作ることができる. 日本人が作ったプラグインには,unite sourceが付属しているものが多いが,ctrlp.vimのエクステンションが付属しているプラグインは少ない. unite sourceを付属させるのであれば,是非ともctrlp.vimのエクステンションも付属させてみてはどうだろうか?

追記

2015 / 12 / 01

何とあのmattnさんにこの記事を取り上げていただいた!

当初は割とパパッと書いた記事で,恥ずかしい間違いなどもあったのだが,まさかmattnさんに紹介していただけると思っていなかった.

mattnさんはブログ中で, g:ctrlp_ext_vars に追加するときに if ~ else を用いるか,get() を用いるかについて堀下げて解説してくださっている. 特に,get() の第一引数に g: という一見変数っぽくないものを与えるあたりについて解説してくださっている.

let s:sample_var = {
      \ 'init': s:sid_prefix . 'init()',
      \ 'accept': s:sid_prefix . 'accept',
      \ 'lname': 'sample extension',
      \ 'sname': 'sample',
      \ 'type': 'path',
      \ 'nolim': 1
      \}
if exists('g:ctrlp_ext_vars') && !empty(g:ctrlp_ext_vars)
  call add(g:ctrlp_ext_vars, s:sample_var)
else
  let g:ctrlp_ext_vars = [s:sample_var]
endif
unlet s:sample_var

とするより,

let g:ctrlp_ext_vars = get(g:, 'ctrlp_ext_vars', []) + [{
      \ 'init': s:sid_prefix . 'init()',
      \ 'accept': s:sid_prefix . 'accept',
      \ 'lname': 'sample extension',
      \ 'sname': 'sample',
      \ 'type': 'path',
      \ 'nolim': 1
\}]

とした方が上級Vimmerに見られるようなので,みなさんも要チェックだ.

ちなみに,Vim scriptはパースがネックである部分もあるので,前者より後者の方が高速に動作するはずだ. カッコよさの他,マシンに優しくしたり,電力消費を僅かに抑えたいのであれば,後者を用いるとよいだろう. (1度しか実行されない処理に何を言っているんだ,というツッコミは無しでお願いしたい)

参考

Windowsでneovimを使う

注意(2016/10/18 追記)

この記事の情報は古くなっているので、新しい記事を参照してください.

前書き

僕自身,neovim自体にはあまり注目していなかった人間であるが,neovimが頑張ってるっぽく,かなり話題を耳にするようになったので,敢えてWindowsでneovimを試すことにした. Windowsで試すきっかけとなったのは,Osaka.vim #6にて,「Windows用のneovimのバイナリって無いんですかね?」と聞いたところ,公式で配布されており,Wikiにもそのことが書かれているという情報をいただいたからである. また,(ゴミ)プラグイン作成者としても,自分のプラグインがneovimで動作するかどうかには興味があった.

ダウンロード

公式でneovim本体のバイナリとQtで実装されたフロントエンドのバイナリが配布されているので,それらをダウンロードしてくる. 公式のWikiに書かれている通り,

  • neovimのAppVeyorからneovim本体のバイナリをダウンロードする(手順は以下とgifアニメを参照).
    • Environment: GENERATOR=Visual Studio 14 Win64, DEPS_PATH=deps64 というラベルをクリック.
    • Artifacts タブをクリック.
    • Neovim.zipをダウンロードし,解凍.
  • Releases · equalsraf/neovim-qtより,neovim-qt.zipをダウンロードし,解凍.
  • Neovim\nvim.exeneovim-qt\ に移動.
  • Neovim\share\nvim\runtime\ 以下にあるファイルを全て neovim-qt\ にぶちまける.

f:id:koturn:20151111194729g:plain

という手順を踏むとよい. あとは,nvim-qt.exe をダブルクリックすると,neovimが起動する. neovim本体である nvim.exe 単体ではneovimを起動できない(Windowsのコンソールをサポートしていない)ので注意すること.

自分でビルドしたい場合

AppVeyorのビルドログを参考にし,以下の順番でコマンドを実行するとよい. ただし,cmake,git,python2.7以上,Visual Studio 2015(2015以上であることが必須)などがインストールされており, cl.exe などのツールがコマンドプロンプトで使用できることが前提である.

:: MSVCでのビルド用のリポジトリ/ブランチを持ってくる
> git clone -q --branch=tb-mingw https://github.com/equalsraf/neovim.git
> git checkout -qf 27f0fa51e9ca886c87159ee1e2c394426d64d128
> cd neovim
:: 環境変数の設定
> set GYP_MSVS_VERSION=2015
> set GENERATOR=Visual Studio 14 Win64
> set DEPS_PATH=deps64
:: サードパーティライブラリのビルド
> mkdir %DEPS_PATH%
> cd %DEPS_PATH%
> cmake -G "%GENERATOR%" ..\third-party\
> set PATH=%PATH%;C:\cygwin64\bin\
> cmake --build .
> cd ..
:: neovimのビルド
> mkdir build
> cd build
> cmake -G "%GENERATOR%" -DCMAKE_BUILD_TYPE=Debug -DDEPS_PREFIX=..\%DEPS_PATH%\usr -DCMAKE_INSTALL_PREFIX=..\INSTALL ..
> cmake --build .

ある程度の開発環境が整っているならば,これでビルドすることができるはずである. 現在のところ,本家のリポジトリを持ってくるわけではないが,将来的には本家から持ってくるようにできるのではないだろうか?

なお,Cygwinバイナリがあるディレクトリのパスを通していると,思わぬところでつまづいたりするので注意すること. neovim-qtのビルドについてはやっていない.(ただのフロントエンドだろうし...)

neovimの設定ファイル

最近になって,Vim%HOME%\_vimrcLinux環境では ~/.vimrc)にあたるファイルは %HOME%\.nvimrc から %XDG_CONFIG_HOME%\nvim\init.vimに,%HOME%\vimfiles\Linux環境では ~/.vim/)にあたるディレクトリは %HOME%\.nvim\ から %XDG_CONFIG_HOME%\nvim\ に変更された.

Linux環境における $XDG_CONFIG_HOME のデフォルトディレクトリ( $XDG_CONFIG_HOME 環境変数が存在しない場合のディレクトリ)は, ~/.config/ であるが,Windowsでは, %AppData% がデフォルトディレクトリとなるらしい(参考). 僕は,Linuxと同様に %XDG_HOME_CONFIG%%AppData% ではなく, %HOME%\.config とした.

既存のVimが用いる .vimrcvimfiles\ をneovimの設定として扱いたい場合,以下のようにしてシンボリックリンクを作成するとよい. (ただし,コマンドプロンプトを管理者権限で起動する必要があるかもしれない. また,環境変数 %XDG_CONFIG_HOME%%HOME% がちゃんと設定され,共に存在することを確認しておこう.)

> mklink /D %XDG_CONFIG_HOME%\nvim %HOME%\vimfiles\
> mklink %HOME%\vimfiles\init.vim %HOME%\_vimrc

init.vimに関しては,シンボリックリンクを作成するのではなく,以下のように記述するのもよいだろう.

source ~/_vimrc

この場合, init.vim にneovimだけで用いる設定を記述することが可能である.

init.vim は新規に書くことが望ましいが,僕は4000行を超える .vimrc を記述しており,neovimの init.vim 用に .vimrc を書き直すようなことはしたくなかったので, .vimrc にneovimの設定も記述することにした. neovimの判定は has('nvim') という式を用いるとよい. neovimを利用している環境であるならば,この式は 1 と評価されるはずである.

注意点

僕が軽くWindowsのneovim + neovim-qtを触って遭遇した問題は,以下のようなものである.

  • フロントエンドであるneovim-qtの問題であると思われるが,文字コードutf-8以外のファイルを開くと文字化けする.
  • neovim側から新規ファイルを作成しようとすると(存在しないファイルを編集しようとすると),"Permission denided" となってしまい,強制的に保存しないといけない(:write! を用いる).
  • コンパイル時にいくつかのオプションを無効化しているので,既存の _vimrc を用いるとエラーが発生した.
    • 例えば,:language コマンドは使用できなかった.
  • neovim本体をフロントエンドであるneovim-qtを用いているので, has('gui_running')0 となることを期待していたが,起動時は 0.起動後は 1 を返却するようになっていた.
  • 当然のことであるが,一部のプラグインはうまく動作しない
    • bling/vim-airline を導入して, :split:vsplit などとして画面を分割すると,盛大にエラー吐いたなど ....

感想

neovimの公式が配布しているWindows向けのバイナリには難が多いと感じた. Linuxで自分でビルドした場合,エンコーディング等の問題もなく,快適に使うことができたので,現状ではWindows対応がややおろそかであると思われる. また, :terminal コマンドが現状では利用できなかったのが少し残念であった.

参考

WhitespaceをC言語ソースに変換する

はじめに

WhitespaceとはBrainfuckやLazyKと同じ難解プログラミング言語と言われる言語のひとつである. 難解プログラミング言語ジョーク言語と言われるが,実装の容易さやシンプルな言語仕様を考えると,とても興味深い言語である.

Brainfuckチューリングマシンに毛がはえた程度の言語であるが,Whitespaceはスタックマシン型の処理系を想定した言語である. そして,スタック操作,算術命令,ヒープ操作,フロー制御,入出力機能を持ち,比較的高い水準でのプログラミングが可能である.

BrainfuckC言語に変換するという例は世の中にあふれ返っており,変換するコードをいくつも見掛けた. 天下のWikipediaにも変換方法は記述されている.

命令 対応するC言語コード
> ptr++;
< ptr--;
+ (*ptr)++;
- (*ptr)--;
. putchar(*ptr);
, *ptr = getchar();
[ while (*ptr) {
] }

しかし,WhitespaceをC言語に変換するという例は見掛けない. そこで,今回はWhitespaceをいかにC言語に変換するかについて書こうと思う.

Whitespaceの言語仕様

まず,Whitespaceの言語仕様を復習しようと思う. どうも簡単にググって出てきた日本語記事で紹介されているWhitespaceの仕様は少し古いものが多く,公式サイトで紹介されているものと異なっている. (日本語Wikipediaにも古い仕様が書かれている) 例えば,以下のサイトには,古いWhitespaceの仕様を記述していたり,古い仕様の処理系を置いてあったりする.

以下の表の命令欄には,

  • [Space] -> S
  • [Tab] -> T
  • [改行] -> L

として,Whitespaceのコードを書いてある. VM Code欄には,後半の説明で便宜的に利用するVM命令の名称と考えてもらうとよい. そして,動作欄にはその命令の動作を書いている.

命令 VM Code 動作
SS[NUMBER] STACK_PUSH 引数に指定された値をスタックにプッシュする(即値のプッシュ)
STS STACK_DUP_N スタックの上からn番目をコピーし,スタックの一番上に積む
SLS STACK_DUP スタックの上から1番目をコピーし,スタックの一番上に積む
STL STACK_SLIDE スタックの上から1番目をキープしつつ,上から2番目から(2 + 引数)番目を捨てる
SLT STACK_SWAP スタックの上から1番目と2番目を交換する
SLL STACK_DISCARD スタックの上から1番目を捨てる
TSSS ARITH_ADD スタックの上から1番目と2番目をポップし,それを加算(2番目 + 1番目)した結果を最上段にプッシュする
TSST ARITH_SUB スタックの上から1番目と2番目をポップし,それを減算(2番目 - 1番目)した結果を最上段にプッシュする
TSSL ARITH_MUL スタックの上から1番目と2番目をポップし,それを乗算(2番目 * 1番目)した結果を最上段にプッシュする
TSTS ARITH_DIV スタックの上から1番目と2番目をポップし,それを除算(2番目 / 1番目)した結果を最上段にプッシュする
TSTT ARITH_MOD スタックの上から1番目と2番目をポップし,その剰余(2番目 % 1番目)をとった結果を最上段にプッシュする
TTS HEAP_STORE スタックの上から1番目と2番目を取り出し,2番目をヒープのアドレスとし,そこに1番目の値を書き込む(heap[2番目] = 1番目
TTT HEAP_LOAD スタックの上から1番目を取り出し,1番目をヒープのアドレスとし,そのアドレスの値をスタックの最上段にプッシュする( stack_push(heap[1番目]);
LSS[LABEL] 引数に指定されたラベルを定義する
LST[LABEL] FLOW_GOSUB 引数に指定されたラベルをサブルーチンとして呼び出す(呼び出し位置を記憶してジャンプ)
LSL[LABEL] FLOW_JUMP 引数に指定されたラベル位置にジャンプする
LTS[LABEL] FLOW_BEZ スタックの上から1番目を取り出し,その値が0ならば引数に指定されたラベル位置にジャンプする
LTT[LABEL] FLOW_BLTZ スタックの上から1番目を取り出し,その値が0より小さいならば引数に指定されたラベル位置にジャンプする
LTL FLOW_ENDSUB サブルーチンの呼び出し元にジャンプする(関数のreturn)
LLL FLOW_HALT プログラムを終了する
TLSS IO_PUT_CHAR スタックの上から1番目を取り出し,その数値に対応するascii文字を出力
TLST IO_PUT_NUM スタックの上から1番目を取り出し,その数値をascii文字として出力
TLTS IO_READ_CHAR スタックの上から1番目を取り出し,その値をヒープのアドレスとし,そこに標準入力から1文字読み込む( heap[1番目] = gethar();
TLTT IO_READ_NUM スタックの上から1番目を取り出し,その値をヒープのアドレスとし,そこに標準入力から数値を読み込む( scanf("%d", &heap[1番目]);

上記の表を見てわかるように,命令はスタック操作,算術命令,ヒープ操作,フロー制御,入出力の5種類に大別でき,それぞれの命令毎にプレフィックスが決まっている.

命令の種類 プレフィックス
スタック操作 S
算術命令 TS
ヒープ操作 TT
フロー制御 L
入出力 TL

よく使う命令ほど,少ない文字で済むように文字が割り当てられている.

Whitespace処理系では,後方へのラベルジャンプをサポートする必要があるため,一度コード全体コンパイルをすることによって,ラベルが定義されている位置を知っておかなければならない. (コンパイル時にラベルの定義は処理するので,「ラベルを定義する」というVM命令は必要ない)

Whitespaceにおける「引数」とは,正規表現[ \t]\+$ にマッチするものと考えておくとよい. 数値はSを0,Tを1とした2進数表記で表現する. 例えば,STTST ならば,2進数: 01101 なので,数値13となる. ラベルはSとTの連続させて表現する.8文字用いて1文字のasciiコードで表現する必要はないが,そのようにしているサンプルコードもある.

Whitespace to C

WhitespaceからC言語に変換するにあたって,サブルーチン呼び出しが問題となった(他の命令については,解説する必要はないだろう). サブルーチン呼び出し(FLOW_GOSUB)はgoto(FLOW_JUMP)と違い,呼び出し元を記憶し,呼び出し終了時(FLOW_ENDSUB)に,記憶していた呼び出し元に戻る必要がある. つまり,C言語でいうところの関数呼び出しを実現しなければならない.

Brainfuckのループのように,ジャンプ先と戻り位置が1対1で対応しているならば問題はないが,サブルーチンは複数箇所から呼び出されるため,戻り位置の候補が複数あることになる. また,Whitespaceには「サブルーチン定義」という命令はなく,サブルーチン呼び出しは単なる呼び出し位置を記憶したラベルジャンプにすぎない. ジャンプ先のラベルがただのジャンプ命令に利用される可能性もあるし,ラベルより前の処理から継続して,ラベル内の処理に突入する可能性もある. そのため,サブルーチンに対応する関数を定義するという手段も使えない. 以上の理由により,サブルーチン呼び出しを単純な方法でC言語に変換することはできない.

そこで, gotosetjmp()longjmp() を用いることにより,サブルーチン呼び出しを実現した. setjmp() は簡単に言うと,呼び出し位置の情報を,引数の jmp_buf 型の変数に保存する関数である. longjmp() は,引数に指定された jmp_buf 位置に戻る. これと goto を組み合わせることで,簡易的なサブルーチン呼び出しが実現できるというわけだ.

具体的には,次のコードのようにC言語コードに変換する.

/* ... */
printf("static jmp_buf call_stack[CALL_STACK_SIZE];\n");
printf("static size_t stack_idx = 0;\n");

/* for (vmcodes : vmcode) { のようなコードでループする */
  switch (vmcode) {
    /* ... */
    case FLOW_GOSUB:
      /* LABEL = read_label(); のような感じでLABELを用意しておく */
      printf(
        "  if (!setjmp(call_stack[call_stack_idx++])) {\n"
        "    goto %s;\n"
        "  }\n", LABEL);
      break;
    case FLOW_ENDSUB:
      printf("  longjmp(call_stack[--call_stack_idx], 1);");
      break;
      /* ... */
  }
/* } */

これでサブルーチン呼び出しを実現できた. ラベルに関しては,SpaceをS,TabをTに置換したラベル名で変換先のC言語コード中に定義する.

SSSTSSTT:

まとめ

WhitespaceからC言語への変換は,サブルーチン呼び出しが鬼門であったが, gotosetjmp()longjmp() を用いることで実現できた.

なお,インタプリタ/C言語へのトランスレータの機能を持つWhitespace処理系は,koturn/Whitespaceに置いてある.

$ make

とすれば簡単にビルドできる. MSVCであっても,

> nmake /f msvc.mk

とすればビルドできるはずだ. そして,以下のように実行すると,WhitespaceからC言語へ変換できる.

$ ./whitespace [Whitespace source file] -t -o out.c

WhitespaceからC言語での変換例

最後に,koturn/Whitespaceを用いた変換例を記載する. このプログラムはWhitespaceのインタプリタ,Whitespaceコードのニーモニック表現,およびC言語ソースへの変換機能を有している. ここで,何となくWhitespaceからC言語への変換のイメージが掴めると思う.

なお,Whitespaceのコードは視認できないので,前述と同じように

  • [Space] -> S
  • [Tab] -> T

とする. ただし,改行文字はそのまま改行を行う文字とする.

変換元のWhitespaceソースコード

SSSS
SSSTSSTSSS
TTSSSST
SSSTTSSTST
TTSSSSTS
SSSTTSTTSS
TTSSSSTT
SSSTTSTTSS
TTSSSSTSS
SSSTTSTTTT
TTSSSSTST
SSSTSTTSS
TTSSSSTTS
SSSTSSSSS
TTSSSSTTT
SSSTTTSTTT
TTSSSSTSSS
SSSTTSTTTT
TTSSSSTSST
SSSTTTSSTS
TTSSSSTSTS
SSSTTSTTSS
TTSSSSTSTT
SSSTTSSTSS
TTSSSSTTSS
SSSTSSSSS
TTSSSSTTST
SSSTTSTTTT
TTSSSSTTTS
SSSTTSSTTS
TTSSSSTTTT
SSSTSSSSS
TTSSSSTSSSS
SSSTTTSSTT
TTSSSSTSSST
SSSTTTSSSS
TTSSSSTSSTS
SSSTTSSSST
TTSSSSTSSTT
SSSTTSSSTT
TTSSSSTSTSS
SSSTTSSTST
TTSSSSTSTST
SSSTTTSSTT
TTSSSSTSTTS
SSSTSSSST
TTSSSSTSTTT
SSSS
TTSSSSS

STSTTTSTTTSTTTSSTSSTTSTSSTSTTTSTSSSTTSSTST

STSTTSTTTSSTTSSTSTSTTTSTTTSTTSTTSSSTTSTSSTSTTSTTTSSTTSSTST




SSSTTSSSSTSTTSSTSSSTTSSTSS
TSSS
T

SSSTTTSTTTSTTTSSTSSTTSTSSTSTTTSTSSSTTSSTST
S
STTTS
S
TSSTTTSTTTSTTTSSTSSTTSTSSTSTTTSTSSSTTSSTSTSTSTTTTTSTTSSTSTSTTSTTTSSTTSSTSS
T
SSSSST
TSSS
S
STTTSTTTSTTTSSTSSTTSTSSTSTTTSTSSSTTSSTST

SSSTTTSTTTSTTTSSTSSTTSTSSTSTTTSTSSSTTSSTSTSTSTTTTTSTTSSTSTSTTSTTTSSTTSSTSS
S

S


T

SSSTTTSSTSSTTSSTSTSTTSSSSTSTTSSTSS
S
SS
ST
TSTTTS
SSSSTSTS
TSST
TSSTTTSSTSSTTSSTSTSTTSSSSTSTTSSTSSSTSTTTTTSTTSSTSTSTTSTTTSSTTSSTSS
S

SSST
TSSS
S
STTTSSTSSTTSSTSTSTTSSSSTSTTSSTSS

SSSTTTSSTSSTTSSTSTSTTSSSSTSTTSSTSSSTSTTTTTSTTSSTSTSTTSTTTSSTTSSTSS
S

SSST
TSSSSSSS
TTS
T

SSSTTSTTTSSTTSSTSTSTTTSTTTSTTSTTSSSTTSTSSTSTTSTTTSSTTSSTST
SSSTSTS
SSSTTST
T
SST
SS
T
  • 実行例
$ ./whitespace.out hworld.ws
Hello, world of spaces!

ニーモニック表現

上記Whitespaceコードを擬似的なVM命令のニーモニック表現で表現したものである. C言語への変換に用いるわけではないが,イメージを掴む助けになるだろう. なお,ジャンプは絶対アドレス指定である.

0000: STACK_PUSH 0
0005: STACK_PUSH 72
0010: HEAP_STORE
0011: STACK_PUSH 1
0016: STACK_PUSH 101
0021: HEAP_STORE
0022: STACK_PUSH 2
0027: STACK_PUSH 108
0032: HEAP_STORE
0033: STACK_PUSH 3
0038: STACK_PUSH 108
0043: HEAP_STORE
0044: STACK_PUSH 4
0049: STACK_PUSH 111
0054: HEAP_STORE
0055: STACK_PUSH 5
0060: STACK_PUSH 44
0065: HEAP_STORE
0066: STACK_PUSH 6
0071: STACK_PUSH 32
0076: HEAP_STORE
0077: STACK_PUSH 7
0082: STACK_PUSH 119
0087: HEAP_STORE
0088: STACK_PUSH 8
0093: STACK_PUSH 111
0098: HEAP_STORE
0099: STACK_PUSH 9
0104: STACK_PUSH 114
0109: HEAP_STORE
0110: STACK_PUSH 10
0115: STACK_PUSH 108
0120: HEAP_STORE
0121: STACK_PUSH 11
0126: STACK_PUSH 100
0131: HEAP_STORE
0132: STACK_PUSH 12
0137: STACK_PUSH 32
0142: HEAP_STORE
0143: STACK_PUSH 13
0148: STACK_PUSH 111
0153: HEAP_STORE
0154: STACK_PUSH 14
0159: STACK_PUSH 102
0164: HEAP_STORE
0165: STACK_PUSH 15
0170: STACK_PUSH 32
0175: HEAP_STORE
0176: STACK_PUSH 16
0181: STACK_PUSH 115
0186: HEAP_STORE
0187: STACK_PUSH 17
0192: STACK_PUSH 112
0197: HEAP_STORE
0198: STACK_PUSH 18
0203: STACK_PUSH 97
0208: HEAP_STORE
0209: STACK_PUSH 19
0214: STACK_PUSH 99
0219: HEAP_STORE
0220: STACK_PUSH 20
0225: STACK_PUSH 101
0230: HEAP_STORE
0231: STACK_PUSH 21
0236: STACK_PUSH 115
0241: HEAP_STORE
0242: STACK_PUSH 22
0247: STACK_PUSH 33
0252: HEAP_STORE
0253: STACK_PUSH 23
0258: STACK_PUSH 0
0263: HEAP_STORE
0264: STACK_PUSH 0
0269: FLOW_GOSUB 282
0274: FLOW_GOSUB 367
0279: FLOW_HALT
0280: ARITH_ADD
0281: FLOW_ENDSUB
0282: STACK_DUP_N 0
0287: HEAP_LOAD
0288: STACK_DUP_N 0
0293: FLOW_BEZ 310
0298: IO_PUT_CHAR
0299: STACK_PUSH 1
0304: ARITH_ADD
0305: FLOW_JUMP 282
0310: STACK_POP
0311: STACK_POP
0312: FLOW_ENDSUB
0313: STACK_DUP_N 0
0318: STACK_DUP_N 0
0323: IO_READ_CHAR
0324: HEAP_LOAD
0325: STACK_DUP_N 0
0330: STACK_PUSH 10
0335: ARITH_SUB
0336: FLOW_BEZ 353
0341: STACK_POP
0342: STACK_PUSH 1
0347: ARITH_ADD
0348: FLOW_JUMP 313
0353: STACK_POP
0354: STACK_PUSH 1
0359: ARITH_ADD
0360: STACK_PUSH 0
0365: HEAP_STORE
0366: FLOW_ENDSUB
0367: STACK_PUSH 10
0372: STACK_PUSH 13
0377: IO_PUT_CHAR
0378: IO_PUT_CHAR
0379: FLOW_ENDSUB

変換後のC言語ソースコード

#include <assert.h>
#include <setjmp.h>
#include <stdio.h>
#include <stdlib.h>

#ifndef __cplusplus
#  if defined(_MSC_VER)
#    define inline      __inline
#    define __inline__  __inline
#  elif !defined(__GNUC__) && (!defined(__STDC_VERSION__) || __STDC_VERSION__ < 199901L)
#    define inline
#    define __inline
#  endif
#endif

#define STACK_SIZE 65536
#define HEAP_SIZE 65536
#define CALL_STACK_SIZE 65536

#define LENGTHOF(array) (sizeof(array) / sizeof((array)[0]))
#define SWAP(type, a, b) \
  do { \
    type __tmp_swap_var__ = *(a); \
    *(a) = *(b); \
    *(b) = __tmp_swap_var__; \
  } while (0)

inline static int  pop(void);
inline static void push(int e);
inline static void dup_n(size_t n);
inline static void slide(size_t n);
inline static void swap(void);
inline static void arith_add(void);
inline static void arith_sub(void);
inline static void arith_mul(void);
inline static void arith_div(void);
inline static void arith_mod(void);
inline static void heap_store(void);
inline static void heap_read(void);

static int stack[STACK_SIZE];
static int heap[HEAP_SIZE];
static jmp_buf call_stack[CALL_STACK_SIZE];
static size_t stack_idx = 0;
static size_t call_stack_idx = 0;


int main(void)
{
  push(0);
  push(72);
  heap_store();
  push(1);
  push(101);
  heap_store();
  push(2);
  push(108);
  heap_store();
  push(3);
  push(108);
  heap_store();
  push(4);
  push(111);
  heap_store();
  push(5);
  push(44);
  heap_store();
  push(6);
  push(32);
  heap_store();
  push(7);
  push(119);
  heap_store();
  push(8);
  push(111);
  heap_store();
  push(9);
  push(114);
  heap_store();
  push(10);
  push(108);
  heap_store();
  push(11);
  push(100);
  heap_store();
  push(12);
  push(32);
  heap_store();
  push(13);
  push(111);
  heap_store();
  push(14);
  push(102);
  heap_store();
  push(15);
  push(32);
  heap_store();
  push(16);
  push(115);
  heap_store();
  push(17);
  push(112);
  heap_store();
  push(18);
  push(97);
  heap_store();
  push(19);
  push(99);
  heap_store();
  push(20);
  push(101);
  heap_store();
  push(21);
  push(115);
  heap_store();
  push(22);
  push(33);
  heap_store();
  push(23);
  push(0);
  heap_store();
  push(0);
  if (!setjmp(call_stack[call_stack_idx++])) {
    goto STTTSTTTSTTTSSTSSTTSTSSTSTTTSTSSSTTSSTST;
  }
  if (!setjmp(call_stack[call_stack_idx++])) {
    goto STTSTTTSSTTSSTSTSTTTSTTTSTTSTTSSSTTSTSSTSTTSTTTSSTTSSTST;
  }
  exit(EXIT_SUCCESS);

STTSSSSTSTTSSTSSSTTSSTSS:
  arith_add();
  longjmp(call_stack[--call_stack_idx], 1);

STTTSTTTSTTTSSTSSTTSTSSTSTTTSTSSSTTSSTST:
  dup_n(0);
  heap_read();
  dup_n(0);
  if (!pop()) {
    goto STTTSTTTSTTTSSTSSTTSTSSTSTTTSTSSSTTSSTSTSTSTTTTTSTTSSTSTSTTSTTTSSTTSSTSS;
  }
  putchar(pop());
  push(1);
  arith_add();
  goto STTTSTTTSTTTSSTSSTTSTSSTSTTTSTSSSTTSSTST;

STTTSTTTSTTTSSTSSTTSTSSTSTTTSTSSSTTSSTSTSTSTTTTTSTTSSTSTSTTSTTTSSTTSSTSS:
  pop();
  pop();
  longjmp(call_stack[--call_stack_idx], 1);

STTTSSTSSTTSSTSTSTTSSSSTSTTSSTSS:
  dup_n(0);
  dup_n(0);
  heap[pop()] = getchar();
  heap_read();
  dup_n(0);
  push(10);
  arith_sub();
  if (!pop()) {
    goto STTTSSTSSTTSSTSTSTTSSSSTSTTSSTSSSTSTTTTTSTTSSTSTSTTSTTTSSTTSSTSS;
  }
  pop();
  push(1);
  arith_add();
  goto STTTSSTSSTTSSTSTSTTSSSSTSTTSSTSS;

STTTSSTSSTTSSTSTSTTSSSSTSTTSSTSSSTSTTTTTSTTSSTSTSTTSTTTSSTTSSTSS:
  pop();
  push(1);
  arith_add();
  push(0);
  heap_store();
  longjmp(call_stack[--call_stack_idx], 1);

STTSTTTSSTTSSTSTSTTTSTTTSTTSTTSSSTTSTSSTSTTSTTTSSTTSSTST:
  push(10);
  push(13);
  putchar(pop());
  putchar(pop());
  longjmp(call_stack[--call_stack_idx], 1);

  return EXIT_SUCCESS;
}


inline static int pop(void)
{
  assert(stack_idx < LENGTHOF(stack));
  return stack[--stack_idx];
}


inline static void push(int e)
{
  assert(stack_idx < LENGTHOF(stack));
  stack[stack_idx++] = e;
}


inline static void dup_n(size_t n)
{
  assert(n < stack_idx && stack_idx < LENGTHOF(stack) - 1);
  stack[stack_idx] = stack[stack_idx - (n + 1)];
  stack_idx++;
}


inline static void slide(size_t n)
{
  assert(stack_idx > n);
  stack[stack_idx - (n + 1)] = stack[stack_idx - 1];
  stack_idx -= n;
}


inline static void swap(void)
{
  assert(stack_idx > 1);
  SWAP(int, &stack[stack_idx - 1], &stack[stack_idx - 2]);
}


inline static void arith_add(void)
{
  assert(stack_idx > 1);
  stack_idx--;
  stack[stack_idx - 1] += stack[stack_idx];
}


inline static void arith_sub(void)
{
  assert(stack_idx > 1);
  stack_idx--;
  stack[stack_idx - 1] -= stack[stack_idx];
}


inline static void arith_mul(void)
{
  assert(stack_idx > 1);
  stack_idx--;
  stack[stack_idx - 1] *= stack[stack_idx];
}


inline static void arith_div(void)
{
  assert(stack_idx > 1);
  stack_idx--;
  assert(stack[stack_idx] != 0);
  stack[stack_idx - 1] /= stack[stack_idx];
}


inline static void arith_mod(void)
{
  assert(stack_idx > 1);
  stack_idx--;
  assert(stack[stack_idx] != 0);
  stack[stack_idx - 1] %= stack[stack_idx];
}


inline static void heap_store(void)
{
  int value = pop();
  int addr  = pop();
  assert(0 <= addr && addr < (int) LENGTHOF(heap));
  heap[addr] = value;
}


inline static void heap_read(void)
{
  int addr = pop();
  assert(0 <= addr && addr < (int) LENGTHOF(heap));
  push(heap[addr]);
}
$ ./whitespace.out hworld.ws -t -o hworld.c
$ gcc -Ofast -march=native -DNDEBUG hworld.c -o hworld.out
$ ./hworld.out
Hello, world of spaces!

Wandboxでの実行結果はこちら

参考