チェシャ猫の消滅定理

数学にプログラミング、素敵なもの何もかも。

現在時刻をモックする Haskell ライブラリ time-machine を作ってみました

主としてテスト時のために、現在時刻を操作する Haskell ライブラリを作成しました。Hackage にも登録済みです。

github.com

試しに次のコードを実行してみましょう。getCurrentTime しているはずなのに、返ってくる値が 1985 年 10 月 26 日になっているはずです。

module Main where

import Control.Monad.TimeMachine
import Control.Monad.Trans ( liftIO )

main :: IO ()
main = backTo (the future) $ do
    t <- getCurrentTime
    liftIO . putStrLn $ "We are at " ++ show t

作成の動機

一般論として、現在時刻に依存する関数やメソッドはテストが難しくなります。例えば次の関数を考えましょう。

getGreeting :: IO String
getGreeting = do
    t <- getCurrentTime
    if utctDayTime t <= 12 * 60 * 60
        then return "Good morning"
        else return "Hello"

この関数は午前中には "Good morning" を、午後には "Hello" を返しますが、時刻に依存して結果が変わってしまうため当然このままではテストできません。最初から時刻を引数として渡すようにするのも一つの方法ではありますが、今回はちょっと別の選択肢を考えます。

今回作成したライブラリ time-machine を用いると、関数の中身はそのまま型だけ変更して

getGreeting :: (MonadTime m) => m String
getGreeting = do
    t <- getCurrentTime
    if utctDayTime t <= 12 * 60 * 60
        then return "Good morning"
        else return "Hello"

としておくことで、内部の getCurrentTime が返す時刻を自由に操作できるようになります。もちろん、普通に IO モナドのコンテクストでこの関数を呼んだ場合には普通に現在時刻を返して来るようになっています。

ちなみに、Ruby ではテスト時に現在時刻をモックするための gem として以下の 2 つがよく知られています。

今回のライブラリはこの gem から着想を得ています。モナドによる DSL を用いることで、同様の効果をより Haskell らしい方法で実現することを目指しました。

使い方

ライブラリ time-machine が提供する主な関数は travelTojumpToaccelerate の 3 つです。使用することでモナドのコンテクストに入り、コンテクスト内で getCurrentTime など現在時刻に依存する IO アクションを使用するとモックされた値が返ってきます。

以下では具体的な使い方について説明します。なお各関数は独立した効果を持ちますが、モナド入れ子にすることで複数の効果を同時に得ることも可能です。

travelTo

現在の(グローバルな)時刻を変化させます。タイムゾーンは変わりません。timecop の travel に相当します。

main = travelTo (oct 26 1985 am 1 24) $ do
    getCurrentTime >>= (liftIO . print)

このコードでは、現在のタイムゾーンにおける 1985 年 10 月 26 日 AM 1:24 を指定しています。getCurrentTimeタイムゾーンに関係なく UTC を返すので、実際には時差を補正した時刻が表示されることになります。

「行き先」となる時刻の指定にはいくつかの方法があります。

  1. 直接 UTC を指定する
  2. 現在のタイムゾーンにおける時刻を指定する(上の例)
  3. 現在との相対時刻を指定する

行き先を指定するための DSLControl.Monad.TimeMachine.Cockpit モジュールに定義されており、例えば travelTo (3 `days` ago) のような自然言語っぽい記述ができるようになっています。ちなみにこの記事の冒頭で登場した backTotravelToエイリアスです。

jumpTo

travelTo とは逆に、現在時刻 (UTC) を保ったまま loadLocalTZ の結果を変化させます。

import qualified Data.Time.Zones as TZ

main = jumpTo "Asia/Shanghai" $ do
    t  <- getCurrentTime
    tz <- loadLocalTZ
    liftIO . print $ TZ.timeZoneForUTCTime tz t  -- CST

なお loadLocalTZ だけではタイムゾーンが確定しないことに注意しましょう。これは、同じ地域でも UTC によってサマータイムになるかどうかが変わるためですが、time-machineサマータイムも含めて正しく扱えるようになっているはずです。

accelerate

時間が進む速さを変化させます。timecop の scale に相当します。

main = accelerate (x 60) $ do
    getCurrentTime >>= (liftIO . print)  -- (1)
    liftIO . threadDelay $ 1000 * 1000
    getCurrentTime >>= (liftIO . print)  -- (2)

このコードでは実時間 1 秒(1000 * 1000 マイクロ秒)のディレイが入っていますが、(2) で表示される時刻は (1) で表示される時刻の約 1 分後になります。これは accelerate (x 60) の効果で、コンテクスト内部の時間が 60 倍に加速しているためです。

なお、accelerate の特殊な場合として halt が用意されています。コンテクスト内で時間のかかる処理を行っても時刻が変化しなくなるため、travelTo を組み合わせて使用すると、テストしたい処理自身の実行にかかる時間を無視して狙った時刻をピンポイントに作り出すことができます。timecop の freeze に相当する機能です。

main = halt $ do
    travelTo (jan 1 1970 am 0 0) $ do
        ....

仕組み

裏側の仕組みはシンプルで、型クラスを使うことでコンテクストによって挙動を変化させています。型クラス MonadTime には、モナドのコンテクスト内部において時刻の情報を返すための関数が定義されています。

class (Monad m) => MonadTime m where
    getCurrentTime      :: m T.UTCTime
    getCurrentTZ        :: m TZ.TZ
    getCurrentTimeScale :: m TimeScale

実際にモックされた時刻のコンテクストを保持しているのはモナド変換子 TimeMachineT で、実装には ReaderT を流用しています。

getCurrentTZgetCurrentTimeScale は一つのコンテクスト内では変化しないので実質単なる ask をそのまま使っていますが、getCurrentTime は「travelTo したあとそのコンテクスト内で経過した時間」が必要なので別途算出しています。

instance (MonadIO m) => MonadTime (TimeMachineT m) where
    getCurrentTime = TimeMachineT $ do
        realCurr <- liftIO T.getCurrentTime
        Spacetime simOrigin realOrigin _ scale <- ask
        let diff = scaledDiffUTCTime scale realCurr realOrigin
        return $ T.addUTCTime diff simOrigin

    getCurrentTZ        = TimeMachineT $ ask >>= return . stTZ
    getCurrentTimeScale = TimeMachineT $ ask >>= return . stTimeScale

travelTojumpToaccelerate の実体はこの TimeMachineTrun するための関数です。

IO もまた MonadTimeインスタンスになっており、かつ本物の getCurrentTimegetCurrentTZ が実装として指定されているため、IO 内で呼ばれた場合には真の現在時刻が返る、という仕組みになっています。

まとめ

今回作成したライブラリ time-machine を使用すると、deloreantimecopRuby gem と同様、現在時刻をモックして時刻依存の関数の挙動を外から操作できるようになります。

内部では型クラスを用いて実装されており、コンテクストによって getCurrentTime の挙動が変わることを利用しています。

なお、型クラスとモナド変換子を同様の考え方で用いることで、時刻に限らず一般に副作用をモックするライブラリとして monad-mock があります。こちらは Template Haskell を使っていたりしてもっと複雑ですが、それはまた別の話。

技術書典 3 で新刊落としました

先日、秋葉原で開催された技術系同人誌のオンリーイベント 技術書典 3 にサークル参加しました。

本当は新刊として Scala 用の静的解析ツール Stainless の入門書を頒布する予定で、サークルカットも完全にその線で準備していたのですが、残念ながら諸事情につき完成しませんでした。

事前にサークルチェックしてくださっていた 36 名の方々には大変申し訳ありませんでした。チェシャ猫先生の次回作にご期待ください。

何も並べるものがないのはちょっとどうかと思ったので、当日は 前回の技術書典 2 で頒布 した既刊『入門 LiquidHaskell』の PDF 版(ダウンロードカード)を持ち込みました。

頒布実績は終了時刻の 17 時まで粘って 31 部です。前回は 14 時頃に準備した 50 部が完売だったので、単純に計算すると 30% 程度のペースだったことになりますね。正直なところ、特殊な題材の割に、既刊にしては思ったより数が出たなという印象です。

なお、BOOTH での PDF 版の委託頒布は引き続き行っていますのでよろしくお願いします。

dodgsonlabs.booth.pm

今回新しく運営サイドから決済アプリが提供されましたが、手元に Android 端末がない(iPhone 未対応)こともあり、また直前にアナウンスされたこともあって今回は対応しませんでした。お隣のサークルが割とアプリ決済されていたようだったので、次回は検討してみようと思います。

また、「立ち読みコーナー」が新設されました。あらかじめ運営に 1 部(見本誌とは別に)提出しておくと、メイン会場とは別に用意された部屋に並べてくれる仕組みです。上にも書いた通り今回は物理書籍の頒布予定がなかったので、キンコーズで PDF から中綴じ印刷して立ち読み用として提出しました。

さて、今回お蔵入りになった Stainless ネタはどうしましょうね? おそらく次回の技術書典 4 は来年 4 月頃に開催されるはずですが、それはまた別の話。

Serverless Meetup Tokyo #6 で Kubernetes について話してきました

先日行われた Serverless Meetup Tokyo #6 で、Kubernetes 上で動作する Serverless フレームワーク Fission について発表してきました。

www.slideshare.net

先週も 似たようなブログ を書いたような気がしなくもないですが、KubelessFission を比較した前回の発表に対し、今回は話題を Fission に限定しています。一方で追加要素として、複数の Function を組み合わせるためのアドオン Fission Workflows を紹介してみました。

コンテナの再利用

スライド中でも言及している通り、一度「特殊化」されたコンテナは一定時間そのまま起動しており、再度同じ Function が呼び出された際に再利用されます。すなわち Function がコンテナ内部の環境に対して副作用を持つ場合、良くも悪くも次の呼び出しに影響するはずです。これを実際に確認してみましょう。

以下ではすでに 公式手順 に従って Fission が Kubernetes クラスタ上にデプロイされ、手元で fission コマンドが使用可能になっているとします。

Environment の作成

スライド中で挙げられていたコマンド例は Python でしたが、単純にするためにここではシェルスクリプトを使います。

$ fission env create --name binary --image fission/binary-env:0.3.0
Use default environment v1 API interface
environment 'binary' created

$ fission env list
NAME   UID                                  IMAGE
binary 00386979-b3c2-11e7-b4db-fa163ed87208 fission/binary-env:0.3.0

Function と Route の作成

コンテナが再利用されていることを確かめるために、コンテナ内の状態を変化させるような恣意的な Function を作成します。

以下のコードを counter.sh として保存します。なお公式で用意されている実行環境 fission/binary-env では Bash は使用できず、shebang に sh を指定する必要があるので地味に気をつけましょう。

#!/bin/sh

date >> /tmp/counter.txt
cat /tmp/counter.txt | wc -l

これで呼び出しごとにコンテナ内のファイルに追記が行われるはずです。このソースコードを指定して Function を作成します。

$ fission function create --name counter --env binary --code counter.sh
function 'counter' created

$ fission function list
NAME    UID                                  ENV
counter 217af646-b3c2-11e7-b4db-fa163ed87208 binary

さらに、この Function を呼び出すためのトリガーを作成します。

$ fission route create --method PATCH --url /counter --function counter
trigger '70a978a9-8cf9-42c3-a018-be0234785bc8' created

$ fission route list
NAME                                 METHOD URL      FUNCTION_NAME
70a978a9-8cf9-42c3-a018-be0234785bc8 PATCH  /counter counter

これでエンドポイントが定義されました。fission route create --help で出てくるオプション --method の候補に PATCH は入っていませんが、今回の例では期待した通りに動作します。

Function の実行

さて、この Function を実行してみると、

$ curl -X PATCH http://$FISSION_ROUTER/counter
1
$ curl -X PATCH http://$FISSION_ROUTER/counter
2
$ curl -X PATCH http://$FISSION_ROUTER/counter
3
$ curl -X PATCH http://$FISSION_ROUTER/counter
4

となり、実際に以前の実行の影響が残っていることがわかります。ちなみに 3 分ほど放置すると特殊化されたコンテナが消えるため、カウンタの値はリセットされます。

Kubernetes 側から直接確認してみましょう。プールされている 3 個 + 特殊化された 1 個で計 4 個の Pod があることが分かります。

kubectl -n fission-function get pods
NAME                                                              READY     STATUS    RESTARTS   AGE
binary-00386979-b3c2-11e7-b4db-fa163ed87208-glxmo1iv-189128r9t8   2/2       Running   0          6m
binary-00386979-b3c2-11e7-b4db-fa163ed87208-glxmo1iv-18912fhfp0   2/2       Running   0          2m
binary-00386979-b3c2-11e7-b4db-fa163ed87208-glxmo1iv-18912fpjtc   2/2       Running   0          8m
binary-00386979-b3c2-11e7-b4db-fa163ed87208-glxmo1iv-18912r3qwl   2/2       Running   0          8m

ひとつだけ AGE が若い Pod は、特殊化されて Deployment の管理下から抜けた Pod の穴を埋めるために新しく立ち上がったものです。今回の場合、Function counter 用に特殊化された Pod は 4 番目に表示されているもので、kubectl describe コマンドで確認するとラベルがひとつだけ付け替えられています。

$ kubectl -n fission-function describe pods/binary-00386979-b3c2-11e7-b4db-fa163ed87208-glxmo1iv-18912r3qwl
Name:       binary-00386979-b3c2-11e7-b4db-fa163ed87208-glxmo1iv-18912r3qwl
Namespace:  fission-function
Node:       calico-1/192.168.235.231
Start Time: Wed, 18 Oct 2017 05:05:48 +0000
Labels:     functionName=counter
        functionUid=217af646-b3c2-11e7-b4db-fa163ed87208
        poolmgrInstanceId=T2bveCV8
        unmanaged=true
...

実際にコンテナの中を覗くと、これまでの Function の呼び出しによって生成されたファイルが残っているはずです。

$ kubectl -n fission-function exec binary-00386979-b3c2-11e7-b4db-fa163ed87208-glxmo1iv-18912r3qwl cat /tmp/counter.txt
Defaulting container name to binary.
Use 'kubectl describe pod/binary-00386979-b3c2-11e7-b4db-fa163ed87208-glxmo1iv-18912r3qwl' to see all of the containers in this pod.
Wed Oct 18 05:12:20 UTC 2017
Wed Oct 18 05:12:38 UTC 2017
Wed Oct 18 05:12:39 UTC 2017
Wed Oct 18 05:12:40 UTC 2017

まとめ

Kubernetes 上の Serverless フレームワーク Fission について、間をおかず同じ Function が呼ばれた場合にコンテナの再利用が行われることを簡単な実験で確認しました。

なお発表中ではスライド 1 枚で済ませてしまいましたが、複数の Function でピタゴラスイッチできる Fission Workflows はなかなか面白い仕組みです。いずれちょっとしたサンプルを公開したいと思っていますが、それはまた別の話。

Kubernetes Meetup Tokyo #7 で Serverless について話してきました

先日行われた Kubernetes Meetup Tokyo #7 で、Kubernetes 上で動作する Serverless フレームワークについて発表してきました。

www.slideshare.net

Serverless on Kubernetes を謳うツールはいくつかありますが、今回はそのうち KubelessFission に焦点を当て、それぞれのアーキテクチャの違いを比較してみました。

当日の補足

Twitter 上で反応をもらった点についていくつか補足します。

Function の合成

回答になっているかどうかちょっと自信がないのですが、複数の Function を連鎖させるという意味であれば、ごく最近 Fission Workflow というツールがリリースされています。

github.com

Fission の追加コンポーネントになっていて、Function を他の Function のトリガにするための仕組みです。有名どころで例えるなら AWS Step Functions が近いでしょうか。

クラウド FaaS との比較

正直なところ自分も同じような印象を持ちました。クラウドベンダ各社が提供している FaaS の強みはマネージドサービスとの連携であって、その恩恵に預かれない Kubernetes はだいぶ Serverless のメリットが目減りしている感じです。

現状でメリットと言えなくもない点は以下のふたつ。

まず当然ですが、なんらかの政治的な事情によりパブリッククラウドが使用できない場合でも使用可能な点。どうせなら Serverless フレームワークと互換性があれば環境間の差異が吸収できるのですが、今のところ Fission はこの機能は提供していないようです。ちなみに Kubeless については Serverless Framework Plugin が提供されています。

もうひとつは、Kubernetes の Events をトリガとして使用できる点です。例えば Slack に通知するとか、外部サービスとのちょっとした連携には役に立つかもしれません。現状トリガに指定できる条件は Namespace、Kind、Label のみですが、前述の Fission Workflow と組み合わせて何かちょっとしたサンプルが作れないかと考えているところです。

総論

現状、ものすごく画期的だとは言い難いですが、とはいえ数週間に一回アーキテクチャごと変更されていたり、Fission Workflow のような新しいツールが動き始めていたりして、決して停滞しているわけではなさそうです。まあ今後に期待しておきましょう。

なお Fission Workflow についてはもう少し調べた上で記事にまとめるか、あるいはまた別の場所で LT にでも仕立てようと狙っていますが、それはまた別の話。

Haskell ライブラリにスターを送るツール thank-you-stars を作ってみました

HaskellGitHub レポジトリを眺めてみると、有名ライブラリであってもスター数が意外と少ないことがあります。かの Yesod ですら本記事執筆時点で 1,794 個であり、Rails の 36,933 個や Django の 28,165 個と比較すると文字通り桁違いです。

スター数は必ずしも OSS としての評価や価値を反映しませんし、そもそも Haskell ユーザの絶対数が少ないからと言ってしまえばそれまでなのですが、若干寂しい感じがしません?

一方、先日 id:teppeis さんが 依存しているライブラリにスターを送る npm ツール を公開されていました。そこで真似して作ってみた Haskell 版が以下です。

github.com

セットアップ後、自分の Cabal / Stack プロジェクトのルートディレクトリで実行するだけで OK。package.cabal から依存ライブラリを読み取り、それが GitHubホスティングされていればスターを付けにいってくれます。

なお、現在の実装では依存先ライブラリのレポジトリ情報をローカルの Hackage DB から取得しています。初めて package.cabal に記述したライブラリはローカルに存在しない可能性があるので、一度ビルドを通してから試してください。スターを付ける GitHub API は冪等なので重ねて実行しても大丈夫です。

ちなみに、このツール自体が予想以上にスターを集めてしまい、

f:id:y_taka_23:20170914143013p:plain

GitHub Trending で Haskell 部門トップになってしまったのですが、それはまた別の話。

JJUG CCC 2017 Spring で Haskell on JVM について話してきました

先日行われた JJUG CCC 2017 Spring で、JVM 上で動作する Haskell について発表してきました。

www.slideshare.net

メインになるコンテンツはふたつの JVM 言語、FregeEta です。

今回はあくまでも Java のイベントなので、発表前半では Haskell の基本概念、特にモナドについてそれなりの時間を割いて説明してみました。さらにそれを踏まえて後半ではモナドを利用した Java ライブラリの呼び出しに焦点を当て、Frege と Eta それぞれの戦略の違いについて解説しています。

そもそも Haskell on JVM というマニアックで Java そのものと無関係なテーマ、かつ時間的に最後の枠ということで、個人的には若干集客を危惧していました。しかしフタを開けてみれば文字通り満席という結果で、それなりに怖いもの見たさ需要はあったのかもしれません。

また、このイベントではアンケートシステムが実装されていて講演者は結果が見られるのですが、その中で「今日の中で一番参考になりました」「monad の解説として非常にわかり易かった」というコメントをくださった方がいました。少しでも誰かの役に立ったのであれば幸いです。

ちなみに、発表中ではあまり掘り下げていませんが、Frege と Eta で実用的なプログラムを書こうとする上で最も大きな違いは、GHC 拡張のサポートです。

Frege は初めから “Vanilla Haskell” がターゲットであると明言しており GHC 拡張がサポートされる予定はありません。一方 Eta は「実用的な機能」を標榜して積極的なサポートを宣言しており、そもそも Java ライブラリの呼び出しからして GHC 拡張を利用する仕組みになっています。

では具体的には Eta はどうやって GHC 拡張を利用しているのか? このあたりを掘り下げた解説もいずれこのブログで書きたいですが、それはまた別の話。

超技術書典で同人誌『入門 LiquidHaskell』を頒布できませんでした

先日、ニコニコ超会議内で行われた「超技術書典」にて、LiquidHaskell の同人誌でサークル参加してきました。

lh101.dodgsonlabs.com

技術書典 2 ではそこそこの部数が出た ので Haskell 同人誌の需要はゼロではないと踏んでいたのですが、びっくりするぐらい売れません でした。とりあえず後に続く人が同じ轍を踏まないように、今回の様子について記録しておきます。

頒布物

頒布した同人誌は以下の 2 種類です。前者は技術書典 2 で頒布したものの増刷、後者は id:kazeula さんから委託を受けた新刊です。

持ち込んだ(冊子版の)部数と実際に売れた部数は以下の通りでした。

  • 入門 LiquidHaskell : 持ち込み 12 部、実売 1 部
  • GHCJS 入門 : 持ち込み 32 部、実売 7 部

技術書典 2 では開始から 3 時間で 50 部実売できたことを考えると、だいぶ客足に差があることがわかります。『入門 LiquidHaskell』は再販なので売れ行きが落ちるのはわかるのですが、『GHCJS 入門』は新刊にもかかわらずあまり売れていません。

設営

ディスプレイは前回とほぼ同じですが、POP のデザインを変更して簡単な要約をつけてみました。上が今回の超技術書典、 下が前回の技術書典 2 です。

当日の様子

売り上げが芳しくなかった原因ですが、やはり客層というかイベントの趣旨の違いが大きかった、というのが実際に参加した印象です。

前回の技術書典 2 はオンリーイベントで、かつ秋葉原という好立地でした。一方、超技術書典はニコニコ超会議のいちコーナであり、しかも会場は都内から 1 時間はかかる幕張、さらにゲーム実況ステージの正面で大音響にさらされる配置です。

実際、売れるかどうか以前にブースに足を止めるお客さん自体がずっと少なかったように思います。もう少し一般ウケするテーマだったらまた違ったかもしれませんが、いかんせん Haskell は世間的にはマイナすぎました。

釣り糸は、魚がいそうな場所で垂らしましょう。

直販できます

というわけで冊子版の在庫があるので、物理的にコンタクトできる方であれば直販が可能です。冊子 + PDF ダウンロードコードで 800 円です。

『入門 LiquidHaskell』についてはとりあえず次回 5 月 21 日の Haskell-jp もくもく回に在庫を持っていきます。欲しい方は声をかけてください。

haskell-jp.connpass.com

なお PDF 版のみ(内容・価格は冊子付きと同じ)は引き続き BOOTH で委託販売していますので、こちらもよろしくお願いします。

booth.pm

ちなみに「次に書く同人誌」のテーマはすでに決まっていて、Haskell 絡みの内容になる予定です。お披露目は早くても冬のコミックマーケットになりますが、それはまた別の話。