再入門Haskell(前編)

(私はあんまり広く情報収集したりしないので、
ここに書く内容はとうによく知られた問題なのかもしれないが、あしからず…)


プログラムの実行において、何らかの外部的な情報
(つまりIOを介して得られる情報)を
そのプログラム全体から参照したいケースというのがある。
典型的な例がプログラムのコンフィギュレーションで、
たとえば、設定ファイルから設定値を読み込み、そのパラメータを用いて実行する、
あるいはコマンドライン引数にしたがって動作する、
またあるいは、環境変数レジストリ、などを参照する場合である。
このような設定はプログラム中のどこで参照されるか
あらかじめ分かっているとも限らないので、
どこからでも気軽に参照できるのが望ましい。
実際のところ、一般的な言語ではこのような設定は
グローバル変数にしてしまうか、
あるいはグローバル変数を嫌うなら
シングルトンな設定クラスを作るか、
そうでないクラスを作ってそれを使う可能性のあるクラスに
持ちまわさせるなどでも良いが、
いずれにせよ、利用されると考えられるよりかなり大きなコードの範囲で
参照可能な変数として置くのが普通である。


ところで、Haskellは参照透明な言語なので、
グローバルなところに変数は置けない。
置けないというのは正しくは無いが、
参照透明のためにいわゆるmutableな変数は置けない。
Haskellには(いわゆるオブジェクト指向で言うところの)クラスは無いので、
グローバル以外の変数のスコープは、
関数の引数、ローカル変数の持つものしかない。
まずはじめに、次のようなプログラムを考える。
(これ以降に示すプログラムは解説用のため、
コンパイルおよび実行を行っていないことをあらかじめお断りします)

server = "www.hoge.com"
portNumber = 8888

main = do
  h <- connect
  process h

connect = do
  return $ connectTo server (PortNmber portNumber)

process h = do
  printf "recv from %s:%d...\n" server portNumber
  dat <- hGetContents h
  putStr dat
  hClose h

サーバのアドレスとポート番号を指定されて、
そこに接続してなにかデータを取ってくるというプログラムのつもりである。
ここでは、アドレスとポート番号はとりあえず定数にしてしまって、
ひとまず動くものを作ったというシナリオを想定していただきたい。
そのような理由で、後々この部分は可変にしたい。
定数の場合、他の言語と同じく、このように
グローバルなところに変数を置くことにより簡単に実装が出来る。
しかし、これを可変にしようとするととたんにこのままでは立ち行かなくなってしまう。
Haskellでこれを行うのには大体三つほど方法が考えられる。


まず一つ目、必要な箇所全てにパラメータを渡してやる方法。

main = do
  server <- getServer
  portNumber <- getPortNumber
  h <- connect server portNumber
  process h server portNumber

connect s p = do
  return $ connectTo s (PortNmber p)

process h s p = do
  printf "recv from %s:%d...\n" s p
  dat <- hGetContents h
  putStr dat
  hClose h

設定は何らかのIO処理をして取得するものとする。
まずはじめに設定を取得して、それを用いる全ての関数に渡してやることによって実現する。
安易な方法だが、これでは大いに問題があって、
このサンプルでは関数が2個しかないが、もっとたくさんあった場合、
それら全てに設定を伝播してやる必要があるので、
とんでもなく面倒になってしまう。
そもそも、そのようなものを関数の引数として渡すのは
なんだかおかしいようにも感じられる。
プログラムを少し変形すると、同じ考え方で
各関数の引数に受け渡すことなく実装することも出来る。

main = do
  server <- getServer
  portNumber <- getPortNumber
  foo server portNumber

foo s p = do
  h <- connect
  process h

  where
  connect s p = do
    return $ connectTo s (PortNmber p)

  process h s p = do
    printf "recv from %s:%d...\n" s p
    dat <- hGetContents h
    putStr dat
    hClose h

このように一つの大きなクロージャにしてしまうと
一つ一つに渡す必要はなくなる。
しかし、これではその設定項目を参照する関数を
全て同じ関数の中におく必要があり、
全体をインデントしなければいけない。
字面的な問題は些細といえば些細なのだが、
設定を参照する関数全てをまとめないといけないということから
モジュラリティにかなり難がある。
また、もう一つの問題点として、
この方式では実行中に設定項目を
プログラム自身によって書き換えるということに対応できない。
たとえば、GUIアプリケーションなどは
設定画面によって実行中に設定を変えるというのは珍しくない。


二つ目の方法として、Stateモナドを使うものが考えられる。
設定を状態とすれば、設定は明示的に持ちまわる必要はなくなるし、
実行中の書き換えにも対応できる。
この方法の欠点は、設定値がモナドになるということだろうか。
設定値を別の関数に渡したり、文字列に操作を行うなど、
何をする前にもモナドを外す必要がある。
また、設定値の持ち回りのために計算がモナドで覆われるので、
さらに別のモナドを使いたい場合、よくあるものとしてIOを行いたい場合は
モナド合成を行ったうえで、各IO処理をリフトさせる必要が出てくる。
これが意外と煩雑でたくさんコードを書いているとよく書き忘れる。
しかも、書き忘れたときのGHCのエラーメッセージが
大抵びっくりするほど分かりずらい。
おそらく間違った型のモナドとして型推論が進んでしまって、
まったく別の場所でエラーが表示されるからなのだが…。
それとは別に、この方法でも設定を使うコードを特定のモナドにする必要があるので、
モジュラリティの点からもあまり良いとは言えないように思える。


これらいずれも元からコードがかなり変わる上に
モジュラリティに難がある。
はじめの例のように、設定固定でプロトタイプを作ってから
可変にするのはなかなか大変である。
そこで、三つ目の方法として、元のプログラムをほとんど変えずに設定に対応させるものを考える。
Haskellは参照透明ではあるが、
それゆえに、設定がリードオンリーなら、割と簡単に実装することが出来る。

configFileName = "hoge"

loadConfig = do
  dat <- readFile configFileName
  [s,p] <- lines dat
  return (s,read p)

(server,portNumber) = unsafePerformIO loadConfig

main = do
  h <- connect
  process h

connect = do
  return $ connectTo server (PortNmber portNumber)

process h = do
  printf "recv from %s:%d...\n" server portNumber
  dat <- hGetContents h
  putStr dat
  hClose h

unsafePerformIOの出番というわけである。
この場合、loadConfigは設定値の初回参照時に実行され、
server,portNumberはその時に決定される。
グラフ簡約なので、一度簡約されるともうloadConfigが実行されることは無い。
それゆえに、このような実装で全く問題なく動作する。
configFileNameもパラメータで渡したい場合は、
同じようなことをもう一段行えばよい。

configFileNameRef = unsafePerformIO $ newIORef ""

setConfigFileName s = writeIORef configFileNameRef s

configFileName = unsafePerformIO $ readIORef configFileNameRef

loadConfig = do
  dat <- readFile configFileName
  [s,p] <- lines dat
  return (s,read p)

(server,portNumber) = unsafePerformIO loadConfig

main = do
  [conf] <- getArgs
  setConfigFileName conf
  h <- connect
  process h

connect = do
  return $ connectTo server (PortNmber portNumber)

process h = do
  printf "recv from %s:%d...\n" server portNumber
  dat <- hGetContents h
  putStr dat
  hClose h

コマンドライン引数からファイル名を指定する例である。
ファイル名の実体としてはIORefで持っておき、
ファイル名の初回参照時にreadIORefで取り出す。
つまり、設定を必要とする計算の全てに先立って
setConfigFileNameを行えば、
以降の計算は全てうまく行われる。


このように、リードオンリーの場合においては
かなり理想的な実装ができる。
この実装にそれ以外の欠点は特に思い当たらないのだが、
設定の書き換えが必要になった場合はこの方法は使えない。
次回、設定の書き換えありで(割と)うまく動作する実装を示す。