動機
論理的に安全だと分かるが、コンパイラで安全性が保証できないようなことを、少しでも保証したいという思いからです。
コンパイラが保証できない例
以下のような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)) )
全部書いた例を以下に載せます。
module Settings where
settings :: [(String, String)]
settings =
[ ("http.port", "8080")
, ("http.host", "example.com")
]
{-# 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
を定義したり、冗長に感じたので、最初のものを先に紹介しました。
おわりに
この方法なら、グローバル変数を引数にとる任意の関数を実行時に評価することが、できるため、利用範囲が広そうです。
もっと直感的で簡素な書き方を模索中・募集中です。