関数型プログラミングのファンクター
1. 概要
関数型プログラミングは、Scalaの世界への旅を始めているJava開発者にとって新しい概念です。 言語構文自体を学ぶだけでは十分ではありません。 全体像を理解するには、開発者は、新しいタイプの抽象化、イディオム、およびそれらをいつどのように使用するかを学ぶ必要があります。
このチュートリアルでは、ファンクターの抽象化とその有用性について説明します。 これは、ファンクター自体であるとは知らなかったとしても、コアScalaライブラリの周りに見られる一般的なパターンです。
2. コンテナタイプ
ファンクターは、カテゴリー間のマッピングです。 名前と概念は、数学の他の分野の一般的なパターンを見つけて抽象化することを目的とした数学的理論に由来しています。
マッピングは、データの変換または関数を参照できます。 ご存知のように、プログラミングの関数は、入力値を出力値に変換する一連の命令です。 「ファンクター」という名前は「関数」という名前に似ており、変換の意味はほとんど同じです。
適切な関数は、あるタイプの値を別のタイプに変換する方法です。 数学では、 f:A =>Bと書かれています。 コンテナ内の値に適切な関数を適用することはできませんでした。
コンテナーまたはコンテキストは、すべてのScala開発者が遭遇するものです。 List [A] は、タイプAのゼロまたは任意の数の値を保持できるコンテナーです。 これにより、機能を抽象化して、コンテナが保持している値のタイプとは関係なく、このようなシーケンスを操作できます。
Option [A] 、 Future [A] 、 Either [A] 、 Map [A、B]などの他のコンテナーについても同じことが言えます。 ]も同様です。 それらはすべて、あらゆるタイプのデータを操作するためのいくつかの汎用ロジックをカプセル化しています。
コンテナ内のデータを機能しない方法で操作するには、値または値のセットを解凍し、関数を適用して、結果をコンテナにパックする必要があります。 Optionコンテナ内の値を操作したいとします。
var patientIdOpt = Some("patient 0")
if (patientIdOpt.isDefined) {
val patientId = patientIdOpt.get
patientIdOpt = Some(s"$patientId is cured")
}
assert(patientIdOpt.contains("patient 0 is cured"))
関数を適用するという単純なタスクは、命令型アプローチでさらに多くのステップを実行します。
適切なタイプA=> B の値を操作する関数と、コンテナー C [A] がある場合、内部の値に適切な関数を適用する方法があります。このコンテナは、コンテナから内部タイプを取り出して、結果をパックする必要がありません。
3. コアライブラリでのファンクターの動作
ファンクターが答えです。 ファンクターは言語抽象化の一部ではないという事実にもかかわらず、すべてのコンテナーにはファンクターのプロパティがあります。 ファンクター
コアライブラリのコンテナには、コンテキストの境界内で、コンテナ内の値に関数を適用する一般的な方法があります。 Option コンテナーに値がない場合、Noneに関数を適用した結果はNone自体になります。 List に複数の値が含まれている場合、関数はそのすべての値に適用されます。
val temperatures = List(38, 39, 39, 37)
val panadol: Int => Int = temperature => temperature - 10
val lowTemperatures = temperatures.map(panadol)
assert(lowTemperatures == List(28, 29, 29, 27))
「魔法」は関数map()の中にあります。 その定義はすべてのコンテナで同じです。
trait F[A] {
def map[A,B](f: A => B): F[B]
}
F [A] は、 map()関数が定義されているコンテナーです。 f:A => B は、コンテナー内の値に適用する適切な関数であり、 F [B] は、関数applicationの結果の値を持つ結果のコンテナーです。
4. 高等な関手
Scalaの豊富な型システムにより、ファンクターをより一般的に定義し、コンテナー型自体を抽象化することができます。 ScalazやCatsなどの外部ライブラリは、高種類 Functor [F[_]]の独自の実装を提供します。
たとえば、Catsライブラリは、 trait Functor [F[_]]自体と共通コンテナの実装を定義します。 オプションにファンクターを使用してみましょう。
まず、build.sbtファイル内に依存関係を追加する必要があります。
scalacOptions += "-Ypartial-unification"
libraryDependencies += "org.typelevel" %% "cats-core" % "2.4.2"
次に、Catsクラスをインポートしましょう。
import cats.instances.option._
import cats.Functor
これで、マップするテストデータを定義できます。
val option = Some("Hello!")
そして、ファンクターのマップ機能を使用します。
val result = Functor[Option].map(option)(_.toUpperCase)
assert(result == Some("HELLO!"))
ライブラリの実装は、圏論がファンクター自体にもたらす一連の法則を尊重します。
5. ファンクターの法則
では、同様の map()関数を持つコンテナーはファンクターですか? 答えは「ほぼ」です。 適切なファンクターになるには、コンテナーのmap()関数は2つの法則に従う必要があります:
- 構成—関数 f()に map()を適用してから、他の関数 g()に map()を適用します。は、 map()を1回適用するのと同様ですが、関数の構成 f()および g()に結果を返す必要があります。 fa.map(f).map(g)= fa.map(f.andThen(g))
- Identity — identity()関数に map()を適用すると、変更なしで同じコンテナが返されます: fa.map(x => x)= fa
6. 結論
この記事では、Scalaの抽象化の手段と、このチェーンにおけるファンクターの役割を示しました。 ApplicativeFunctorやMonad、などの他の抽象化に加えて、アプリケーションの開発に機能的アプローチを適用するための豊富な機器セットがあります。
いつものように、トピックのすべての例はGitHubにあります。