Vim スクリプトにおける辞書は、AWK の連想配列、Perl のハッシュ、あるいは Python の辞書と基本的には同じです。つまり、辞書は順不同のコンテナーであり、整数ではなく文字列によってインデックスが付けられます。
この Vim スクリプトを話題にした連載の第 4 回では、Vim スクリプトにおける重要なデータ構造である辞書を取り上げ、辞書のコピー、フィルタリング、拡張、削除を行うためのさまざまな関数について説明します。また、リストと辞書の違いに重点を置いた例、そして組み込みリストに関する第 3 回の記事で作成したリスト・ベースのソリューションよりも辞書を使用したほうが賢明な例を紹介します。
Vim スクリプトで辞書を作成するには、キーと値のペアからなるリストを波括弧で囲みます。キーと値のそれぞれのペアでは、コロンを使ってキーと値を区分します。以下はその一例です。
リスト 1. 辞書の作成
let seen = {} " Haven't seen anything yet
let daytonum = { 'Sun':0, 'Mon':1, 'Tue':2, 'Wed':3, 'Thu':4, 'Fri':5, 'Sat':6 }
let diagnosis = {
\ 'Perl' : 'Tourettes',
\ 'Python' : 'OCD',
\ 'Lisp' : 'Megalomania',
\ 'PHP' : 'Idiot-Savant',
\ 'C++' : 'Savant-Idiot',
\ 'C#' : 'Sociopathy',
\ 'Java' : 'Delusional',
\}
|
作成した辞書にアクセスするには標準の角括弧によるインデックス表記を使用しますが、インデックスには数値ではなく、文字列を使用します。
let lang = input("Patient's name? ")
let Dx = diagnosis[lang]
|
辞書に該当するキーがない場合には、例外がスローされます。
let Dx = diagnosis['Ruby'] **E716: Key not present in Dictionary: Ruby** |
その一方、get() 関数を使用すると、存在しないかもしれないエントリーにも安全にアクセスすることができます。get() が取る引数は、辞書そのものと、辞書のなかで検索するためのキーです。該当するキーが辞書に含まれていれば、そのキーに対応する値が返されます。キーがない場合には、get() はゼロを返します。あるいは、キーが見つからない場合に get() が返す値を 3 番目の引数として指定することもできます。
let Dx = get(diagnosis, 'Ruby') " Returns: 0 let Dx = get(diagnosis, 'Ruby', 'Schizophrenia') " Returns: 'Schizophrenia' |
特定の辞書のエントリーにアクセスする 3 番目の方法として、エントリーのキーが ID 文字 (英数字とアンダーバー) だけで構成されている場合には、以下のような「ドット表記」を使用して対応する値にアクセスすることもできます。
let Dx = diagnosis.Lisp " Same as: diagnosis['Lisp'] diagnosis.Perl = 'Multiple Personality' " Same as: diagnosis['Perl'] |
この特殊な簡易表記では、辞書をデータ・レコードまたはデータ構造体として極めて簡単に使用することができます。
let user = {}
let user.name = 'Bram'
let user.acct = 123007
let user.pin_num = '1337'
|
Vim スクリプトには、辞書に含まれるすべてのキーのリスト、すべての値のリスト、またはすべてのキーと値のペアのリストを取得するために使用できる以下の関数が用意されています。
let keylist = keys(dict) let valuelist = values(dict) let pairlist = items(dict) |
上記の items() 関数が実際に返すのはリストのリストであり、それぞれの「内部」リストには 1 つのキーとそれに対応する値という 2 つの要素しかありません。したがって、辞書のエントリーを繰り返し処理する場合、この items() はとりわけ重宝します。
for [next_key, next_val] in items(dict)
let result = process(next_val)
echo "Result for " next_key " is " result
endfor
|
辞書での割り当ては、Vim スクリプトのリストでの割り当てとまったく同じような動作をします。辞書は参照 (つまり、ポインター) によって表されるため、辞書を別の変数に割り当てると、両方の変数が指すベースとなるデータ構造が同じものになってしまいます。この事態を回避するには、最初に元の辞書をコピーするか、またはディープ・コピーを行ってください。
let dict2 = dict1 " dict2 just another name for dict1
let dict3 = copy(dict1) " dict3 has a copy of dict1's top-level elements
let dict4 = deepcopy(dict1) " dict4 has a copy of dict1 (all the way down)
|
リストの場合と同じく、同一のものであるかどうかを比較するには is 演算子を、値を比較するには == 演算子を使用します。
if dictA is dictB " They alias the same container, so must have the same keys and values elseif dictA == dictB " Same keys and values, but maybe in different containers else " Different keys and/or values, so must be different containers endif |
辞書にエントリーを追加するには、単に値を新しいキーに割り当ててください。
let diagnosis['COBOL'] = 'Dementia' |
別の辞書から複数のエントリーをマージする場合には、extend() 関数を使用します。1 番目の引数 (拡張対象) と 2 番目の引数 (追加エントリーが含まれる引数) は、どちらも辞書でなければなりません。
call extend(diagnosis, new_diagnoses) |
extend() は、複数のエントリーを明示的に追加する場合にも便利な関数です。
call extend(diagnosis, {'COBOL':'Dementia', 'Forth':'Dyslexia'})
|
辞書から単一のエントリーを削除する手段は 2 通りあり、組み込みの remove() 関数を使用するか、または unlet コマンドを使用するかのいずれかです。
let removed_value = remove(dict, "key") unlet dict["key"] |
複数のエントリーを辞書から削除するには、filter() を使用したほうが簡潔で、効率の点でも優れています。filter() 関数は、リストに対して使用する場合とほとんど同じような動作をしますが、v:val を使用して各エントリーの値をテストするだけでなく、v:key を使用して、キーをテストすることもできるという点が異なります。以下に、値とキーをテストする例を記載します。
リスト 2. 値およびキーのテスト
" Remove any entry whose key starts with C... call filter(diagnosis, 'v:key[0] != "C"') " Remove any entry whose value doesn't contain 'Savant'... call filter(diagnosis, 'v:val =~ "Savant"') " Remove any entry whose value is the same as its key... call filter(diagnosis, 'v:key != v:val') |
filter() の他にも、辞書とリストの両方で使用できる組み込み関数およびプロシージャーがあります。辞書に対してリスト関数を適用すると、ほとんどすべての場合 (主な例外には string() があります)、その関数は辞書の値で構成されるリストが渡されたかのように振る舞います。リスト 3 に、特によく使用されるリスト関数を記載します。
リスト 3. 辞書でも動作するその他のリスト関数
let is_empty = empty(dict) " True if no entries at all let entry_count = len(dict) " How many entries? let occurrences = count(dict, str) " How many values are equal to str? let greatest = max(dict) " Find largest value of any entry let least = min(dict) " Find smallest value of any entry call map(dict, value_transform_str) " Transform values by eval'ing string echo string(dict) " Print dictionary as key/value pairs |
map() 組み込み関数は特に、辞書内のデータを正規化する際に有用です。例えば、ユーザーの登録名が含まれている辞書 (おそらくユーザー ID がインデックスとして付けられている辞書) があるとします。その場合、以下の方法を使用することで、必ず各名前の先頭文字のみが大文字になるようにすることができます。
call map( names, 'toupper(v:val[0]) . tolower(v:val[1:])' ) |
map() を呼び出すと、辞書の各値を順に v:val に割り当て、その文字列の式を評価した後、値をその式の結果に置き換えます。この例では、map() を呼び出すことで名前の最初の文字を大文字に変換し、残りの文字を小文字に変換してから、変更されたこの文字列を新しい名前の値として使用しています。
連載第 3 回の記事では、指定したテキストを囲むコメント・ボックスを生成するという簡単な例で、Vim スクリプトの可変個引数を持つ関数の引数について説明しました。テキスト・ストリングの後にオプションの引数を追加することで、コメント・イントロデューサー、「ボックス作成」文字として使用する文字、そしてコメントの幅を指定することができます。リスト 4 に、このオリジナルの関数をもう一度記載します。
リスト 4. 可変個引数パラメーターとして渡されたオプション引数
function! CommentBlock(comment, ...)
" If 1 or more optional args, first optional arg is introducer...
let introducer = a:0 >= 1 ? a:1 : "//"
" If 2 or more optional args, second optional arg is boxing character...
let box_char = a:0 >= 2 ? a:2 : "*"
" If 3 or more optional args, third optional arg is comment width...
let width = a:0 >= 3 ? a:3 : strlen(a:comment) + 2
" Build the comment box and put the comment inside it...
return introducer . repeat(box_char,width) . "\<CR>"
\ . introducer . " " . a:comment . "\<CR>"
\ . introducer . repeat(box_char,width) . "\<CR>"
endfunction
|
可変個引数は関数のオプションを指定するには便利ですが、2 つの大きな欠点があります。それは、可変個引数は関数パラメーターの明示的な順序付けを強いること、そして関数の呼び出しに、その順序を暗黙的に残してしまうことです。
リスト 4 に示されているように、いずれかの引数がオプションの場合には一般的に、引数を指定する順序を前もって決めておかなければなりません。しかしこの必要性によって、設計で問題が生じてきます。順序の後のほうにくるオプションを指定するには、ユーザーがそのオプションより前のすべてのオプションを明示的に指定しなければならないためです。理想的には、1 番目のオプションは最もよく使用されるオプションで、2 番目のオプションは 2 番目によく使用されるオプションという順序になるでしょうが、実際には、この順序を関数が広く利用される前に決定するのは簡単なことではありません。関数が利用される前から、どうやって大部分のユーザーにとって最も重要なオプションがわかるというのでしょうか。
例えばリスト 4 の CommentBlock() 関数では、コメント・イントロデューサーが最も必要になりそうなオプション引数であるという前提の下、コメント・イントロデューサーをパラメーター・リストの先頭に配置しています。しかし、関数のユーザーが一貫して C や C++ でプログラミングを行い、デフォルトのコメント・イントロデューサーをまったく変更しない場合を考えてみてください。さらに、コメント・ブロックの幅が新しいプロジェクトによってさまざまに異なってくることがわかったとしたらどうなるでしょうか。このような状況になると、非常に面倒なことになります。なぜなら開発者は、最初の 2 つのオプション引数は常にデフォルト値のままでよいことがわかっていても、3 つすべてのオプション引数を毎回指定しなければならないためです。
" Comment of required width, with standard delimiter and box character... let new_comment = CommentBlock(comment_text, '//', '*', comment_width) |
このことは直接 2 つ目の問題につながります。すなわち、明示的に指定しなければならないオプション引数がある場合、それ以外にもオプション引数をいくつか指定しなければならないことになりがちですが、通常、オプション引数はデフォルトで最もよく必要とされる値に設定されるため、ユーザーはオプション引数を指定することに慣れていません。したがって、どのような順番で指定する必要があるのかに関しても詳しくありません。その結果、以下のような実装エラーが起こりがちです。
" Box comment using ==== to standard line width... let new_comment = CommentBlock(comment_text, '=', 72) |
上記によって、以下のような戸惑いを感じるほどの (実際にはコメントとして扱われることのない) コメントが生成されます。
=727272727272727272727272727272 = A bad comment =727272727272727272727272727272
問題は、オプション引数には設定しなければならないオプションを明示的に示す手段が何もないことです。オプション引数の重要性は、引数リスト内での位置によって暗黙的に決まるので、オプション引数の順番を誤って指定すると、自然とその重要性も変わってきます。
これは、行うべき作業にふさわしくないツールを使用してしまった典型的な例です。順序が重要な意味を持ち、位置によって最も適切にオプション引数が識別されるのであれば、リストは完璧なツールです。けれどもこの例のように、オプション引数の順序が決まっていることが役に立つというよりも、むしろ厄介の種となり、その位置が混同されやすい場合には、オプション引数を誤って識別するというエラーが、見つけにくいエラーとして発生する可能性があります。
このような場合にふさわしいのは、ある意味リストとは正反対のデータ構造、つまり辞書です。辞書の場合、オプション引数の順序に意味がない代わりに、明確にオプション引数が識別されます。リスト 5 に先ほどの関数と同じ関数を記載しますが、今回はオプションが可変個引数パラメーターではなく、辞書によって指定されています。
リスト 5. 辞書で渡されるオプション引数
function! CommentBlock(comment, opt)
" Unpack optional arguments...
let introducer = get(a:opt, 'intro', '//' )
let box_char = get(a:opt, 'box', '*' )
let width = get(a:opt, 'width', strlen(a:comment) + 2)
" Build the comment box and put the comment inside it...
return introducer . repeat(box_char,width) . "\<CR>"
\ . introducer . " " . a:comment . "\<CR>"
\ . introducer . repeat(box_char,width) . "\<CR>"
endfunction
|
このバージョンの関数では、引数を 2 つだけ渡します。具体的には、必須のコメント・テキストと、それに続くオプションの辞書です。続いて組み込み関数 get() を使用して各オプションを取得しますが、オプションが指定されていない場合には、引数のデフォルト値を取得します。その上で、関数の呼び出しが名前付きオプションと値のペアを使用して関数の振る舞いを構成します。このようにすることで、関数内でパラメーターを構文解析するための実装が多少簡潔になり、関数呼び出しが遥かに読みやすくなって、エラーが起こる可能性が減ることになります。以下の例を見てください。
" Comment of required width, with standard delimiter and box character...
let new_comment = CommentBlock(comment_text, {'width':comment_width})
" Box comment using ==== to standard line width...
let new_comment = CommentBlock(comment_text, {'box':'=', 'width':72})
|
連載第 3 回の記事では、サンプルの AlignAssignments() という関数を更新し、変更を行ったテキスト行をリストに保管するようにしました。リスト 6 に、更新後の関数をもう一度記載します。
リスト 6. 更新後の AlignAssignments() 関数
function! AlignAssignments ()
" Patterns needed to locate assignment operators...
let ASSIGN_OP = '[-+*/%|&]\?=\@<!=[=~]\@!'
let ASSIGN_LINE = '^\(.\{-}\)\s*\(' . ASSIGN_OP . '\)\(.*\)$'
" Locate block of code to be considered (same indentation, no blanks)...
let indent_pat = '^' . matchstr(getline('.'), '^\s*') . '\S'
let firstline = search('^\%('. indent_pat . '\)\@!','bnW') + 1
let lastline = search('^\%('. indent_pat . '\)\@!', 'nW') - 1
if lastline < 0
let lastline = line('$')
endif
" Decompose lines at assignment operators...
let lines = []
for linetext in getline(firstline, lastline)
let fields = matchlist(linetext, ASSIGN_LINE)
call add(lines, fields[1:3])
endfor
" Determine maximal lengths of lvalue and operator...
let op_lines = filter(copy(lines),'!empty(v:val)')
let max_lval = max( map(copy(op_lines), 'strlen(v:val[0])') ) + 1
let max_op = max( map(copy(op_lines), 'strlen(v:val[1])' ) )
" Recompose lines with operators at the maximum length...
let linenum = firstline
for line in lines
if !empty(line)
let newline
\ = printf("%-*s%*s%s", max_lval, line[0], max_op, line[1], line[2])
call setline(linenum, newline)
endif
let linenum += 1
endfor
endfunction
|
このバージョンは、データをリロードする代わりにキャッシュに入れることで、関数の効率性を大幅に改善する結果となりましたが、そのために保守のしやすさが犠牲になっています。具体的には、このバージョンの関数は各行のさまざまなコンポーネントを 3 つの要素からなる小さな配列に保管するため、このコードには、名前からその目的を推測できない「マジック・インデックス」(v:val[0] や line[1] など) が散在することになってしまいました。
辞書は、まさにこの問題を解決するために作られています。なぜなら、辞書はリストと同じようにデータを 1 つの構造に集約しますが、リストとは異なり、それぞれのデータには番号ではなく、文字列のラベルを付けるからです。これらの文字列を慎重に選択すれば、コードを大幅に簡潔化し、マジック・インデックスの代わりに、意味のある名前を使用することができます (各行の lvalue には v:val.lval、各行の演算子には line.op を指定するなど)。
辞書を使って関数を書き直すのは、リスト 7 のとおり、明らかに簡単なことです。
リスト 7. さらに改善された AlignAssignments() 関数
function! AlignAssignments ()
" Patterns needed to locate assignment operators...
let ASSIGN_OP = '[-+*/%|&]\?=\@<!=[=~]\@!'
let ASSIGN_LINE = '^\(.\{-}\)\s*\(' . ASSIGN_OP . '\)\(.*\)$'
" Locate block of code to be considered (same indentation, no blanks)...
let indent_pat = '^' . matchstr(getline('.'), '^\s*') . '\S'
let firstline = search('^\%('. indent_pat . '\)\@!','bnW') + 1
let lastline = search('^\%('. indent_pat . '\)\@!', 'nW') - 1
if lastline < 0
let lastline = line('$')
endif
" Decompose lines at assignment operators...
let lines = []
for linetext in getline(firstline, lastline)
let fields = matchlist(linetext, ASSIGN_LINE)
if len(fields)
call add(lines, {'lval':fields[1], 'op':fields[2], 'rval':fields[3]})
else
call add(lines, {'text':linetext, 'op':'' })
endif
endfor
" Determine maximal lengths of lvalue and operator...
let op_lines = filter(copy(lines),'!empty(v:val.op)')
let max_lval = max( map(copy(op_lines), 'strlen(v:val.lval)') ) + 1
let max_op = max( map(copy(op_lines), 'strlen(v:val.op)' ) )
" Recompose lines with operators at the maximum length...
let linenum = firstline
for line in lines
let newline = empty(line.op)
\ ? line.text
\ : printf("%-*s%*s%s", max_lval, line.lval, max_op, line.op, line.rval)
call setline(linenum, newline)
let linenum += 1
endfor
endfunction
|
この新しいバージョンで変更した部分は、太字で示されています。変更は 2 箇所だけで、1 つは各行のレコードがハッシュではなく辞書になっていること、もう 1 つは各レコードの要素に対する後続のアクセスでは数字のインデックスではなく、名前付き参照を使用していることです。全体的な結果として、コードが読みやすくなっているだけでなく、配列のインデックスを指定する際に起こりがちなインデックス番号のずれなどのエラーの原因がなくなっています。
Vim では以下の組み込みコマンドを使用して、重複する行をファイルから削除することができます。
:%sort u |
組み込み sort コマンドの u オプションによって、(行がソートされた後に) 重複する行が削除され、先頭の % によって、その特殊な sort がファイル全体に適用されることになります。これは便利な方法ですが、ファイルの個々の行の順序が維持されなくても構わないという場合にだけ使用できます。例えば、順位が付けられている入賞者のリストや、限られたリソースを利用するための順番待ちの登録用紙、あるいは優先順に並べられた To-Do リストなど、行の順番に重要な意味がある場合には、この方法を使うと問題が起こる可能性があります。
辞書のキーは本質的に一意であるため、辞書を使用することでファイルから重複する行を削除し、しかも元の行の順序を維持することも可能です。これを実現する単純な関数をリスト 8 に記載します。
リスト 8. 一意性によって順序を維持する関数
function! Uniq () range
" Nothing unique seen yet...
let have_already_seen = {}
let unique_lines = []
" Walk through the lines, remembering only the hitherto-unseen ones...
for original_line in getline(a:firstline, a:lastline)
let normalized_line = '>' . original_line
if !has_key(have_already_seen, normalized_line)
call add(unique_lines, original_line)
let have_already_seen[normalized_line] = 1
endif
endfor
" Replace the range of original lines with just the unique lines...
exec a:firstline . ',' . a:lastline . 'delete'
call append(a:firstline-1, unique_lines)
endfunction
|
Uniq() 関数は範囲を使用するように宣言されているため、バッファー内のある範囲の行に対して Uniq() 関数を呼び出す場合でも、この関数は一度だけしか呼び出されません。
この関数が呼び出されてまず初めに行うことは、空の辞書 (have_already_seen) のセットアップです。この辞書を使用して、指定された範囲内ですでに出現した行を記録します。以前に出現していない行については、unique_lines に保管されたリストに追加します。
この関数が次に行うのは、まさにこの行の選り分けを行うためのループ処理です。getline() によって、バッファーから指定された行の範囲を取得し、それぞれの行を繰り返し処理します。その際、最初に行の先頭に「>」を追加して、その行が空ではないことを確実にします。このようにする理由は、Vim スクリプトの辞書は空の文字列をキーとして持つエントリーを保管できないため、バッファーから取得した行が空であると、その行は正しく have_already_seen に追加されないからです。
行が正規化されると、関数はその行が、have_already_seen 辞書でキーとしてすでに使用されているかどうかをチェックします。キーとして使用されている場合には、同じ行がすでに出現して unique_lines に追加されていることになるので、すでに追加されているのと同じ行は無視されます。まだキーとして使用されていなければ、その行は初めて出現したことになるので、元の (正規化されていない) 行を unique_lines に追加し、正規化されたバージョンを have_already_seen にキーとして追加しなければなりません。
この方法ですべての行がフィルタリングされると、unique_lines には一意の行だけが、最初に出現した順序のとおりに含まれることになります。後は、元の行一式を削除し、(append() によって) これらの蓄積された一意の行で置き換えればよいだけです。
このような関数が使用できれば、標準モードのキー・マップをセットアップして、以下のようにファイル全体でコマンドを呼び出すことも可能です。
nmap ;u :%call Uniq()<CR> |
あるいは、これを特定の行のセット (例えば、ビジュアル・モードで選択された範囲など) に適用することもできます。
vmap u :call Uniq()<CR> |
これまで取り上げた Vim スクリプトの基本機能 (文と関数、配列、およびハッシュ) があれば、Vim のコア機能セットに対して、ありとあらゆる追加機能を作成することができます。しかし、これまで説明した拡張機能ではいずれも、ユーザーが標準モードのコマンドを実行するか、または挿入モードで特定のシーケンスを入力することによって、明示的に振る舞いを要求しなければなりませんでした。
この連載の次回の記事では、Vim の組み込みイベント・モデルについて調べ、ユーザーの編集作業に合わせて自動的にトリガーされるユーザー定義関数をセットアップする方法を詳しく説明します。
学ぶために
- Vim エディターを拡張するための組み込み言語、Vimスクリプトについて学ぶには、まず「Vim エディターのスクリプトの作成: 第 1 回 変数、値、式」(developerWorks、2009年5月) を読んでください。
- 「Vim エディターのスクリプトの作成: 第 2 回 ユーザー定義関数」(developerWorks、2009年7月) では Vim スクリプトのスカラー・データ型であるストリング、数値、そしてブール値について説明しています。
- 「Vim エディターのスクリプトの作成: 第 3 回 組み込みリスト」(developerWorks、2010年1月) では、リスト・データ構造を紹介し、いくつかの例に沿って、その使い方を説明しています。
- Vim エディターとその多数のコマンドについての学習を続けるには、以下のリソースを参照してください。
- Vim ホーム・ページ
- オンライン・ブック「A Byte of Vim」
- Vim に関するさまざまな本
- Vim 独自のマニュアル
- Steve Oualline 著「Vim Cookbook」
- さらに広範な Vim スクリプトを調べるには、以下を参照してください。
- developerWorks Linux ゾーンに豊富に揃った Linux 開発者向けの資料を調べてください。記事とチュートリアルの人気ランキングも要チェックです。
- developerWorks に掲載されているすべての「Linux のヒント」シリーズの記事と Linux チュートリアルを参照してください。
- developerWorks の Technical events and webcasts で最新情報を入手してください。
- Twitter で developerWorks をフォローしてください。
製品や技術を入手するために
- Vim ディストリビューションのダウンロード・ページから、それぞれのプラットフォームに対応した Vim の最新バージョンにアップグレードしてください。
- developerWorks から直接ダウンロードできる IBM ソフトウェアの試用版を使用して、Linux で次の開発プロジェクトを構築してください。
議論するために
- My developerWorks コミュニティーに加わってください。ここでは他の developerWorks ユーザーとのつながりを持てる他、開発者が主導するブログ、フォーラム、グループ、ウィキを調べることができます。
