こんにちは。マイクロアドでソフトウェアエンジニアをしている飛田と申します。私は主に UNIVERSE Ads というプロダクトの開発に携わっています。 UNIVERSE Ads では、より関数型ライクな設計や実装を取り入れることにより、高い保守性と効率性を目指しています。
マイクロアドでは、プロダクトの一部にdoobieというDB操作ライブラリを使っていまして、 詳細は割愛しますが、その中でTransactorという自然変換を行うものが登場します。
今回はライブラリの背景にある概念を知るために、「自然変換」の概要について学びましたので、ここで共有させていただきます。
過去の記事:
- 「関数型言語をもっと使いこなしたい!」マイクロアドの新卒エンジニアがデータサイエンティストの先輩に圏論の初歩を指導してもらった話 - MicroAd Developers Blog
- 圏論初心者が関手に入門し、Scalaで実装した話 - MicroAd Developers Blog
自然変換とは
以前の記事で、関手とは「圏と圏を、構造を保ちつつ、対応付けるもの」と書きましたが、 自然変換とは「関手から関手への射で、関手の構造を保つもの」です。
定義:圏から圏への関手が存在するとき、以下の条件を満たすを自然変換といいます。
- tはの各対象に対して射を対応させます。
- の任意の射に対してが成り立ちます。
つまり、自然変換とはの関手からへの射のあつまりで、以下の図式を可換にするものです。
Scalaの自然変換の例
List
からOption
への自然変換 headOption
について取り上げたいと思います。
例えばString
からInt
への射length
が存在したとき、
headOption
が自然変換であるためには下記の図式が可換になる必要があります。
以下のコードは、 List
と Option
をファンクタとして実装し、headOption
が自然変換かどうかを軽くチェックするものです。
trait Functor[F[_]] { def map[A, B](f: A => B): F[A] => F[B] } val listFunctor = new Functor[List] { override def map[A, B](f: A => B): List[A] => List[B] = _.map(f) } val optionFunctor = new Functor[Option] { override def map[A, B](f: A => B): Option[A] => Option[B] = _.map(f) } def headOption[A](list: List[A]): Option[A] = list.headOption def length(str: String): Int = str.length val lengthAfterHeadOption: List[String] => Option[Int] = optionFunctor.map(length) compose headOption[String] val headOptionAfterLength = headOption[Int] _ compose listFunctor.map(length) val testData = List("Foo", "Bar", "FooBar") assert(lengthAfterHeadOption(testData) == headOptionAfterLength(testData))
lengthAfterHeadOption
と headOptionAfterLength
という2種類の合成射があり、射が辿る経路はそれぞれ異なりますが、
関数が返す最終的な値は同じなり、コード末尾にあるassert
文の結果は成功します。
考察など
初めて自然変換について学んだ感想ですが、何が「自然」なのかを理解するのが難しく思えました。
Scalaのサンプルコードを見ますと、確かにheadOption
とlength
を合成した関数は、合成の順序にかかわらず同じ値を返しますが、
なぜそうなるかを考えると結構奥が深そうです。
List
ファンクタとOption
ファンクタはそれぞれ構造を保ち、List#map
はListの長さを保ちながら各要素を変換し、Option#map
はSome
もしくはNone
のデータ構造を保ちながら中身の要素を変換します。それぞれが保つ構造を上手く加味して、headOption
とlength
を合成した関数が合成の順序にかかわらず同じ値を返すようになり、その結果、headOption
とlength
の関係が整合的になることが「自然」なのかなと思いました。
おわりに
ご覧いただきありがとうございました! 今回の記事は自然変換の簡単な説明になりましたが、 次回以降の記事ではScalaの実装で実践的に自然変換を使う例などが書ければと思います!