はじめに
こんにちは。マイクロアドでソフトウェアエンジニアをしている田口と申します。
私が所属しているチームでは COMPASS というプロダクトに関する開発をメインで行っており、広告配信システム(Scala)、管理画面(Kotlin・Java)、バッチ系(Digdag(Python)・Spark Streaming(Scala)) ...etc など幅広い分野を担当しています。
担当しているプロダクトの中で広告配信システム(Scala)は、2014年に初期実装され現在(2021年)まで追加改修を継続的に行っているプロダクトになります。2019年の後半に「そろそろ刷新しよう」という声が上がり粛々と基盤固めを進めていましたが、他のプロダクトが落ち着いた今、ようやくチーム一丸となって広告配信システムのリプレイスに取り掛かり始めています。
今回のリプレイスでは Scala の関数型ライブラリである Cats、Cats Effect を使用することになり、はじめの一歩としてチーム全員で Scala With Cats を輪読しました。
本記事ではその振り返りとして、自分自身やチームメンバーの理解度が低かった項目や注意が必要だと思った部分を取り上げようと思います。なお関数型自体の話はほとんど出てきません。
Scala With Cats とは
こちらのテキストはどういう目的のために書かれているものかというと、冒頭で以下のように説明されています。
- モナド、関数、その他の関数型プログラミングパターンを紹介する
- これらの概念が Cats でどのように実装されるかを説明する
このテキストはあくまで関数型学習の"はじめの一歩"なのですが、抽象的なものを学ぶのは往々にして難しいものですよね。
読む前に知っておきたかった3つのこと
さて私自身やメンバーの反応から、理解しづらく感じた概念や注意点がこちらです。
- 変位
- トランポリン
- LazyList / Stream が混在していること
主に、関数型固有のものではないもともと難しい概念が出てきたときに「あれ、これってそもそもなんだっけ?全然テキストが頭に入ってこない・・・」と引っかかっていた様に感じました。これらをあらかじめ理解しておくと、主題である関数型の概念理解により集中できるのではないかと思います。
簡単にまとめると、以下の様になります。
- 変位周りに苦手意識がある場合、Producer・Consumer という言葉と結び付けて考えてみたら理解しやすいかも
- トランポリンという言葉にピンとこない場合、Scala でのトランポリン化について先に勉強しておこう
- LazyList?? Stream?? なんだそれはという場合、Stream ≒ LazyList だと知っておこう
それぞれ詳しく説明していきます。
変位周りに苦手意識がある場合
変位の話って小難しいですよね。
- 「共変は
B
がA
のサブタイプのとき、F[B]
がF[A]
のサブタイプであるということ」 - 「反変は
A
がB
のサブタイプのとき、F[B]
がF[A]
のサブタイプであるということ」
というよう説明を色んなところで目にするんですが「共変は感覚的に理解できるけど、反変がなんだかよくわからないな」と感じていました。私と同じ様に感じる人は、ちょっと視点を変えて PECS 原則に出てくる Producer・Consumer という言葉と結びつけて考えると理解がしやすいのかなと思います。
PECS 原則について
Java のジェネリックプログラミングにおいて、メソッドに柔軟性を持たせるための原則として有名なこちらの言葉。Producer extends, Consumer super
の頭文字をとってこの様に言いますよね。
Producer というのは指定した型の値が戻り値として使われているメソッドを表す言葉です。戻り値として、指定した型を"生産する"ので Producer ということだと思っています。Producer には? extends T
と指定する(共変として定義する)と柔軟性が増すというのが、Producer extends の部分になります。
次に Consumer ですが、こちらは指定した型の値を引数で受け取るメソッドのことを表す言葉です。指定した型の値を"消費する"ので Consumer ということだと思っています。Consumer には? super T
と指定する(反変として定義する)と柔軟性が増すというのが、Consumer super の部分ですね。
Scala における Producer・Consumer とは
Java はメソッド単位で境界ワイルドカード型を使用することで柔軟なコレクションを定義し、Scala はクラス単位で変位を指定することができるという違いがありますが、Scala の場合はクラスに対する性質として、Producer・Consumer というものがあると考えて良いのではないかと思っています。
Scala においての変位の文脈で、Producer・Consumer という言葉を使っている文献はインターネット上でもあまり見つけられないのですが、Scala 3 の公式ドキュメント にてクラス名の例として Producer・Consumer という言葉が使われていました。
// an example of a covariant type trait Producer[+T]: def make: T // an example of a contravariant type trait Consumer[-T]: def take(t: T): Unit
さて Scala における Producer として分かりやすい例は、List や Seq、Option などがあります。
これらは共変として定義されており、例えば以下のようにB
がA
のサブタイプのとき、F[B]
はF[A]
として振る舞うことができます。
sealed trait Animal case class Cat(name: String) extends Animal val cats: Seq[Cat] = Seq(Cat("ノラ"), Cat("ミケ")) // Seq[Cat] は Seq[Animal] として振る舞うことができる。 val animals: Seq[Animal] = cats
「Cat
がAnimal
のサブタイプのとき、F[Cat]
がF[Animal]
のサブタイプであるということ」というのは直感的に理解しやすいと思います。
Consumer はどんなものがあるかというと Scala に関するドキュメントの中で反変の説明で使われているクラスは例えば、以下の様なものがあります。
- Scala 公式のドキュメント に登場する Printer
- Scala With Cats に登場する JsonWriter
- Cats のドキュメント に登場する Show
abstract class Printer[-A] { def print(value: A): Unit }
trait JsonWriter[-A] { def write(value: A): Json }
trait Show[T] extends ContravariantShow[T] trait ContravariantShow[-T] extends Serializable { def show(t: T): String }
Show だけ Cats に実装されているものを持ってきたのでちょっと複雑ですが、いずれも指定した型の値を"消費する"メソッドをもつ、Consumer と呼ぶことの出来る性質を持つクラスが例として挙げられていることがわかると思います。「こういった Consumer なクラスを反変にすると柔軟な定義になるんだ」というのがわかっていると、反変に対する理解がしやすくなるのではないかと個人的には考えています。
Printer を使った反変のコード例を示します。
sealed trait Animal case class Cat(name: String) extends Animal { override val toString: String = s"Cat the $name" } val animalPrinter: Printer[Animal] = animal => println(animal.toString) // Printer[Animal] は Printer[Cat] として振る舞うことができる。 val catPrinter: Printer[Cat] = animalPrinter catPrinter.print(Cat("ミケ"))
「Cat
がAnimal
のサブタイプのとき、F[Animal]
がF[Cat]
のサブタイプであるということ」というのはなかなか理解しづらいですよね。
「Cat を受け取って消費する catPrinter の正体が実は Animal を受け取って消費する animalPrinter だった。」として、受け取り口としては Cat を要求します。Cat は Animal として扱えるので Animal として消費されることに何も問題はない、というのが反変の説明になります。
Contravariant の contramap
もうひとつ、反変に関連するもので理解がしづらく感じたものが contramap という関数の存在です。contramap はF[B]
にA => B
を渡すとF[A]
が手に入る関数で、ちょうど map 関数に対して引数の型が逆になっています。
List や Option など Producer な概念に対して定義しようと考えるとわけが分からなくて混乱すると思うのですが、「これは Consumer に対する処理なんだ」とわかっている状態だと、スッと理解が出来るんじゃないかなと思っています。
- Consumer な
F[B]
は、B を消費して何かをするインスタンスである A => B
という関数があれば A を B に変換してから B を消費して何かをするインスタンス、つまりF[A]
を作る事ができる- それが contramap という存在。つまり
F[B]
にA => B
を渡すとF[A]
が手に入る
テキストにも書いていますが map が処理を後ろにくっつけるイメージだとしたら、contramap は処理を前にくっつけるイメージです。わかりやすいですね。
トランポリンという言葉にピンとこない場合
4.6.3 Eval as a Monad
にて「defer はトランポリン化されてるからスタックオーバーフローは起こさない。」という説明がされるのですが、トランポリン自体の説明はされません。
さらっと出てくる言葉なので、このテキストを理解する上では特に必要ないのかもしれないのですが「トランポリンってなに?もうわかんない!」と、この周辺の理解を諦めてしまいそうな方の場合、この概念をさらっとでも知っておくと良いかと思います。
さてこのトランポリンですが、概念自体は Scala に限ったものではなくスタックオーバーフローを起こさずに深く再帰を行うための手法です。このテキストは Scala のためのものなので、それを読む前にこの概念を知らない方が勉強するのに最適かなと思うのはScala関数型デザイン&プログラミングScalazコントリビューターによる関数型徹底ガイド(以下 FP in Scala)の第13章外部作用とI/O
です。Scala を使って、トランポリン化するためのデータ構造を徐々に構築していく過程が見られて理解に役立つかなと思います。
Cats の実装を読むときも、トランポリンの Scala における実装を知っていると読みやすくなりそうですね。
LazyList?? Stream?? なんだそれはという場合
LazyList または Stream ですが、共に Scala に標準定義されている遅延リストのためのデータ構造になります。個人的には「フィボナッチ数列作るときに使うやつ」でおなじみのデータ構造というイメージがあります。
def fibFrom(a: Int, b: Int): LazyList[Int] = a #:: fibFrom(b, a + b)
Scala 2.13 以降では、Stream は deprecated になり、改善版である LazyList の使用を促す様になりました。Scala With Cats は 2.13 以前に書かれたものを随時アップデートしているのでしょう。おそらく直し漏れがあるのだと思うのですが 現在(2021/08)のテキストを読むと LazyList を使った説明の途中で唐突に Stream という単語が出てきたりします。そのため Stream ≒ LazyList という関係を知っていないと読んでいて混乱するはずです。
おわりに
尻つぼみ感がものすごいですが「Scala With Cats を読む前に知っておきたかったこと」をいくつか上げてみました。
- 変位の説明が難しく感じたら、クラスには Producer・Consumer という性質を持つことがあると思い出して
- Scala でのトランポリン化が気になったら FP in Scala を読んでみて
- Stream ≒ LazyList というのを知っておいて
また内容の難しさとは直接の関係はないので触れていなかったのですが、Scala With Cats は英語で書かれているテキストになります。私みたいに英語が苦手な方が、関数型概念の学習のはじめの一歩としてこのテキストを選択した場合、専門用語が適切に訳せず内容を理解するハードルが余計に高くなるんじゃないかなと感じました。
具体例をあげると Monoid の文脈で出てくるthe identity element
という単語を「恒等式の要素」などと直訳して「これってどういう意味だろう」と悩んだりすることかと思います。元も子もない話になってしまうのですが英語が苦手な方は、FP in Scala を読んでからこのテキストを読んだほうが、関数型の概念を理解をするのが色々楽になって良いのではないかなと思います。
Scala With Cats のその先は
私たちチームは今回 Scala With Cats の輪読会で関数型の基本的な概念を学びました。しかし、Cats を使った WEB アプリの実装や改修を行えるようになるには基本的な概念だけでなく
- Cats、Cats Effect で実装されている型クラスについて
- http4s や doobie などの Cats 製のライブラリと組み合わせてアプリケーションを構築する方法
などを学ぶ必要があると思っています。そのために、以下のドキュメントや本を読むのが良いのではないかと今のところ考えています。
チームにおいて、関数型を使った設計やプログラミング手法を学ぶ旅はまだまだ始まったばかりです。今回の主題ではないので書きませんでしたが、DDD も取り入れて開発を進めていますので、そちらの理解を深めていく必要性も感じています。
マイクロアドでは Cats、DDD を使用した広告配信システムの開発にチャレンジしたいという仲間を募集しています。また、サーバサイドエンジニアだけでなく機械学習エンジニア、フロントエンジニア、インフラエンジニアなど幅広く募集しています。気になった方は以下からご応募いただければと思います。