アンダースコア・ドット記法

前回の記事「ドット記法の威力」では、モジュールやデータ型、型クラスのスコープに属する関数をプログラマがより簡単に参照できる方法として、ドット記法の使い方について学びました。

すでに見たとおり、ドット記法には以下の用法があり、ドットの直前には、データ型が参照されるスコープを明示する (TextArea) か、もしくは値自体 (inputArea) が現れます。

スコープもしくは値がドットの直前に現れる
-- dot notation on scope
TextArea.getCaretPosition inputArea
-- dot notation on reference
inputArea.getCaretPosition

アンダースコア・ドット記法を用いることで、さらに簡潔に書くことができるようになります。

アンダースコア・ドット記法の導入

inputArea の型が TextArea であることがわかっている、という状況を仮定しましょう。最も単純な例は、明示的にこれを宣言することです。

明示的な宣言
insertionPoint :: TextArea -> JFX Int
insertionPoint inputArea = inputArea.getCaretPosition

これを見ると inputArea がやや繰り返しになっていますが、アンダースコア・ドット記法を用いることでこの冗長性を排除することができます。

アンダースコア・ドット記法による自由変数の参照
insertionPoint :: TextArea -> JFX Int
insertionPoint = _.getCaretPosition
追記

関数定義はラムダ式に名前をつけるだけであるということを忘れないように。関数定義 f x = x とラムダ式による宣言 f = \x → x とは同じものです。

言い換えれば、アンダースコア・ドット記法は単に

ラムダ項
\inputArea -> inputArea.getCaretPosition

アンダースコア・ドット項
_.getCaretPosition

に置き換えるものであると言えます。

それでは、便利な使い方をもう少し見てみましょう。

その他の使用法

現在のスレッド名を取得する際、次のような様々な記法を使うことができます。

現在のスレッド名を取得する方法いろいろ
-- do notation
do
    thread <- Thread.current()
    name   <- thread.getName

-- lambda with formal parameter
Thread.current() >>= \t -> t.getName

-- lambda with partial application
-- (needs 'Thread' as explicit scope)
Thread.current() >>= Thread.getName

-- underscore-dot notation
Thread.current() >>= _.getName

アンダースコア・ドット記法は、高階関数に対して、普通ならセクションや部分適用を使う代わりに使用する場合にも便利です。古典的な map 関数を使って、スレッドのリストからスレッド名にマッピングする例を見てみましょう。

複数のスレッドをその名前にマップする
map _.getName threads

レコード記法についても、特に更新したり値を操作したりする際に、アンダースコア・ドット記法が役に立ちます。フィールド name を持つレコード Person があったと仮定しましょう。ただしレコードに対する変更とは、単にレコードを渡して更新されたものを返す処理を指します。

アンダースコア・ドット記法によるレコードの更新
data Person = Person {name :: String}
setName :: String -> State Person ()
setName newName = do
    State.modify _.{name = newName}

最後に考察として

アンダースコアが使用できるのは、ドットが直後に続く場合 に限られます。まだ束縛されていない変数を参照するために独立したアンダースコアを使用することはできません。これは他の言語と大きく異なるところです。

すべての関数もしくはラムダ項は、アンダースコアで参照される自由変数を高々一つしか含みません。これは、Frege や Haskell では原則として、関数やラムダ項は引数を一つしか取らないからです。それ以外の自由変数は、以下の例のように束縛されないまま残ります。

二つ目以降の引数に対するいくつかの記法
String.startsWith "Dierk" "D"
"Dierk".startsWith "D"
map (\s -> s.startsWith "D") ["Ingo","Dierk"]
map (_.startsWith "D") ["Ingo","Dierk"]

さらに、アンダースコアは入れ子にして使うこともできます。

アンダースコア・ドット記法の入れ子
data Person  = Person  {addr :: Address}
data Address = Address {street :: String}
setStreet :: String -> State Person ()
setStreet newStreet = do
    State.modify _.{addr <- _.{street = newStreet} }

最初のアンダースコアは Person を、次のアンダースコアはその Address を参照しており、それぞれがどの型を参照しているのかは明らかです。

しかし、コード中のアンダースコアの意味を「解読」することが難しい時であっても、明示的な引数に置き換えることはいつでも可能です。

アンダースコアを名前付き引数に戻す
...
    State.modify (\p -> p.{addr <- (\a -> a.{street = newStreet} ) } )

将来的に、 IDE のサポート機能で二つの記法を切り替えを可能にするのは難しくありません。また _. の後に続くコードの補完も、IDE の機能として面白そうです。

Haskell との比較

Frege でのドットの扱いを Haskell で同じように書くことはできず、特別な構文拡張であると考えることができます。

この拡張には、以下に挙げるような様々な利点があります。

  • 上記のような、Java プログラマにとってより馴染みのある簡潔な記法が使える

  • Java 風の API やその元々の定義と相性がよい

  • IDE でコード補完が可能

  • TypeDirectedNameResolution が可能

results matching ""

    No results matching ""