任意の関数をTemplateHaskellでコンパイル時に実行して安全性を確保する

動機

論理的に安全だと分かるが、コンパイラで安全性が保証できないようなことを、少しでも保証したいという思いからです。

コンパイラが保証できない例

以下のようなsettingsがあったときに、

settings :: [(String, String)]
settings = 
  [ ("http.port", "8080")
  , ("http.host", "example.com")
  ]

以下の式は失敗せずにうまくいきます。

fromJust (lookup "http.port" settings) == "8080"

ですが、fromJustは引数がNothingのときに失敗するため安全ではないです。

fromJust :: Maybe a -> a
-- (import Data.Maybeする必要あり)

浅い階層でfromJustを使っていれば、実行してすぐにエラーに気がつけると思いますが、ある関数が呼ばれるまでは評価されない状況などでは、とても不安になります。(将来の変更でキーの名前が"http.port"から"port"に変更になったりすることも考えられ、そういうときにコンパイルして、変更すべき箇所をすぐ検出したいですよね)

TemplateHaskellでコンパイル時実行

上記の解決策として、型レベルプログラミングで頑張るという案もあると思います。

それよりも、fromJust (lookup "http.port" settings)コンパイル時に実行してしまう方がより直感的で分かりやすいスタイルになると思ったので模索しました。
コンパイル時に実行すれば、"http.port"というキーが無いときにはコンパイルしたタイミングで検出できるようになります。

書き方は以下でOKです。
$(lift (fromJust (lookup "http.port" settings)) )

全部書いた例を以下に載せます。

Settings.hs
module Settings where

settings :: [(String, String)]
settings = 
  [ ("http.port", "8080")
  , ("http.host", "example.com")
  ]
Main.hs
{-# LANGUAGE TemplateHaskell #-}

import Data.Maybe
import Settings
import Language.Haskell.TH.Syntax (lift)


main :: IO ()
main = do
    -- (from: https://www.reddit.com/r/haskell/comments/7yvb43/ghc_compiletime_evaluation/)
    let p = $(lift (fromJust (lookup "http.port" settings)) )
    print p
出力
"8080"

一つ注意点は、TemplateHaskellの都合上、変数settingsを別モジュールに定義して、importする必要があることです。

試しにわざと
"http.part"とタイポしちゃった場合は、ちゃんとコンパイル時にコンパイルエラーがでるので、間違いにすぐ気づけます!

-- (タイポしちゃった)
let p = $(lift (fromJust (lookup "http.part" settings)) )
コンパイルエラー
    • Exception when trying to run compile-time code:
        Maybe.fromJust: Nothing
      Code: lift (fromJust (lookup "http.part" settings))
    • In the untyped splice:
        $(lift (fromJust (lookup "http.part" settings)))
   |         
26 |     let d = $(lift (fromJust (lookup "http.part" settings)) )
   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

サンプルリポジトリ

この記事にある、コードをリポジトリにしたものがここにあります。
手元でコンパイルエラーするかどうか試したりするに役立ててください。

https://github.com/nwtgck/compile-time-evaluation-example-haskell

他の書き方

以下のようにも書いても、同じように働きました。

-- (from: https://www.reddit.com/r/haskell/comments/1kpu1h/can_haskell_programs_be_compiled_such_that_the/)
let p2 = $( let x = fromJust (lookup "http.port" settings)  in [| x |] )

すこし、xを定義したり、冗長に感じたので、最初のものを先に紹介しました。

おわりに

この方法なら、グローバル変数を引数にとる任意の関数を実行時に評価することが、できるため、利用範囲が広そうです。
もっと直感的で簡素な書き方を模索中・募集中です。

参考

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.