Template Haskellを使って,時間の長さをいい感じに(人間が読みやすい形で)記述できるライブラリを作った.
モチベーション#
JavaScriptのライブラリに,msというものがある.
READMEを読めば一目で使い方が把握できるが,以下のなことができる.
ms('2 days'); // 172800000
ソースコード中に1000 * 60 * 60 * 24 * 2などと記述するのも悪くはないが,やはり自然言語表現の方が読み手にとって親切だ.
ただし,この方法には一つ問題があって,文字列はパーズに失敗しうる.
> const ms = require('ms')
undefined
> ms('1d')
86400000
> ms('foo')
undefined
パーズできない文字列を引数として与えると,undefinedが返ってくる.実行時まで評価されない以上,避けられない問題ではあるが,明らかにバグの温床となりうる.
コンパイル時に検証する#
実行時までパーズの成否がわからないのであれば,コンパイル時にそれをやってしまえばよい.この解決策は,動的に組み立てられた文字列を引数として受け取る能力を捨ててしまうことになるが,ほとんどのユースケースでは,リテラルが与えられるであろうという想定に基づき,今回は動的な呼び出しはサポートしないことにする.Template Haskellの準クォートを使えば,任意の文字列を受け取り,パーズした上で,Haskellの式に変換することができるので,2 daysなどの文字列表現を受け取り,コンパイルの段階で,それを数値として埋め込むことが可能になる.
準クォートについては,前回の記事を参照のこと.
ユースケースその1#
以下は1秒毎に"hey!"という文字列を標準出力に出力するプログラムの例である.
import Control.Concurrent (threadDelay)
import Control.Monad.Fix (fix)
main :: IO ()
main = fix $ \loop -> do
putStrLn "hey!"
threadDelay 1000000
loop
threadDelayは,Int型の値を受け取り,指定された長さだけ現在のスレッドの実行を停止するが,この際に受け取るのはマイクロ秒である.ソースコード中に現れる1000000という値が1秒間という長さを表していることは必ずしも自明ではない.しかし,durationを使えば以下のように記述することができる.
{-# LANGUAGE QuasiQuotes #-}
import Control.Concurrent (threadDelay)
import Control.Monad.Fix (fix)
import Data.Time.Clock.Duration (µs)
main :: IO ()
main = fix $ \loop -> do
putStrLn "hey!"
threadDelay [µs| 1s |]
loop
[µs| 1s |]の意味するところは,「1秒間という長さをマイクロ秒に換算した値」くらいのところである.この式自体は,RelativeDuration a => aという型を持つが,IntがRelativeDurationのインスタンスになっているのでうまくいく.他にもDoubleやIntegerなどがインスタンスになっている.
ユースケースその2#
HTTPサーヴァの実装時に,Web.Cookieを用いて,レスポンスにSet-Cookieヘッダを含める場合を考えよう.CookieのMax-ageは,timepackageが提供するDiffTime型で与えることができるが,durationを使えばこれを非常に簡潔に記述することができる.
import Data.Default (def)
import Data.Time.Clock.Duration (t)
import Web.Cookie (setCookieMaxAge)
handleRequest = do
...
let cookie = def { setCookieMaxAge = Just [t| 2 weeks |] }
...
準クォート自体が表す式自体は,AbsoluteDuration a => a型を持ちDiffTimeやNominalDiffTime,CUSecondsなど,単位を伴った時間の長さを表す型がインスタンスになっている.
その他#
@hiroqnがOCamlヴァージョンを作ったらしい.お楽しみに.
(2018/06/30追記)公開された
