HaskellにおけるConfigurations Problemを解決する

Posted on June 3, 2014

以下の論文が元ネタなので適宜参照されたし。

Configurations Problemとは

Configurations Problemとは、ユーザー設定のような実行時データをいかにしてプログラム全体で利用できるようにするか、という問題。単純な問題なんだけど、Haskellのような純粋関数型プログラミング言語では意外と解決が難しい。純粋でない関数型プログラミング言語(例えばOCaml)なら、副作用を使って簡単に解ける。

(* 設定の型定義 *)
type config = { foobar : int }

(* デフォルト設定 *)
let default_config = { foobar = 123 }

(* 設定の参照 *)
let config = ref default_config

(* ファイルなどから設定を読み込む *)
let read_config : unit -> config = fun () -> ...

(* 設定を使う何らかの関数 *)
let f () =
  print_int !config.foobar

let () =
  config := read_config ();
  f ()

見ての通り、カジュアルにconfigにアクセスしている。Haskellだとこうはいかない。典型的な解決策は設定データを関数の引数にして持ちまわす方法。

data Config = Config { foobar :: Int }

readConfig :: IO Config
readConfig = ...

f :: Config -> IO ()
f config = do
  print $ foobar config

main :: IO ()
main = do
  config <- readConfig
  f config

小さいプログラムならこれで十分だけど、プログラムの奥深くまで設定を伝播しないといけない場合などに、いちいち引数で持ちまわすのが面倒くさい。こういうときにunsafePerformIOを使いたくなってしまう。

import System.IO.Unsafe (unsafePerformIO)

data Config = Config { foobar :: Int }

readConfig :: IO Config
readConfig = ...

config :: Config
config = unsafePerformIO readConfig
{-# NOINLINE config #-}

f :: IO ()
f = print $ foobar config

main :: IO ()
main = f

どうみても危険です本当にありがとうございました。

件の論文は、他の解決策(ReaderモナドとかGHCのImplicitParameter拡張とか)も示しているけど、どれもいまいちで使えないから、代わりに型クラスを使った華麗な解決策を提案するぜ、という流れになっている。

なぜ型クラスを使うのかは後で説明するとして、Hackageにはまさにこの問題を解くためのパッケージがある。その名もreflectionと言い、Edward Kmett先生作のありがたいパッケージだ。

reflectionを使った解決策

細かい説明は面倒なので省略するけど、reflectionのGivenという型クラスを使うと華麗に解決できる。

{-# LANGUAGE FlexibleContexts #-}

import Data.Reflection (Given, give, given)

data Config = Config { foobar :: Int }

readConfig :: IO Config
readConfig = ...

f :: Given Config => IO ()
f = do
  print $ foobar config
  where config = given :: Config

main :: IO ()
main = do
  config <- readConfig
  give config f

要点は3つ。

  1. 設定データが必要な関数にGivenの型クラス制約をつける
  2. 設定データの取得はgivenを使う
  3. 設定データを与えるにはgiveを使う

これであら不思議、設定データを明示的に持ちまわす必要がなくなったし、unsafePerformIOを使ってないので安全になった。

なぜ型クラスを使うのか

簡単に言えば、設定データを引数で持ちまわすのが面倒くさいんだから、型クラスの機構を使ってそれを自動的に達成すればいいのではないか、という発想らしい。(誤読していたらすみません)

要は、設定データを型レベルに持ち上げて、型クラスの制約によってプログラム全体に伝播し、設定データが必要な場面で値レベルに落とせばいいということらしい。

詳細は論文に書いてある。

reflectionの魔法

元の論文は、設定データを型レベルに持ち上げる前提で話を進めているのだけど、現実的にはそれ自体が結構難しい。設定データは普通、外部ファイルから読み込んで利用するので、それだけで依存型プログラミングバリバリのとんでもないものができてしまう。(依存型に詳しくないのでちょっと想像できない)

reflectionは、型クラスの内部実装まで踏み込んだものすごい魔法を利用して、そのあたりの問題を回避している。どういう魔法かというと、型クラス制約のついた関数は、実行時ディスパッチの実現のために内部的に辞書と呼ばれる暗黙のパラメータを受けとるように解釈されることを利用して、設定データを無理矢理に辞書に詰め込むことで型クラスの機構に設定データの持ちまわしを肩代わりさせているのだとか。

詳細は以下の記事に述べられているので参照してほしい。