Scalaでの型クラスの紹介

  • パート1:This post
  • パート2:Scalaでのタイプセーフなシリアル化
  • パート3:高親切な型を持つ型付きエラー処理

型クラスとは何ですか

あなた自身のコードで型クラスを理解し 名前が呼び起こすものとは対照的に、型クラスはオブジェクト指向言語のクラスではありません。 ウィキペディアによると:

コンピュータサイエンスでは、型クラスはアドホック多型をサポートする型システム構築物です。 これは、パラメトリック多態性型の型変数に制約を追加することによって実現されます。

私たちはいくつかの開梱を持っています:

  • 「アドホック」多型とは何ですか? それが何であれ、それは型クラスが私たちが達成するのを助けるものです。
  • ‘パラメトリック多型’タイプとは何ですか? それが何であれ、それが私たち自身の型クラスを作成する方法です。

型クラスはHaskellで最初に登場し、Haskellで抽象化を構築する基本的な方法です。 それらは非常に重要であるため、Learn You a Haskellの本の最初のページで紹介されています:

型クラスは、いくつかの動作を定義するインターフェイスの一種です。 型が型クラスの一部である場合、それは型クラスが記述する動作をサポートし、実装することを意味します。 わかりましたので、型に対していくつかの動作を定義しようとしているようで、型に応じてその動作を異なる方法で実装しようとしているようです。 その最後の部分は、”型に応じて異なる動作を実装する”ことは、以前の”多態性”ビットです。 そのため、この用語は意味を成し始めています:それは型のクラスです。 型クラスによって指定された動作を実装するクラスは、その型のクラス—’型クラス’のメンバーであると言われます。

この時点で、これは継承のように聞こえると言うのが適切でしょう。 それらは両方とも、抽象化を構築し、多態性を提供することを可能にします。

では、なぜ型クラスを探索するのですか? 多くの場合、それらは継承階層よりもかなり強力で拡張可能です。 彼らははるかに簡単にすることができ、あなたのコードベースがすでにFPをOOよりも傾いているときにはるかに適しています。 強力なOOプロジェクトでも、型クラスはその日を救うことができます。

型クラスの定義

これで十分です—記事の残りの部分では、継承を介してJavaで実際に実装され、型クラスを使用して実装される非常に使い慣れた機

私たちは皆、Java(したがってScala)のすべてのオブジェクトに存在するメソッドに精通しています:.toString。 その目的は、問題のオブジェクトの文字列表現を生成することです。 これを実現するために型クラスをどのように使用するかを見てみましょう。 HaskellのバージョンのtoStringShowと呼ばれているので、その名前に大まかに固執します:

trait Showable {
def show(a: A): String
}

それはそれです:私たちのShowable型クラスがあります。 なぜ型パラメータAを取るのですか? 型クラスの最初の定義を思い出すと、”パラメトリック多態型の型変数に制約を追加することによって達成されました。”Showableはパラメーター的に多型であり、Aは型変数であり、型パラメータとして型(A)を取ることを意味します。 (定義に従って、Aに制約を配置しなかったことに注意してください。 これは、型クラスを一緒に構成するとき、またはそれらを利用するときに行うことです。)

型クラスの実装

今、私たちは表示できるようにしたい任意の型のためのShowableトレイトの実装を提供する必要があります。 これらの実装は、文字通りトレイトShowableのインスタンスになるため、型クラスの”インスタンス”と呼ばれます。 これらは、ほとんどの場合、.showをオーバーライドする匿名クラスであり、それらの定義には定型文が含まれているためです(lambda Javaプログラミング以前の’function objects’を覚)、makeというヘルパーを定義します。 まず、UserName型の実装から始めましょう:

object Showable {
def make(showFn: A => String): Showable = new Showable {
override def show(a: A): String = showFn(a)
}
}final case class UserName(first: String, last: String)implicit val defaultUserNameShowable = Showable.make(userName => s"${userName.first} ${userName.last}")val janeName = UserName("Jane", "Doe")

では、どのようにこれを呼び出すのでしょうか? 次のように、showableインスタンスからメソッドを直接呼び出すことができます:

defaultUserNameShowable.show(janeName) // "Jane Doe"

構文の改善

UserNameShowable実装を呼び出すことができましたが、あまりうまくいきませんでした。 (defaultUserNameShowable)の周りに型クラスインスタンスへの参照があり、その名前を知っている必要がありました。 これは私たちが抽象化したいものです。 物語がここで終わった場合、Scalaの型クラスはそれほど使いやすいものではありません。 幸いなことに、私たちはimplicitsを使用して大幅に人間工学を改善することができます。 呼び出しヘルパーを使用してShowableのコンパニオンオブジェクトを拡張します:

implicit class ShowSyntax(a: A) extends AnyVal {
def show(implicit showable: Showable): String = showable.show(a)
}
...
janeName.show

この”拡張メソッド”をスコープのどこかに配置することで、すべてのオブジェクトが.showメソッドを持っているふりをすることができます。

これは何を達成しましたか

では、代わりにtoStringUserNameオーバーライドしないのはなぜですか? さて、UserNameは私たちが依存している図書館に住んでいて、私たちがそれを消費している時までにすでにコンパイルされているとしましょう。 それはfinalクラスであるため、サブクラス化することはできません。 しかし、型クラスを使用して、私たちは本質的にアドホックな方法でそれに新しい動作を”添付”することは自由です。 これは上記の定義の”アドホック”部分です: 型自体が定義されている場所とは別に、この多態性を追加することができます。

さらに、継承とは異なり、Scalaで型クラスを定義するとき、それらは”コヒーレント”である必要はありません。Showableの複数の実装を定義し、それらの間で選択できます。 いくつかの型クラスでは、これは良いことですが、他のクラスではあまりありません。

Haskellでは、型クラスは一貫しています。 Scala3では、Martin Oderskyによると、型クラスごとに、一貫性があるかどうかを選択することができます。

しかし、今のところ、のためのshowの別の意味を提供してみましょうUserName:

implicit val secretUserNameShowable = Showable.make(_ => "<secret person>")

ここで行う必要があるのは、Showableインスタンスのうちの一つだけが一度にスコープ内にあることを確認することだけです。 そうでなければ、それは大したことではありません、私たちはコンパイル時の’あいまいな暗黙の値’エラーを取得します:

janeName.show
> ambiguous implicit values:
both value secretUserNameShowable in object typeclasses of type => Showable
and value defaultUserNameShowable in object typeclasses of type => Showable
match expected type Showable

重要な注意点の1つは、UserNameShowableを結合していないことです。 私たちがしたことは、UserNameに対してShowableの意味を定義し、それらを互いに完全に独立させたままにすることだけです。 このデカップリングは、保守性に大きな影響を与え、コード全体の依存関係の絡み合いを防ぐのに役立ちます。

コンパイル時の安全性

showの意味を提供していないオブジェクトに対して.showを呼び出そうとするとどうなりますか? 言い換えれば、ターゲットのスコープ内にShowableのインスタンスがない場合は? コンパイラエラーが発生するだけです:

java.util.UUID.randomUUID().show
> could not find implicit value for parameter showable: Showable

型クラスを定義するときに、実際にこのエラーメッセージをカスタマイズできます:

@scala.annotation.implicitNotFound("No Showable in scope for A = ${A}. Try importing or defining one.")
trait Showable {
...
}
...
janeName.show
> No Showable in scope for A = java.util.UUID. Try importing or defining one.

キーテイクアウト

  • 型クラスは、それらの動作を実装するオブジェクトや型とは完全に別々に動作のセットを定義することができます。
  • 型クラスは純粋なScalaで表現され、型パラメータを取る特性と、構文をきれいにするための暗黙的なものがあります。
  • 型クラスを使用すると、ソースを変更できない型やオブジェクトの動作を拡張または実装できます。 (それらは、式の問題を解決するために必要なアドホック多型を提供する)。

次に来るもの

確かに、Showableは些細な型クラスですが、その用途はありません。 しかし、はるかに有用なものを簡単に想像することができます:

trait SafeSerializable {
def serialize(a: A): Array
def deserialize(bytes: Array): A
}
trait Convertible {
def to(a: A): B
def from(b: B): A
}
trait Functor] {
def map(a: F, f: A => B): F
}

SafeSerializable Akka Serializationのようなものに代わる素晴らしい選択肢であり、実行時にシリアライザが私たちの型に登録されているかどうかを確認するまで待ちます。 コンパイル時にシリアル化の実装を暗黙的に渡す方がはるかに安全です!

Convertibleは、型間でデータを変換するための素晴らしい再利用可能な方法です。 Bijectionと呼ばれる同じ目標を念頭に置いたTwitterのライブラリがあります。

Functorは、’categorical programming’と呼ばれる関数型プログラミングの分野におけるいくつかの非常に強力な抽象化のビルディングブロックです。 ScalaZやCatsのようなライブラリは、Functorのような型クラスに基づいて構築されています。 今回議論しなかった重要な概念の1つは、型クラスを簡単に構成して、Functorのような単純な構成ブロックからますます複雑な動作を構築できるというこ

さらに、Scalaエコシステムの多くの必須ライブラリ(Play JSON、Slick、et. al)型クラスを利用し、多くの場合、それらをAPIのメインドライバとして公開します(e.G.JSONのFormat型クラスを再生します。 型クラスを自分で理解することは、たとえ自分で書いたことがないとしても、Scalaエコシステムの中にあるものをより効果的に活用するための素晴ら

将来的には、SafeSerializableのようなすぐに実用的なタイプのクラスを見ていきます。 Functor]Monad]のようなより抽象的なカテゴリ型クラスの紹介については、catsライブラリのドキュメントを参照してください。 型クラスを使用したScalaでのプログラミングのより詳細なガイドについては、以下を参照してくださunderscore.io テーマに関する素晴らしい本を持っています: Scala With Cats

パート2:Scalaでのタイプセーフなシリアル化

GIST with code

コメントを残す

メールアドレスが公開されることはありません。