インテリセンス快適な構文とAPI

インテリセンスの快適さと言語の構文に関するエッセイ。

Intellisensability

コードを書いているときに入力補完の候補とその概要がポップアップメニューに出てくるやつを インテリセンス と呼ぶことにします。インテリセンスを使うとコードを書くのが楽になりますが、頻繁に暴発する場合は逆に手間になります。インテリセンスが役に立っている状態を インテリセンス快適 と呼ぶことにします。

インテリセンス快適な構文

暴発の例: 既知の単語と新しい単語

インテリセンスが暴発しやすい状況の1つに、ユーザーが新しい単語を書こうとしているときに、既存の単語を補完候補に出してしまう現象があります。ここでいう新しい単語とは、まだ定義を書いていない変数や関数の名前です。例えば1引数のラムダ式 (関数オブジェクトのリテラル) の構文が <parameter> => <expression> だったとして、次のようなコードを書くと、

var xs = new[] { 1, 2, 3 };

xs.Select(x => x + 1);

最後の行の xs.Select( の後、入力補完は Select の引数として式を期待して使用可能な名前 (xs など) を補完しようとし、入力しようとしている単語 (x) をハイジャックしがちです。

一方、同じ種類のラムダ式の構文が fun <parameter> -> <expression> だった場合、これは fun キーワードの直後の式は新しい単語ではないので、入力補完を抑制 でき ます。

let xs = [|1; 2; 3|]

xs |> Array.map (fun x -> x + 1)

暴発の例: 参照と定義の語順

似たような例として、語順の問題もあります。例えば次の関数の定義では、型パラメーター T を定義される位置 (<T>) より前の位置 (T Identity) に書かなければいけませんが、この段階では補完候補として T が現れないので、他の T から始まる単語を入力しがちです。

// C#
public static T Identity<T>(T value)
{
    return value;
}

一方 Java では、型パラメーターを定義する位置を移動することにより、この問題を解決しています。

// Java
public static <T> T identity(T value)
{
    return value;
}

最近見つけた別の例に TypeScript の import があります。これは他のファイル (モジュール) にある定義を参照できるようにするための構文で、次のように書けます:

// "./other.ts" で定義されている A, B, C を修飾なしで参照可能にする。
import { A, B, C } from "./other";

波括弧の中を入力している段階では、どのモジュールを参照するつもりなのかインテリセンスが知らないので、入力補完は起きません。しかし、先に import { } from "./other"; と書いてから波括弧の中に戻ることで入力補完ができるようになります。もし語順が逆だったら、カーソルの移動なしで入力補完ができていたでしょう:

// 擬似コード
import "./other" { A, B, C };

快適な構文の例: ドット記法

C系の構文を持つオブジェクト指向言語 (C++ とか) では x.m でメンバーを参照しますが、 . を入力したときに入力補完が起こるのが通例です。これにより、入力中の式に対して可能な操作をワンタッチで検索でき、メモリアクセスの負荷を減らします。

前述の上のコードにある xs.Select についても、配列 xs に対する map 操作の名前を忘れても xs. と書いた瞬間に候補が出て、それをざっと眺めれば Select を思い出すことができるはずです。(たぶん)

一方、前述の下のコード (xs |> Array.map) では、xs を書いた後に配列の操作が Array モジュール (※名前空間のようなもの) に含まれていることを思い出さなければ連鎖を続けることができません。ややインテリセンス快適さを損ねます。

C# には、型の定義に対して非侵入的にメソッドを増やすことができる、拡張メソッドという糖衣構文があり、これのおかげでインテリセンス快適さがかなり高まります。前述の Select がその一例です。拡張メソッドを定義するには、関数の定義の第一引数に this というキーワードをつければいいのですが、

// 定義側
public static string Scream(this string message) {
    return message.ToUpper() + "!";
}

// 参照側
var message = "hello".Scream(); // Scream("hello") の糖衣構文
Console.WriteLine(message); //=> HELLO!

もしこの制約がなかったら、 "hello". と入力した瞬間に第一引数の型が string であるすべての関数が候補に上がってしまい、一覧性を失います。

インテリセンス快適でないAPIの例

最高にインテリセンス快適な状況は、入力したい単語の一部を数文字入力した時点で、候補リストの一番上にその単語が現れて、そのままコミットするという流れです。候補に現れても一番上でなければ、マウスで選択するにせよ、カーソル移動するにせよ、追加の手間がかかります。

API の設計によってインテリセンスの検索性を損ねることがあります。C# で、もし2つの値が等しいことを表明する関数が Assert.Equal (Assert モジュールの中の Equal 関数) だったら、 Assert.Equals (すべての型に継承される関数の1つ) と混乱します。

// 等しくなければ表明エラー
Assert.Equal(actual, expected);

// 何も起こらない。
Assert.Equals(left, right);

これは日本語入力の際に「か」(→ 火 可 …)などを漢字変換するときの問題に似ています。

まとめ

言語やAPIはインテリセンス快適さをなるべく損ねないように作りましょう。

関連リンク

関連記事