Scalazの背後にある原則
1. 序章
このチュートリアルでは、Scalaエコシステムの一般的なライブラリの1つであるScalazの背後にある基本原則について説明します。 純粋関数と不変のデータ構造を備えたScalaの世界に最高の関数型プログラミングをもたらします。
また、既存のScala機能を拡張して、表現力と簡潔さを向上させます。 最後に、Scalaでほとんどすべてを純粋に機能的な方法で実行するための一連のユーティリティメソッドを追加します。
Scalaを初めて使用する場合は、穏やかなScalaの紹介から始めてください。 この記事を最大限に活用するには、アドホックなポリモーフィズムも理解する必要があります。
2. 暗黙的
Scalazは、アドホック多相性に大きく依存しています。 しかし、Scalaがアドホック多相性をどのようにサポートするかを理解するには、暗黙の概念に精通していることが重要です。 これは、Scalaの世界に移動する人にとって、より混乱し、苛立たしい概念の1つであることに注意してください。
2.1. 暗黙のパラメータ
非常に簡単に言えば、各Scalaメソッドが a 、 def max(a:Int、b:Int)のbなどの関数パラメーターを持っているようにパラメータ多相で見たタイプパラメータでは、パラメータリストの先頭にimplicitキーワードでマークされた暗黙的なパラメータを持つこともできます。
Scalaコレクションで使用されるsortedメソッドがどのように宣言されているかを見てみましょう。
def sorted[B >: A](implicit ord: Ordering[B]): Repr
ほぼすべてのタイプで再利用できるように、汎用タイプBを使用していることに注意してください。 ただし、タイプOrderingのordと呼ばれる別のパラメーターが必要です。これは、そのタイプの並べ替え戦略を定義します。 ソート可能なScalaのネイティブ型はすべて、Orderingを定義します。 したがって、並べ替え可能にしたいカスタムタイプについては、Orderingも定義する必要があります。
キーワードは、以前のように開発者が ord の引数を指定しない場合、メソッドにアクセスできる適切な変数を探す必要があることをコンパイラーに暗黙的に通知します。問題があり、代わりにそれを使用してください。
つまり、コンパイラーは、開発者が依存関係を明示的に提供することを期待するのではなく、依存関係を暗黙的に解決しようとする必要があります。
コンパイラーが使用可能なすべてのスコープを検索し、必要な暗黙的パラメーターのタイプに適合するものが見つからない場合、エラーをスローします。
2.2. 暗黙の引数の例外はありません
Ordering の新しい実装を追加せずに、 Int、 String、およびその他の組み込み型を並べ替えることができます。
it should "sort ints and strings" in {
val integers = List(3, 2, 6, 5, 4, 1)
val strings = List("c", "b", "f", "e", "d", "a")
assertResult(expected = List(1, 2, 3, 4, 5, 6))(integers.sorted)
assertResult(expected = List("a", "b", "c", "d", "e", "f"))(strings.sorted)
}
整数のみをラップする新しいタイプを導入するとします。
case class IntWrapper(id: Int)
次に、新しいタイプの List でsortedを呼び出そうとすると、次のようになります。
it should "sort custom types" in {
val wrappedInts = List(IntWrapper(3), IntWrapper(2), IntWrapper(1))
assertResult(expected = List(IntWrapper(1), IntWrapper(2), IntWrapper(3)))(wrappedInts.sorted)
}
コードはコンパイルされず、コンパイルの失敗の原因となるエラーが表示されます。
No implicit arguments of type: Ordering[IntWrapper]
このエラーは、Scalaがソート方法を知らないため、新しいタイプのソート戦略を定義して提供する必要があることを示しています。
2.3. 暗黙の値
メソッド呼び出しポイントで引数を明示的に提供することにより、暗黙的なパラメーターを満たすことができます。
しかし、それは彼らの本質を否定します。 暗黙の力を使用すると、 implicit キーワードで引数を定義することで引数を暗黙の値にすることができるため、sortedに明示的に渡す必要はありません。
implicit val ord: Ordering[IntWrapper] = (x, y) => x.id.compareTo(y.id)
上記の値がスコープ内に提供されると、以前に見たエラーが突然消えます。 これは、コンパイラがIntWrapperタイプをソートする方法を認識できるようになったことを意味します。 テストを実行すると、合格するはずです。
暗黙の値は同じメソッド内にある必要はありません。暗黙のスコープは、インポートされたクラスだけでなくクラス全体にも拡張されます。
これと同じパターンを使用して、 List.sorted が任意のタイプで動作できるようにすることができます。これは、コンパイラーがタイプに応じて切り替えることができるタイプごとに個別の実装を暗黙的に提供するだけです。リストの要素。
3. 高種類のタイプ
関数型プログラミングでは、コードで宣言されたすべての変数または値は、特定の型階層を使用して構築されます。 これは、Scalaと関数型プログラミングサイクルの高度なトピックです。
したがって、型の基本から始めて、型階層を構築します。
3.1. 適切な値
いくつかの変数宣言を見てみましょう。
val name = "Greg"
val age = 34
上記のスニペットでは、「Greg」は、それ以上操作しなくても完全に意味のある生データであり、番号34についても同じことが言えます。
平易な英語で、誰かが「Greg」と言うのを聞いた場合、それらが何を意味するのかについての未回答の質問はほとんどありません。 最終状態にあるため、これを適切な値と呼びます。
3.2. 適切なタイプ
前のサブセクションから、Scalaコンパイラーは「Greg」から String のタイプ、および34からIntのタイプを推測できます。 ]。
したがって、StringとIntは適切なタイプであると結論付けることができます。 型システムでは、必要なのは含めるデータだけであるため、最終状態にあります。 次のタイプを使用して、より明示的にすることができます。
val name: String = "Greg"
3.3. 抽象型
別の例を見てみましょう:
val fruits = List("Oranges", "Apples", "Mangoes")
このスニペットでは、変数fruitsに割り当てているものも適切な値です。 Scalaコンパイラーは、タイプを List[String]として再度推測できます。 ただし、フルーツを明示的なタイプで宣言できるかどうかを考えてみましょう。
val fruits: List = List("Oranges", "Apples", "Mangoes")
これは機能せず、コンパイルは次のエラーで失敗します:タイプリストはタイプパラメータを取ります。 String とは異なり、Listは型階層の最終状態ではありません。 それは未回答の質問を残します:どのタイプのリスト、または単に、何のリスト? コンパイラはListが特定のタイプの値を持つことを期待しているためです。
したがって、文字列の List と言えば、名前や年齢のListと同じように意味があります。 値のカテゴリが言及されている場合、内部タイプを合理的に推測できます。 それでは、上記の宣言を最終的な状態にしましょう。
val fruits: List[String] = List("Oranges", "Apples", "Mangoes")
したがって、 String は適切な型ですが、 List は、 String 、 Int、よりも型階層の上位にあるため、抽象型です。 ]およびその他の適切なタイプ。 List には、適切なタイプを含める必要があります。 上記の例では、 List[String]が適切なタイプです。
3.4. 型構築子
前のサブセクションに続いて、注意すべき重要な点は、 List[_]が型コンストラクターであるということです。 具体的な型が与えられると、その型のListのインスタンスを構築します。 これは、 StudentId(_)のような値コンストラクターに似ています。 整数の学生IDを指定すると、StudentIdのインスタンスが作成されます。
タイプListは、他のコンテナタイプ Option 、 Either 、および Future と同様に、タイプ階層の同じレベルにあります。 これらのコンテナタイプは、タイプを指定して特定されるまで抽象的です。
このようにして、コンテナタイプは最終状態に到達します。 言い換えると、コンテナタイプを使用すると、過大評価または適切なタイプを抽象化できます。
コンテナタイプが適切なタイプよりもタイプ階層の上位にあるという事実が、次のような抽象化を作成できる理由です。
def sort[A](xs: List[A]): List[A]
これは、前に見たように、パラメトリック多型です。 タイプパラメータAをStringやなどのタイプ引数に置き換えることにより、任意のタイプのListでsortを呼び出すことができます。 ]Int。 したがって、値型は適切な型と呼ばれ、コンテナ型は型構築子と呼ばれます。
3.5. 型構築子の抽象化
型構築子を抽象化することもできたらどうでしょうか? 上記のスニペットに見られるように、 List [A] などの適切な型を抽象化できるので、 F [Int] などの型コンストラクターを抽象化してみませんか?
この場合、 List をパラメーターに変換し、型コンストラクター引数を指定することで、他の型コンストラクターのようなOptionに置き換えることができます。
試してみましょう。 コンテナに任意の整数があると想像してください。
val nums = List(1, 2, 3)
val numOpt = Some(5)
そして、含まれているすべての要素を2倍にします。
nums.map(_ * 2) // List(2,4,6)
numOpt.map(_ * 2) // Some(5)
これは簡単にできます! 仮に、ListまたはOptionのどちらを渡しても、同じ操作を実行できる単一の抽象関数を作成するとします。 以前のsort関数は、任意のタイプの List をソートできますが、整数のリストだけでなく、整数のリストも取得できるdoubleIt関数が必要です。整数の任意のコンテナ、および含まれている要素を2倍にします。
def doubleIt[F[Int]](xs: F[Int]): F[Int]
これはパラメトリックポリモーフィズムに似ていますが、型ではなく型構築子を超えています。 このメソッドの本体を実装して、実装を変更せずに任意の整数コンテナーを渡すことができるようにする方法を考えてください。
3.6. より高い種類
以前と同じように型クラスを使用できます。 doubleIt は、 F がリストであるかどうかに関係なく、遭遇した F[Int]の処理方法を知るための支援が必要であることを忘れないでください]、 Option 、、またはメソッド呼び出し時に提供するその他の型コンストラクター。
このヘルプは、暗黙のパラメーターの形式で提供されます。
def doubleIt[F[Int]](xs: F[Int])(implicit doubler: Doubler[F]): F[Int] = {
doubler.makeDouble(xs)
}
次に、型クラスを定義する必要があります。
trait Doubler[F[Int]] {
def makeDouble(xs: F[Int]): F[Int]
}
object Doubler {
implicit object listDoubler extends Doubler[List] {
def makeDouble(xs: List[Int]): List[Int] = xs.map(_ * 2)
}
implicit object optionDoubler extends Doubler[Option] {
def makeDouble(xs: Option[Int]): Option[Int] = xs.map(_ * 2)
}
}
この型クラスのスコープで、 doubleIt は、ListまたはOptionでラップされた整数を2倍にする方法を認識します。
"Doubler" should "work on any supported container type" in {
val list: List[Int] = List(1,2,3)
val opt: Option[Int] = Some(5)
assertResult(expected = List(2,4,6))(actual = doubleIt(list))
assertResult(expected = Some(10))(actual = doubleIt(opt))
}
これが、高種類のポリモーフィズムという用語の由来です–型構築子の抽象化。 ほとんどの場合、Fの代わりにF[_] のような表記があり、この場合は F[Int]のような適切なタイプです。 もちろん、私たちが持っているのは不自然な例です。
現在、含まれているパラメータは適切なタイプです。 次のテストに合格するように、それを抽象化する場合はどうなりますか。
"Doubler" should "work on any supported container type" in {
...
val strList: List[String] = List("a", "b", "c")
...
assertResult(expected = List("aa", "bb", "cc"))(actual = doubleIt(strList))
}
この抽象化をサポートするには、doubleItの署名を変更する必要があります。 そうしないと、上記のテストは次のエラーでコンパイルに失敗します:パラメーターdoublerの暗黙が見つかりません:Doubler [List、String]。
メソッドシグネチャをアップグレードしましょう:
def doubleIt[F[_], A](xs: F[A])(implicit doubler: Doubler[F,A]): F[A]
同様に、型クラスを更新する必要があります。
trait Doubler[F[_], A] {
def makeDouble(xs: F[A]): F[A]
}
object Doubler {
...
implicit object stringListDoubler extends Doubler[List, String] {
def makeDouble(xs: List[String]): List[String] = xs.map(s => s concat s)
}
...
}
4. 結論
このチュートリアルでは、Scalazのバックボーンを形成する基礎概念のいくつかについて説明しました。 Scalazで使用されているその他の概念については、type-classesおよびpimpmy librarypatternに関する記事をご覧ください。
Scalazの機能に遭遇すると、この記事の1つ以上の概念と参照されている概念に何が起こっているのかをマッピングすることができます。
いつものように、完全なソースコードはGitHubでから入手できます。