生涯未熟

生涯未熟

プログラミングをちょこちょこと。

関数の自己参照とオプションデザイン

この記事はRob Pike氏の

commandcenter.blogspot.jp

を翻訳したものになります。
※Rob Pike氏より翻訳の許可は頂いております。氏に多大な感謝を。


私は自分が書いているGoのパッケージのオプション設定に対して良い方法を度々試してきました。 オプションを型として扱う、などがそうです。 パッケージは複雑で、様々なオプションがあることでしょう。

オプションに対してのアプローチは数多くありますが、使い勝手が良いものや、あまりAPIを必要としないことや(少なくともユーザーが苦にならない程度)、肥大化することなく必要に応じてスケールするようにしたいと考えていました。
しかし、オプション構造体・大量のメソッド・バリアントコンストラクタ等、様々な既存の方法を試しましたが、満足のいく結果を得られませんでした。

1年ほどの多々の試行とGopher達との議論を経て、ついに満足のいく手法を発見しました。
あなたにとってその手法が好まれるかもしれないし、そうでもないかもしれませんが、関数の自己参照の興味深い使い方の一つとして示しています。
読んで頂けると幸いです。

それでは、まずは簡単なものから始めていきましょう。
完成形に向かって徐々に修正していく形で進めていきます。

最初にOption型を定義します。
1つの引数、つまりFooが動作する関数です。

オプションがそのオプション自身の状態を設定するために呼び出す関数として実装されているというアイデアです。 奇妙に見えるかもしれませんが、狂気の中にこそ方法があります。

次は、Option型の関数呼び出し時に渡すオプションを設定する*FooのOptionメソッドを定義します。
このメソッドは、Fooが定義されているpkgパッケージと同じパッケージに定義されています。

Goを使っているのでメソッドを可変長引数にして、多くのオプションを設定することができます。

オプションを提供するために、pkgパッケージに適切な名前とシグネチャを持つ関数を定義します。

冗長性を制御するためにFooの冗長性を表すフィールドに整数値を設定します。
わかりやすい名前の関数を作り、冗長性を設定したオプションを返すようにします。
これはクロージャーを意味し、そのクロージャー内でフィールドを設定します。

何故、単純に設定する代わりにクロージャーを返すのでしょうか?
なぜなら、ユーザーはクロージャーをわざわざ書く必要がなく、Optionメソッドも使いやすくなるからです。
(ここからどんどん良くなっていきますよ)

パッケージのクライアントでは、次のように書いてFooオブジェクトの冗長性オプションを設定できます。

これは簡単なもので目的はほとんど満たしていますが、Optionのメカニズムを使用して一時的な値を設定したい。
つまり、Optionメソッドが前の状態に戻るようにしたいのです。

これを実現するのは簡単で、Optionメソッドの関数型の基礎型から返される空のinterfaceとして保存するだけです。
interfaceの値はコード内を流れていきます。

クライアントは前のコードと同じように使用することが出来ますが、以前の状態を復元したい場合は、
最初の呼び出しの戻り値を保存し、それを引数として実行するだけです。

オプションの復元を実行する際の型アサーションは不器用な作りになっています。
この辺りの設計をもう少し良くすることで、更に良いコードになるでしょう。

まず、オプションのフィールドを設定する関数を再定義し、前の状態を復元するための別のオプションを返すようにします。

この関数の自己参照の定義は、ステートマシンを想起させます。
ここでは少し違った使い方をしており、関数が逆に関数を返す形になります。
(補足:リンク先の映像で使われているスライドのここの辺りを読めば理解が早いと思います)

次に、*FooのOptionメソッドの戻り値の型(と意味)をinterface{]からoptionに変更します。

最後の実装は、実際のオプション機能の実装です。

クロージャーの内部は、interfaceの値ではなくoptionを返されなければいけません。
つまり、状態を復元するにはクロージャーを返却する必要があります。

しかし、これは簡単なことです。
状態を復元するのにクロージャーを準備し、それを繰り返すことで可能になるということです。

これは以下のようになります。

クロージャーの内部の最後の行が return previous から return Verbosity(previous) に変更されていることに注目してください。
単に古い値を返すのではなく、Verbosity関数を呼び出して元に戻すクロージャーを作成し、これを返します。

今のクライアントを見れば、これは凄く良いということがわかりますね。

最後に、Goの遅延メカニズムを利用してクライアントですべてを整理します。

返される verbosity は冗長性の値ではなく、クロージャーになっているので冗長性の値自体は隠蔽されていることは注目に値します。
もし、隠蔽された冗長性の値が必要な場合は、もう少し"マジック"が必要になりますが、もう既に充分なほど"マジック"があります。

これらの実装が過度なものに見えるかもしれませんが、実際には各々の実装は数行でありながら、高い普遍性を持っています。
最も重要なのは、パッケージのクライアントの観点から見ると非常に使いやすいということです。
ついに満足できるデザインが出来上がりました。
また、私はGoのクロージャーを使って目標を達成出来たことに満足しています。