wizard モノイド (翻訳)

Original post: The wizard monoid

最近の GHC 8.0 は、IO 用の Monoid インスタンスを提供しています。このブログ記事では、組み合わせ可能な “wizard” を作りつつ、なぜこのインスタンスが便利なのかを示す例をお見せします。

Wizard

ここで使う “wizard” とは、ユーザーに複数の入力を促し、全ての入力が完了したら、いくつかのアクションを実行するようなプログラムです。

簡単な wizard の例です:

… 実行例:

What is your name?
Gabriel<Enter>
What is your age?
31<Enter>
Your name is: Gabriel
Your age is: 31

… それで、以下はもう少し複雑な wizard の例です:

… 実行例:

Would you like to delete file1.txt?
y<Enter>
Would you like to delete file2.txt?
n<Enter>
Would you like to delete file3.txt?
y<Enter>
Removing file1.txt
Removing file3.txt

以上に挙げた例では、ユーザーが要求された入力を全て入力し終えるまで、変更ができないアクションを実行するのは避けたいという要求があります。

モジュール性

最初の例を見直してみましょう:

この例は、実質的には2つの独立した wizard を組み合わせています:

  • 最初の wizard はユーザーの名前を要求し、表示している
  • 2つ目の wizard はユーザーの年齢を要求し、表示している

しかし、アクションを実行する前に全ての入力が必要だったので、2つの wizard のロジックをそれぞれ混ぜる必要がありました。

これら2つの wizard を別々に定義し、より大きな wizard に合体させる方法があったらどうでしょう? IOMonoid インスタンスの長所を活かせば可能です。こんな感じ:

このプログラムはさっきの例と完全に同じ動きをします。が、ユーザーの名前を扱うロジックは、ユーザーの年齢を扱うロジックと完全に分離されています。

この方法でうまくいくのは、それぞれの wizard を2つの部分に分けたからです:

  • リクエストの部分 (ユーザーに入力を求める部分など)
  • レスポンスの部分 (その入力に応じたアクションを実行する部分など)

… そしてそれぞれの wizard に IO (IO ()) という型を与えることによって、型レベルでこれを実現しています:

外側の IO アクションは“リクエスト”です。リクエストが終了したとき、外側の IO アクションは内側の IO アクション、つまり“レスポンス”を返します。例えば:

wizard は (<>) 演算子を使って組み合わせることができます。IO アクションに限って言うなら、以下のような動作をします:

言い換えるなら、IO アクションを2つ組み合わせるということは、それぞれの IO アクションを実行して結果を組み合わせるということなのです。これは、2つの IO アクションをネストさせると、アクションを実行して結果を組み合わせるという処理を2回実行する、ということも示しています:

つまり、2つの wizard を組み合わせると、リクエストを組み合わせてレスポンスも組み合わせたことになるのです。

この方法は2つ以上の wizard でもうまくいきます。例えば:

これをアクションの形で表現するために、さっきの例をもう一度見てみましょう:

nameage はかなり似ているので、共通の関数を使うような実装にすることができますね:

2つの wizard のロジックが混ざっていたとき、この共通化のロジックを使うことはできませんでした。しかしロジック毎に別々の wizard に分割すると、プログラムを小さくするための共通構造を突くことができます。

このプログラムの圧縮によって、簡単に新しい wizard を追加することができます:

… そして、モノイド関連の標準ライブラリ関数を活用しましょう。例えば foldMap を使えば wizard を大量に作ることができます。

より重要なのは、プログラムが何をしているのか一目瞭然になりました。読みやすさは書きやすさに比べ、大きな美徳です。

最後の例

ファイル削除の例も同じ観点から見直してみましょう:

さっきと同じパターンで、シンプルにすることができます:

やるべきなのは、1つのファイルに対して処理を行う wizard を定義すること、そして foldMap を使って wizard を大量に生成することだけです。IOMonoid インスタンスは、全てのリクエストを束ねて表示し、後で選択したファイルを削除してくれます。

結論

ユーザーが望む wizard の全てにこのパターンが適用できるわけではありません。例えば、wizard が互いに依存しているような状況では、このパターンはすぐに使い物にならなくなります。しかし、このパターンは MonoidIO インスタンスを他の Monoid のインスタンスと (もしくは自分自身と!) 連結させ、新しい動作を生成するような一例にはなっています。