RIOパターン
gpt-5.icon
Haskell の RIO pattern は、
「副作用・依存関係・設定をすべて Env(環境)にまとめ、ReaderT Env IO(= RIO Env)をアプリ全体の基盤にする設計パターン*
という考え方です。
実務向け Haskell でよく使われるアーキテクチャで、目的は主に次の3つです。
依存関係を明示的にする
テストしやすくする
大規模化しても破綻しにくくする
まず基本形。
code:hs
newtype App a = App
{ runApp :: ReaderT Env IO a
}
deriving (Functor, Applicative, Monad, MonadIO, MonadReader Env)
ここで:
Env … アプリ全体の「環境」
App … アプリ用のモナド(実体は ReaderT Env IO)
という構造になります。
1. Env に「全部」入れる
Env には以下を集約します。
設定値(Config)
Logger
DB connection
外部 API client
各種ハンドル
例:
code:hs
data Env = Env
{ envConfig :: Config
, envLogger :: Logger
, envDB :: Connection
}
そして必要な場所では
code:hs
ask >>= \env -> ...
または
code:hs
view envDB
のように取得します。
重要なのは:
👉 グローバル変数を使わず、全部 Reader 経由
という点です。
2. 「依存」を型で表現する
RIO pattern では「この関数は何に依存しているか」を型で表します。
典型例:
code:hs
foo :: HasDB env => RIO env ()
という形。
HasDB は typeclass:
code:hs
class HasDB env where
dbL :: Lens' env Connection
これにより:
foo は「DB を持つ env」ならどこでも使える
巨大な Env に直接依存しない
という疎結合になります。
これはかなり重要な設計ポイントです。
3. 純粋ロジックと副作用を分離
基本方針:
ビジネスロジック:純粋関数
IO / DB / API:RIO
例:
code:hs
calculateScore :: User -> Int
calculateScore = ...
saveUser :: HasDB env => User -> RIO env ()
こうすることで:
ロジックは普通にユニットテスト可能
RIO 部分だけモック Env でテスト
という構造になります。
4. main は「組み立て係」
main では:
1. Config 読む
2. Logger 作る
3. DB 繋ぐ
4. Env 作る
5. runRIO
だけやります。
code:hs
main :: IO ()
main = do
config <- loadConfig
logger <- newLogger
db <- connectDB
let env = Env config logger db
runRIO env app