副作用を含むコードで関数型のテクニックを利用

リチャード伊真岡氏:「Scala ZIOをバッチ処理で使ってみた」という発表をします。リチャード伊真岡と申します。マーベリック株式会社というところで働いています。

今日一番大事なことを最初に言おうと思います。発表の内容はどうでもいいので、私の名前だけ覚えていってもらえば満足です。

(会場笑)

ということで大事なことは済んだので、本題に移っていきたいと思います。

Scala ZIOの話なんですけど、この人はScalaのZIOというライブラリの作者でJohn De Goesさんという人です。

この関数型の最大のメリットの1つのtestabilityの話をブログでしているんですね。「"Functional programming ordinarily gives us the incredible ability to easily test our software."」。けっこう強いステートメントですね。

もう1人のDaniel Spiewakさん。

関数型界隈ですごく有名な方で、この人もすごく強いことを言っています。「"I would argue that the whole point of functional programming in general is testing"」と、かなり強いことを言っています。

というわけでみなさんもご存知かと思いますが、純粋関数はtestabilityが高い。インプットはだいたいアウトプットが出てくるだけなので、テストを書く時にはその予測されるアウトプットと比べればよいと。

ただ、実際のプログラムは副作用が入ってくるので「副作用はテストをするのが難しいですね」という話になります。

副作用を含むコードがあって、副作用と純粋関数呼び出しが混ぜこぜになるとテストはやりにくいので、ここで関数型のテクニックを使いましょう!

testabilityを向上させるために

そうすると、ZIOもこの流れに沿うんですけど、descriptionとinterpreterの分離ということでいきます。

色分けをすると、descriptionというのはScalaでやるとfor文をよく使うんですけど、プログラムを実際に走らせるコードじゃなくて、「こういう処理をします」という記述、descriptionだけをやります。

この部分はfor文を通過しただけではプログラムがは走らないんですね。このfor文をprogramDescriptionというvalに束縛して、unsafeRunというところに渡して初めてプログラムが走るというテクニックを使って書きます。

上のほうでプログラムを記述するdescriptionである木構造のようなものを作っています。それで、それを走らせて初めて副作用が発生するということです。

ちょっとここで動くから見てもらいたいんですけど……。

(アニメーションが流れる)

こういう木構造が出てきて、これをランタイムと組み合わせると初めて動きます。いいでしょう?

(会場笑)

次のスライドに行くかな? 行かない?

(会場笑)

ごめんなさい。次のスライドに行きますね。動画に凝り過ぎてスライドが……つい動かしたくなっただけです。ごめんなさい。これ1時間半ぐらい掛けて作ったんです。

(会場笑)

つい動かしたくなっちゃいました。ごめんなさい(笑)。

こういうdescriptionとinterpreterを分離してコードを書くテクニックで、Free MonadやTagless Finalと呼ばれるテクニックがあります。ZIOもその流れに沿っています。

なんですけど、とくにTagless Finalについて、ZIOの作者の人のJohn De Goesさんがちょっと批判をしているんですね。

「テストはそんな簡単じゃないよ、綺麗にならないよ」と。testabilityが良くならないという話をしていました。

なぜかと言うと、さっき言ったようにdescriptionを関数型で記述する、つまりツリー構造のデータを作ってもそのツリー構造が期待したアウトプットと比較できないわけですね。

例えばprintln、出力をするというコードが含まれる副作用を書いたとして、期待されるコードと動作とどう比較するかみたいなことがあって、それが比較できない。それでこういう話のブログを書いて「ちょっと問題がありますよ」みたいな話をしています。

そこで「ZIOが良いんじゃないか」という話をこの作者の人がしています。

Tagless Finalなどに比べてtestabilityが向上するんじゃないかという話をしていて、かつ、非関数型プログラマにも馴染みやすいというのは、このZIOというライブラリの大きな開発動機の1つになっています。

ZIOをバッチ処理で使ってみる

これを我々マーベリックがバッチ処理のところで使ってみたという話をしていきます。

ZIOの入門みたいなところで、こういう戻り値型がたくさん出てくるわけですね。R、E、Aとありまして、右からいきます。

右のAが、処理が成功したときに返ってくる型です。次の真ん中のEはFailure Type。これはThrowableであったり、それを継承したものであったり、処理が失敗したときのExceptionが起こったときに投げられる型です。最後の左はEnvironment Typeと、ZIOの中では呼んでいます。ZIOってDependency Injectionを関数型と組み合わせることができて、そのDependency Injectionを表す型になります。ちょっとこれは後で説明します。

それで、我々が誇る素晴らしいプロダクトの「Cirqua」というものがあります。

Cirquaはインフィード広告と呼ばれるプロダクトです。

せっかくなのでプロダクトの動きを見ていきます。スマホとかでWebページを表示すると、広告が表示される領域があるんですね。ここで読み込まれた広告配信サーバに要求を送って、広告が返ってきます。

広告が表示されたあとにクリックされたら、その後トラッキングサーバというところにそのクリックされたというのが送られて、広告主ページでさらに商品を買うなどのユーザーの行動が継続されたら、それもトラッキングサーバに送られます。

トラッキングサーバというのは、そういうユーザーの行動を逐一計測しております。当然計測したらデータ処理の処理フローに流すわけですね。

最終的には、AWSのAthenaを使ってクエリを走らせて集計しています。このAthenaのクエリはバッチ処理で走らせておりまして、このバッチ処理のコードはCirquaという我々のプロダクトの中では独立しています。独立をしているので、ZIOという新しいライブラリを試すのにも良いだろうということで、我々はまずはここから導入をしてみました。

それで、さっき言ったZIOの戻り値型のEnvironment Type、ここはDIを使います。ここをうまく使いこなすことがキーになってくる感じなんですね。ここを説明していきます。

cake patternを使ってDIをやっていく

DIを使っていくので、まずはDIすべきコンポーネントを特定していきましょう。

今回はいくつか例を挙げていますけど、ConfigであったりAWSを使うのでS3であったり、あるいはデータベースでAthenaとか、そういうDIとして使うコンポーネントを特定します。

ZIOのブログとしても例として出てくるんですけど、cake patternを使ってDIをやっていきます。

cake patternを少し紹介します。サービスと呼ばれるトレイトを作って、それを囲むコンポーネントと呼ばれるようなトレイトを付けます。

そのプロダクションの実装とテストの実装を作っていきます。

ちょっとZIOのはいろいろあるんですけど、だいたいこんな感じでやるのがcake patternです。S3の場合だったらS3のコンポーネントとか、AthenaとかConfigとかいろいろなコンポーネントを積み重ねて、右のコードにあるextendsでwithとやって、cakeが層のように重なっていくのでcake patternと呼ばれています。

僕的にはどちらかと言うとおまんじゅうに見えるので、おまんじゅうっぽく「アッタカイヨー」という感じですね。

あんまりウケない。

(会場笑)

これはバッチ処理の中のコードです。

ちょっと省略してあまり詳細を載せていないですけど、loggerの処理を呼び出してデータベースコンポーネントの処理を呼び出す。1つ1つのコンポーネントがさっき言ったDIで使うコンポーネントですね。それで、Athenaのコンポーネントを呼び出している。

この戻り値型、普通は書かないところは書かなくてもいいんですけど、あえて書くとさっき言ったようなコンポーネントで重なったcakeがEnvironment Typeになっているわけですね。

Throwableだったりのエラーの型があって、最後のBatchResultというのが成功したときに返ってくる型です。この1つ1つの構文の中の処理がまたfor文で入れ子構造になっていて、こういうかたちで書いていくことができます。

ちなみに「cake patternはアンチパターンじゃないのか」と言う人がScala界にはよくいるんですけど、ZIOの作者によると、右下の小さい構文に入ってくるとwithでくっついているcakeの層が薄くなるので、小さいfor文の中を見ればそんなに依存性が大きくなくてリファクタリングもしやすい。だから「そんなにアンチパターンじゃないだろ」というのが作者の見解です。

ZIOでtestabilityが向上するのかは疑問

ここまでの話の中でEffect(副作用)が発生するところはDIを使うと。DIを使ってテストをするのでtestabilityが向上するという話をしたんですけど、ちょっとここで私はよくわからなくなりました。というのは、関数型プログラミングでtestabilityが向上するという話で2番目に出てきたDaniel Spiewakさん。

これは動画の中から抜き出したんですけど、この人、testabilityの向上の話をしているのはAlgebraic Laws、代数的規則と言ったらいいんですか? その話をしていて、別にDIの話とかはしていないんです。

プログラムが満たすべきルールを型クラスが満たすべきルールとして定義しているみたいな話があったんですけど。

これはすごいツッコミ待ちのスライドなので、ぜひ強い人からツッコんでほしいんですけど。

そういう型クラスが満たすべきルールを、うまく数学的に定義して、いい感じにテストをしようみたいな話をしていたはずなので、これはTagless FinalとかZIOがその副作用に対する数学的規則を定義できないという意味では、ZIOはTagless Finalと変わらないので、これでZIOによってTagless Finalよりtestabilityが向上するというのがよくわかりませんでした。

この話は実はZIOの作者の人もしていて「"Tagless-final type classes do not,in general, have algebraic laws."」。代数的規則はTagless Finalでは成り立たないという話をしています。

ここまではわかるんですけど、私がこの先でわからなかったのは「Tagless FinalでZIOに変えて使ってもこれは一緒だから、なぜtestabilityが向上するのかわからなかった」という話なんですね。

今までの話で出てきたことをまとめます。

ZIOはおもしろいライブラリだと思うんですけど、testabilityという1点に関してはちょっとTagless finalとFree Monadに比べて良いところがあるというのを理解できませんでした。なのでツッコミというか教えてくれる人がいたらうれしいなという感じなんですけど。

なので、ZIOでどうやってテストをうまく書くかというと、DIの使い方とかモックの使い方となると、それは関数型のメリットなんだろうか。関数型のメリットではなくて、それぞれのDIの使い方とかテクニックとかそういう話になってしまう気がしました。

テストという点から離れると、ZIOには他にもメリットがたくさんあるライブラリなので、非関数型プログラムよりも馴染みやすくて、それ以外のメリットあるという話ならZIOの導入の動機にもなるということで、ここでZIOの他のメリットを……まとめる時間がなかったので、すみません。ここでトークを終わります。

(会場笑)

ありがとうございます。

(会場拍手)