Template Haskell でいい感じに時間の長さを書けるライブラリ duration を作った

Template Haskell を使って,時間の長さをいい感じに(人間が読みやすい形で)記述できるライブラリを作った.

hackage.haskell.org

github.com

(実は GW 前くらいには既に公開していたけど,ブログを書く余裕がなかった……)

モチベーション

JavaScript のライブラリに,ms というものがある.

www.npmjs.com

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 などの文字列表現を受け取り,コンパイルの段階で,それを数値として埋め込むことが可能になる.

準クォートについては,前回の記事を参照のこと.

ryota-ka.hatenablog.com

ユースケースその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 という型を持つが,IntRelativeDurationインスタンスになっているのでうまくいく.他にも DoubleInteger などがインスタンスになっている.

ユースケースその2

HTTP サーバの実装時に,Web.Cookie を用いて,レスポンスに Set-Cookie ヘッダを含める場合を考えよう.Cookie の Max-age は,time package が提供する 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 型を持ち DiffTimeNominalDiffTimeCUSeconds など,単位を伴った時間の長さを表す型がインスタンスになっている.

その他

id:hiroqnOCaml バージョンを作ったらしい.お楽しみに.

(2018/06/30 追記) 公開された

qiita.com