はじめに
こんにちは。マイクロアドでソフトウェアエンジニアをしている飛田と申します。私は主に UNIVERSE Ads というプロダクトの開発に携わっています。 UNIVERSE Ads では、より関数型ライクな設計や実装を取り入れることにより、高い保守性と効率性を目指しています。
過去の記事では、圏やモノイドについて書きましたが、今回の記事では「関手」について共有させていただきたいと思います。
過去の記事:
- 「関数型言語をもっと使いこなしたい!」マイクロアドの新卒エンジニアがデータサイエンティストの先輩に圏論の初歩を指導してもらった話 - MicroAd Developers Blog
- 新卒2年目のエンジニアがモノイドの数学的な定義について調べてScalaで実装してみた - MicroAd Developers Blog
以降、関手について説明しますが、数学用語としてそれについて言及する場合は「関手」という言葉を使用し、関数型プログラミング用語としてそれについて言及する場合は「ファンクタ」という言葉を使用したいと思います。
関手とは
関手とは「圏と圏を、構造を保ちつつ、対応付けるもの」です。 以下は関手の定義です。
定義:圏から圏
への関手とは、次の条件を満たす対応
です。
(1) の各対象
を
の各対象
に対応させる
(2) の射
を
の射
に対応させる
(3) の各対象
の恒等射
に対して
(4) 任意の射および
に対して
(1)と(2)についてですが、これは単に圏間で射と対象を対応付けているだけです。
(3)に関しては、圏の対象の恒等射を関手
で写したものは、圏
の対応する対象の恒等射と等しくなくてはならないということを表しています。
(4)に関しては、と
を合成してから
で写したものと、
と
を
で写してから合成したものは、等しくなくてはならないということを表しています。
つまり、(1)〜(3)の条件では圏と圏の間で射と対象を対応付けていて、(3)は射の合成を対応付けています。
関手の例: 大小関係を保存する関手
関手の例として、「圏と圏の間で大小関係の構造を保つ関手」について説明したいと思います。
前順序集合とは
関手の具体例について言及する前に、まず、前順序集合について説明したいと思います。
前順序関係とは、集合上の二項関係「
」のことで、次の条件を満たすものです。
*1
- 反射律:
の任意の要素
に対し、
が成り立つ
- 推移律:
の任意の要素
に対し、
かつ
なら
が成り立つ
そして、前順序関係が定まった集合を前順序集合といいます。
以降、この記事では前順序関係の例として大小関係を取り扱います。
例えば、自然数は任意の数に対して、反射律と推移律が成り立つため、前順序集合です。
大小関係の構造を保つ関手
前順序集合を圏とみなしたときに、大小関係を保つ関手
は下図のようになります。
図中の圏ですが、これらはそれぞれ「前順序集合を圏とみなしたもの」を表しています。
圏
の各射は大小関係を表していて、例えば、図中の
は
を表しています。
以下の表に、図中の各射が表す大小関係を示します。
図中の射 | 射が表す大小関係 |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
そして、この関手の例では、は元の圏の数を 2 倍にするものとしています。
また、関手
は、
という条件も満たしていて、例えば、
圏
で
、圏
で
が成り立ちます。
恒等射に関しては、例えば圏で
、圏
で
が成り立ち、
という条件を満たすことが確認できます。
圏では
という大小関係がありますが、関手
によって、圏
でもこの大小関係が引き継がれ
となっており、大小関係の構造が保たれていることが確認できます。
Scala で関手を実装する
まず、Scalaの型と関数の集まりをゆるく圏であると考えるため、以下のような圏を定義してみます。
圏:
- 対象はScalaの型で、例えば、
String
やInt
等、良い振る舞いをする(well-behaved)もの - 射は引数を1つとるScalaの関数で、例えば、
String#length: String -> Int
等、同じく良い振る舞いをするもの
ここで「良い振る舞いをする」とは、関数であれば例えば、副作用のない(純粋)関数が含まれると考えられます。 また逆に例外を発生させる関数など、(結合律や単位律などの)圏として満たすべき公理を壊しそうなものは、実装上可能であっても取り除いて考えることにします。
そして、圏の自己関手(圏
から
自身への関手)として Functor というトレイトをScalaで定義すると、以下のようになります。
こちらのトレイトを実装する際は、冒頭で述べた関手の定義(1)〜(4)を満たすようにする必要があります。
trait Functor[F[_]] { def map[A, B](f: A => B): F[A] => F[B] }
f: A => B
は元の圏の射を表していて、F[A] => F[B]
は関手によって写された後の射を表しています。
圏と圏の間で対象の対応を考える場合は、対象の恒等射をmap
の引数に渡してやれば、対象を対応付けることができます。
List ファンクタ
それでは上述した圏の自己関手の例として List ファンクタについて説明したいと思います。
以下は List ファンクタの実装例です。Scalaの
List
自体がファンクタなので、map
の実装は単にList#map
を呼び出すだけになっています。
val listFunctor = new Functor[List] { override def map[A, B](f: A => B): List[A] => List[B] = _.map(f) }
以下は List ファンクタのサンプルプログラムおよび概念図です。
// 元の圏の射 val length: String => Int = _.length val toDouble: Int => Double = _.toDouble // 元の圏の対象 val strValue: String = "abc" val intValue: Int = length(strValue) val doubleValue: Double = toDouble(intValue) // ファンクタによって写された射 val mappedLength: List[String] => List[Int] = listFunctor.map(length) val mappedToDouble: List[Int] => List[Double] = listFunctor.map(toDouble) val mappedToDoubleLength: List[String] => List[Double] = listFunctor.map(toDouble compose length) // ファンクタによって写された対象 val mappedStrValue: List[String] = List(strValue) val mappedIntValue: List[Int] = List(intValue) val mappedDoubleValue: List[Double] = List(doubleValue)
サンプルプログラムでは、 length
とtoDouble
という射を定義していて、listFunctor
で射をList[A] => List[B]
に変換するようにしています。
対象と射の対応関係ですが、各射 length
, toString
, toDouble
は、map(length)
, map(toStr)
, map(toDouble)
に、各対象String
, Int
, Double
はList[String]
, List[Int]
, List[Double]
に対応付いています。
また、恒等射の対応関係に関しては、以下の条件が成り立ちます。
// 恒等射の対応関係のチェック List(strValue).map(identity) == mappedStrValue // true List(intValue).map(identity) == mappedIntValue // true List(doubleValue).map(identity) == mappedDoubleValue // true
射の合成順序に関しては、以下の条件が成り立ちます。
// 射の合成順序のチェック val value = "関数が等しいかどうかテストするための値" // F(g ∘ f) val Fgf = List((toDouble compose length)(value)) // F(g) ∘ F(f) val FgFf = (listFunctor.map(toDouble) compose listFunctor.map(length))(List(value)) // F(g ∘ f) == F(g) ∘ F(f) Fgf == FgFf // true
雑感
今回、関手(ファンクタ)について調べてみた個人的な雑感ですが、実際のプロダクトの設計にファンクタを取り入れるためには、さらに圏論について理解を深める必要があるかと思いました。
関数型ライブラリなどを使用する際に、ラリブラリで事前に用意されているファンクタのmap
を利用して、プログラムを書いたりすることは簡単そうですが、
一方で、実際のプロダクトにファンクタを設計として組み込むというのは、難しそうです。理想を言えば「仕様書を読んで、適切なデータ構造でモデリングして、それをファンクタとして実装する」レベルまでファンクタを使いこなせるようになりたいですね。
今後はより、ファンクタに関する深い理解をするために、関数型っぽい開発をしているOSSのコード等を読んで、実際のアプリケーションでどのようにファンクタが応用されているのかを調査していきたいと思います。
終わりに
ご覧いただきありがとうございます! 将来的には、ファンクタのような概念をUNIVERSE Adsに組み込んで、より品質の高いプロダクトを作っていきたいと思います。
マイクロアドでは 「広告配信システムをつくりたい!」 「関数型プログラミング言語をガンガン使っていきたい!」というエンジニアを積極的に募集しています。 ご興味を持たれた方はこちらから是非ご応募下さい!
*1:「」という記号を使いましたが、前順序関係は反射律と推移律を満たす集合上の二項関係で、大小関係に限った話ではありません。