Scala3 の型クラス: 初心者ガイド |元帳

Scala3 の型クラス: 初心者ガイド |元帳

Scala3 の型クラス: 初心者ガイド | Ledger Platoブロックチェーンデータインテリジェンス。垂直検索。あい。

このドキュメントは、Scala の散文にはすでに精通しているものの、すべての ` について困惑している初心者の Scala3 開発者を対象としています。implicits` とコード内のパラメーター化された特性。

この文書では、その理由、方法、場所、時期について説明します。 型クラス (TC).

このドキュメントを読んだ後、初心者の Scala3 開発者は、使用するための確かな知識を獲得し、ScalaXNUMX のソース コードを詳しく学ぶことができます。 たくさん Scala ライブラリを学習し、慣用的な Scala コードの作成を開始します。

その理由から始めましょう…

表現の問題

1998年には、 フィリップ・ワドラー氏はこう述べた 「表現の問題は古い問題の新しい名前である」ということです。それはソフトウェアの拡張性の問題です。 Wadler 氏の著作によると、式の問題の解決策は次の規則に従わなければなりません。

  • ルール 1: の実装を許可する 既存の行動 (Scala の特性を思い浮かべてください) に適用される 新しい表現 (ケースクラスを考えてください)
  • ルール 2: の実装を許可する 新しい行動 適用される 既存の表現
  • ルール 3: 危険を与えてはなりません タイプセーフ
  • ルール 4: 再コンパイルが必要であってはなりません 既存のコード

この問題を解決することが、この記事の糸口になります。

ルール 1: 新しい表現に対する既存の動作の実装

どのオブジェクト指向言語にも、ルール 1 の解決策が組み込まれています。 サブタイプ多型。任意の ` を安全に実装できますtrait` への依存関係で定義されていますclass依存関係を再コンパイルせずに、独自のコード内で ` を使用します。実際にそれを見てみましょう:

スカラ

def todo = 42
type Height = Int
type Block = Int

object Lib1:
 trait Blockchain:
 def getBlock(height: Height): Block

 case class Ethereum() extends Blockchain:
 override def getBlock(height: Height) = todo

 case class Bitcoin() extends Blockchain:
 override def getBlock(height: Height) = todo


object Lib2:
 import Lib1.*

 case class Polkadot() extends Blockchain:
 override def getBlock(height: Height): Block = todo

val eth = Lib1.Ethereum()
val btc = Lib1.Bitcoin()
val dot = Lib2.Polkadot()

この架空の例では、ライブラリ `Lib1` (5行目) 特性を定義します `Blockchain` (6 行目) とその 2 つの実装 (9 行目と 12 行目)。 `Lib1` はこの文書全体で同じままになります (規則 4 の施行)。

`Lib2` (15行目) 既存の動作を実装します `Blockchain` 新しいクラスについて `Polkadot` (ルール 1) をタイプセーフ (ルール 3) の方法で、再コンパイルせずに `Lib1` (ルール 4)。 

ルール 2: 既存の表現に適用される新しい動作の実装

`で想像してみましょうLib2` 新しい動作が必要です `lastBlock` それぞれに特別に実装されます `Blockchain`.

最初に思い浮かぶのは、パラメータのタイプに基づいて大きなスイッチを作成することです。

スカラ

def todo = 42
type Height = Int
type Block = Int

object Lib1:
 trait Blockchain:
 def getBlock(height: Height): Block

 case class Ethereum() extends Blockchain:
 override def getBlock(height: Height) = todo

 case class Bitcoin() extends Blockchain:
 override def getBlock(height: Height) = todo

object Lib2:
 import Lib1.*

 case class Polkadot() extends Blockchain:
 override def getBlock(height: Height): Block = todo

 def lastBlock(blockchain: Blockchain): Block = blockchain match
 case _:Ethereum => todo
 case _:Bitcoin => todo
 case _:Polkadot => todo
 

object Lib3:
 import Lib1.*

 case class Polygon() extends Blockchain:
 override def getBlock(height: Height): Block = todo

import Lib1.*, Lib2.*, Lib3.*
println(lastBlock(Bitcoin()))
println(lastBlock(Ethereum()))
println(lastBlock(Polkadot()))
println(lastBlock(Polygon()))

このソリューションは、すでに組み込まれている言語である型ベースのポリモーフィズムを弱い再実装したものです。

`Lib1` は変更されません (この文書全体でルール 4 が適用されていることを思い出してください)。 

`で実装されたソリューションLib2`は まあまあ `でさらに別のブロックチェーンが導入されるまでLib3`。このコードは 3 行目で実行時に失敗するため、タイプ セーフティ ルール (ルール 37) に違反します。Lib2` はルール 4 に違反します。

別の解決策は ` を使用することですextension`.

スカラ

def todo = 42
type Height = Int
type Block = Int

object Lib1:
 trait Blockchain:
 def getBlock(height: Height): Block

 case class Ethereum() extends Blockchain:
 override def getBlock(height: Height) = todo

 case class Bitcoin() extends Blockchain:
 override def getBlock(height: Height) = todo

object Lib2:
 import Lib1.*

 case class Polkadot() extends Blockchain:
 override def getBlock(height: Height): Block = todo

 def lastBlock(): Block = todo

 extension (eth: Ethereum) def lastBlock(): Block = todo

 extension (btc: Bitcoin) def lastBlock(): Block = todo

import Lib1.*, Lib2.*
println(Bitcoin().lastBlock())
println(Ethereum().lastBlock())
println(Polkadot().lastBlock())

def polymorphic(blockchain: Blockchain) =
 // blockchain.lastBlock()
 ???

`Lib1` は変更されません (文書全体でルール 4 が適用されます)。 

`Lib2` はその型の動作を定義し (21 行目)、既存の型の ` 拡張機能を定義します (23 行目と 25 行目)。

28 ~ 30 行目では、新しい動作を各クラスで使用できます。 

しかし、この新しい動作を多態的に呼び出す方法はありません (行 32)。これを試みると、コンパイル エラー (行 33) が発生するか、タイプ ベースのスイッチが発生します。 

このルール 2 は注意が必要です。私たちはポリモーフィズムの独自の定義と「拡張」トリックを使用してそれを実装しようとしました。そしてそれは奇妙でした。

という欠落部分があります アドホック ポリモーフィズム: 動作と型が定義されている場合は常に、型に従って動作実装を安全にディスパッチする機能。を入力 型クラス パターン。

タイプクラスパターン

タイプ クラス (略して TC) パターン レシピには 3 つのステップがあります。 

  1. 新しい動作を定義する
  2. 動作を実装する
  3. 動作を利用する

次のセクションでは、最も簡単な方法で TC パターンを実装します。それは冗長で、扱いにくく、非実用的です。ただし、これらの警告はドキュメント内で段階的に修正される予定ですので、お待ちください。

1. 新しい動作を定義する
スカラ

object Lib2:
 import Lib1.*

 trait LastBlock[A]:
 def lastBlock(instance: A): Block

`Lib1` はまたもや手付かずのままです。

新しい動作 is 特性によって実現されたTC。トレイトで定義された関数は、その動作のいくつかの側面を適用する方法です。

パラメータ `A` は動作を適用するタイプを表します。これは ` のサブタイプですBlockchain` 私たちの場合は。

いくつかの意見:

  • 必要に応じて、パラメータ化された型 `A` は、Scala 型システムによってさらに制約される可能性があります。たとえば、「」を強制することができます。A` になることBlockchain`. 
  • また、TC ではさらに多くの関数を宣言することができます。
  • 最後に、各関数にはさらに多くの任意のパラメーターを含めることができます。

ただし、読みやすくするために、物事を単純にしておきます。

2. 動作を実装する
スカラ

object Lib2:
 import Lib1.*

 trait LastBlock[A]:
 def lastBlock(instance: A): Block

 val ethereumLastBlock = new LastBlock[Ethereum]:
 def lastBlock(eth: Ethereum) = eth.lastBlock

 val bitcoinLastBlock = new LastBlock[Bitcoin]:
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

タイプごとに新しい `LastBlock` 動作が予期されているため、その動作の特定のインスタンスが存在します。 

`Ethereum` 実装行 22 は ` から計算されますeth` インスタンスはパラメータとして渡されます。 

`の実装LastBlock`のための`Bitcoin` 25行目はアンマネージIOを使用して実装されており、そのパラメータは使用されていません。

それで、「Lib2` 新しい動作を実装します `LastBlock`のための`Lib1` クラス。

3. 動作を利用する
スカラ

object Lib2:
 import Lib1.*

 trait LastBlock[A]:
 def lastBlock(instance: A): Block

 val ethereumLastBlock = new LastBlock[Ethereum]:
 def lastBlock(eth: Ethereum) = eth.lastBlock

 val bitcoinLastBlock = new LastBlock[Bitcoin]:
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

import Lib1.*, Lib2.*

def useLastBlock[A](instance: A, behavior: LastBlock[A]) =
 behavior.lastBlock(instance)

println(useLastBlock(Ethereum(lastBlock = 2), ethereumLastBlock))
println(useLastBlock(Bitcoin(), bitcoinLastBlock))

30行目 `useLastBlock` のインスタンスを使用しますA` と `LastBlock` そのインスタンスに定義された動作。

33行目 `useLastBlock` は ` のインスタンスで呼び出されますEthereum` と ` の実装LastBlock` で定義Lib2`。 ` の代替実装を渡すことができることに注意してください。LastBlock[A]` (考えてください 依存性注入).

`useLastBlock` は、表現 (実際の A) とその動作の間の接着剤です。データと動作は分離されており、これが関数型プログラミングが提唱していることです。

議論

式の問題のルールを要約しましょう。

  • ルール 1: の実装を許可する 既存の行動  適用される 新しいクラス
  • ルール 2: の実装を許可する 新しい行動 適用される 既存のクラス
  • ルール 3: 危険を与えてはなりません タイプセーフ
  • ルール 4: 再コンパイルが必要であってはなりません 既存のコード

ルール 1 は、サブタイプ多態性を使用してすぐに解決できます。

先ほど提示した TC パターン (前のスクリーンショットを参照) はルール 2 を解決します。これはタイプ セーフ (ルール 3) であり、` には触れませんでした。Lib1` (ルール 4)。 

ただし、次のような理由により、使用するのは非現実的です。

  • 33 ~ 34 行目では、そのインスタンスに沿って動作を明示的に渡す必要があります。これは余分なオーバーヘッドです。 ` と書けばいいだけですuseLastBlock(Bitcoin())`.
  • 31 行目の構文は一般的ではありません。私たちはむしろ、簡潔でよりオブジェクト指向の文章を書くことを好みます。instance.lastBlock()` ステートメント。

TC を実際に使用するための Scala の機能をいくつか紹介しましょう。 

開発者エクスペリエンスの強化

Scala には、開発者にとって TC を本当に楽しいエクスペリエンスにする独自の機能セットと糖衣構文があります。

暗黙的に

暗黙的スコープは、特定の型のインスタンスが XNUMX つだけ存在できる、コンパイル時に解決される特別なスコープです。 

プログラムは、` を使用してインスタンスを暗黙のスコープに置きます。given` キーワード。あるいは、プログラムはキーワード ` を使用して暗黙のスコープからインスタンスを取得できます。using`.

暗黙的なスコープはコンパイル時に解決され、実行時に動的に変更する方法が知られています。プログラムがコンパイルされると、暗黙的なスコープが解決されます。実行時に、使用される暗黙的なインスタンスが欠落することはあり得ません。唯一考えられる混乱は、間違った暗黙的インスタンスを使用することによって発生する可能性がありますが、この問題は椅子とキーボードの間の生き物に残されます。

次の理由から、グローバル スコープとは異なります。 

  1. 状況に応じて解決されます。プログラムの XNUMX つの場所では、暗黙的スコープ内で同じ特定の型のインスタンスを使用できますが、これら XNUMX つのインスタンスは異なる場合があります。
  2. コードは、暗黙的な使用法に達するまで、舞台裏で暗黙的な引数関数を関数に渡します。グローバルメモリ空間を使用していません。

型クラスに戻りましょう!まったく同じ例を見てみましょう。

スカラ

def todo = 42
type Height = Int
type Block = Int
def http(uri: String): Block = todo

object Lib1:
 trait Blockchain:
 def getBlock(height: Height): Block

 case class Ethereum() extends Blockchain:
 override def getBlock(height: Height) = todo

 case class Bitcoin() extends Blockchain:
 override def getBlock(height: Height) = todo

`Lib1` は、以前に定義したものと同じ未変更のコードです。 

スカラ

object Lib2:
 import Lib1.*

 trait LastBlock[A]:
 def lastBlock(instance: A): Block

 given ethereumLastBlock:LastBlock[Ethereum] = new LastBlock[Ethereum]:
 def lastBlock(eth: Ethereum) = eth.lastBlock

 given bitcoinLastBlock:LastBlock[Bitcoin] = new LastBlock[Bitcoin]:
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

import Lib1.*, Lib2.*

def useLastBlock[A](instance: A)(using behavior: LastBlock[A]) =
 behavior.lastBlock(instance)

println(useLastBlock(Ethereum(lastBlock = 2)))
println(useLastBlock(Bitcoin()))

19行目 新しい動作 `LastBlock` は、前に行ったのとまったく同じように定義されています。

22 行目と 25 行目、`val` は ` に置き換えられますgiven`。 ` の両方の実装LastBlock` は暗黙的なスコープに置かれます。

31行目 `useLastBlock` 動作を宣言します `LastBlock暗黙的なパラメータとして ` を使用します。コンパイラは ` の適切なインスタンスを解決します。LastBlock` 呼び出し元の場所からコンテキスト化された暗黙的なスコープから取得します (行 33 と 34)。 28 行目は ` からすべてをインポートしますLib2` (暗黙的なスコープを含む)。したがって、コンパイラは、行 22 と行 25 で定義されたインスタンスを ` の最後のパラメータとして渡します。useLastBlock`. 

ライブラリ ユーザーとしては、型クラスの使用が以前よりも簡単になりました。行 34 と行 35 では、開発者は動作のインスタンスが暗黙的なスコープに挿入されていることを確認するだけで済みます (これは単なる ` である場合もあります)。import`)。暗黙的が ` ではない場合given` ここでコードは `using` それをコンパイラが彼に伝えます。

Scala は、クラス インスタンスをその動作のインスタンスとともに渡すタスクを暗黙的に容易にします。

暗黙の糖類

前のコードの 22 行目と 25 行目はさらに改善できます。 TC の実装を繰り返してみましょう。

スカラ

given LastBlock[Ethereum] = new LastBlock[Ethereum]:
 def lastBlock(eth: Ethereum) = eth.lastBlock

 given LastBlock[Bitcoin] = new LastBlock[Bitcoin]:
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

22行目と25行目は、インスタンス名が未使用の場合は省略できます。

スカラ


 given LastBlock[Ethereum] with
 def lastBlock(eth: Ethereum) = eth.lastBlock

 given LastBlock[Bitcoin] with
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

22 行目と 25 行目、型の繰り返しは ` に置き換えることができますwith` キーワード。

スカラ

given LastBlock[Ethereum] = _.lastBlock

 given LastBlock[Bitcoin] = _ => http("http://bitcoin/last")

単一の関数を含む縮退特性を使用しているため、IDE は SAM 式を使用してコードを簡素化することを提案する場合があります。それは正しいことですが、カジュアルにコードゴルフをしている場合を除いて、これは SAM の適切な使用法ではないと思います。

Scala は、構文を合理化し、不必要な名前付け、宣言、型の冗長性を取り除くための構文糖衣を提供します。

拡張

賢く使用すると、「extension` メカニズムにより、型クラスを使用するための構文を簡素化できます。

スカラ

object Lib2:
 import Lib1.*

 trait LastBlock[A]:
 def lastBlock(instance: A): Block

 given LastBlock[Ethereum] with
 def lastBlock(eth: Ethereum) = eth.lastBlock

 given LastBlock[Bitcoin] with
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

 extension[A](instance: A)
 def lastBlock(using tc: LastBlock[A]) = tc.lastBlock(instance)

import Lib1.*, Lib2.*

println(Ethereum(lastBlock = 2).lastBlock)
println(Bitcoin().lastBlock)

行 28 ~ 29 は汎用拡張メソッド `lastBlock` は任意の ` に対して定義されていますA` 付きLastBlock` 暗黙的なスコープ内の TC パラメータ。

行 33 ~ 34 では、拡張機能はオブジェクト指向構文を活用して TC を使用します。

スカラ

object Lib2:
 import Lib1.*

 trait LastBlock[A]:
 def lastBlock(instance: A): Block

 given LastBlock[Ethereum] with
 def lastBlock(eth: Ethereum) = eth.lastBlock

 given LastBlock[Bitcoin] with
 def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

 extension[A](instance: A)(using tc: LastBlock[A])
 def lastBlock = tc.lastBlock(instance)
 def penultimateBlock = tc.lastBlock(instance) - 1

import Lib1.*, Lib2.*

val eth = Ethereum(lastBlock = 2)
println(eth.lastBlock)
println(eth.penultimateBlock)

val btc = Bitcoin()
println(btc.lastBlock)
println(btc.penultimateBlock)

28 行目では、繰り返しを避けるために、拡張機能全体に対して TC パラメータを定義することもできます。 30 行目では、拡張子の TC を再利用して ` を定義します。penultimateBlock` (たとえ ` で実装できたとしても)LastBlock` 特性を直接)

TC を使用すると魔法が起こります。表現はより自然に感じられ、動作が「」であるかのような錯覚を与えます。lastBlock` はインスタンスと混同されています。

TC付汎用タイプ
スカラ

import Lib1.*, Lib2.*

def useLastBlock1[A](instance: A)(using LastBlock[A]) = instance.lastBlock

def useLastBlock2[A: LastBlock](instance: A) = instance.lastBlock

val eth = Ethereum(lastBlock = 2)
assert(useLastBlock1(eth) == useLastBlock2(eth))

行 34 では、関数は暗黙的な TC を使用します。名前が不要な場合は、TC に名前を付ける必要がないことに注意してください。

TC パターンは非常に広く使用されているため、「暗黙的な動作を持つ型」を表現するジェネリック型構文が存在します。行 36 の構文は、前の構文 (行 34) よりも簡潔です。これにより、名前のない暗黙的な TC パラメーターを具体的に宣言することが回避されます。

これで開発者エクスペリエンスのセクションは終了です。 TC を使用および定義するときに、拡張機能、暗黙的メソッド、および一部の糖衣構文がどのように乱雑でない構文を提供できるかを見てきました。

自動導出

多くの Scala ライブラリは TC を使用しており、プログラマがコード ベースに実装する必要があります。

たとえば、Circe (json 逆シリアル化ライブラリ) は TC ` を使用します。Encoder[T]`と`Decoder[T]` プログラマーがコードベースに実装するためのものです。実装すると、ライブラリのスコープ全体を使用できるようになります。 

TC のこれらの実装は、多くの場合、 データ指向マッパー。ビジネス ロジックは必要なく、書くのは退屈で、ケース クラスとの同期を維持するのは負担です。

このような状況では、これらのライブラリは、いわゆる 自動 導出または 半自動 導出。たとえばキルケを参照 自動 および 半自動 導出。半自動導出では、プログラマはいくつかのマイナーな構文を使用して型クラスのインスタンスを宣言できますが、自動導出ではインポートを除いてコードを変更する必要がありません。

内部では、コンパイル時に汎用マクロがイントロスペクトします。 純粋なデータ構造として、ライブラリ ユーザー用の TC[T] を生成します。 

一般的に TC を導出するのは非常に一般的であるため、Scala はその目的のための完全なツールボックスを導入しました。このメソッドは、導出を使用する Scala 3 の方法ではありますが、ライブラリのドキュメントで常に宣伝されているわけではありません。

スカラ

object GenericLib:

 trait Named[A]:
 def blockchainName(instance: A): String

 object Named:
 import scala.deriving.*

 inline final def derived[A](using inline m: Mirror.Of[A]): Named[A] =
 val nameOfType: String = inline m match
 case p: Mirror.ProductOf[A] => compiletime.constValue[p.MirroredLabel]
 case _ => compiletime.error("Not a product")
 new Named[A]:
 override def blockchainName(instance: A):String = nameOfType.toLowerCase

 extension[A] (instance: A)(using tc: Named[A])
 def blockchainName = tc.blockchainName(instance)

import Lib1.*, GenericLib.*

case class Polkadot() derives Named
given Named[Bitcoin] = Named.derived
given Named[Ethereum] = Named.derived

println(Ethereum(lastBlock = 2).blockchainName)
println(Bitcoin().blockchainName)
println(Polkadot().blockchainName)

18行目 新しいTC `Named` が導入されています。このTCは厳密に言えばブロックチェーンビジネスとは無関係です。その目的は、ケースクラスの名前に基づいてブロックチェーンに名前を付けることです。

まず、36 ~ 38 行目の定義に注目してください。 TC を導出するには 2 つの構文があります。

  1. 36 行目では、` を使用して TC インスタンスを case クラスに直接定義できます。derives` キーワード。コンパイラは内部で指定された ` を生成します。Named` のインスタンスPolkadot` コンパニオンオブジェクト。
  2. 37 行目と 38 行目、型クラスのインスタンスは ` を使用して既存のクラスに指定されます。TC.derived

行 31 では汎用拡張が定義されており (前のセクションを参照)、`blockchainName` は自然に使われます。  

`derives` キーワードには ` という形式のメソッドが必要ですinline def derived[T](using Mirror.Of[T]): TC[T] = ???` これは 24 行目で定義されています。コードが何を行うかについては詳しく説明しません。大まかに言うと:

  • `inline def` マクロを定義します
  • `Mirror` は型をイントロスペクトするためのツールボックスの一部です。ミラーにはさまざまな種類があり、コードの 26 行目では ` に焦点を当てています。Product` ミラー (ケースクラスは製品です)。 27 行目、プログラマが ` ではないものを導出しようとした場合Product`、コードはコンパイルされません。
  • `Mirror` には他のタイプが含まれます。そのうちのXNUMX人、「MirrorLabel` は型名を含む文字列です。この値は、` の実装の 29 行目で使用されます。Named`TC。

TC 作成者は、メタ プログラミングを使用して、型を指定して TC のインスタンスを一般的に生成する関数を提供できます。プログラマーは、専用のライブラリ API または Scala 派生ツールを使用して、コードのインスタンスを作成できます。

TC を実装するために一般的なコードが必要か特定のコードが必要かにかかわらず、それぞれの状況に応じた解決策があります。 

すべての利点の概要

  • 表現の問題を解決します
    • 新しいタイプは、従来の特性継承を通じて既存の動作を実装できます
    • 新しい動作を既存の型に実装できる
  • 関心事の分離
    • コードは壊れておらず、簡単に削除できます。 TC はデータと動作を分離します。これは関数型プログラミングのモットーです。
  • 安全です
    • イントロスペクションに依存しないため、タイプセーフです。これにより、型に関係する大規模なパターン マッチングが回避されます。このようなコードを書いていることに遭遇すると、TC パターンが完全に適合するケースが見つかるかもしれません。
    • 暗黙的なメカニズムはコンパイル安全です。コンパイル時にインスタンスが見つからない場合、コードはコンパイルされません。実行時に驚くことはありません。
  • アドホックなポリモーフィズムをもたらす
    • アドホックポリモーフィズムは通常、従来のオブジェクト指向プログラミングでは欠落しています。
    • アドホックポリモーフィズムを使用すると、開発者は従来のサブタイピング (コードを結合する) を使用せずに、無関係なさまざまな型に対して同じ動作を実装できます。
  • 依存関係の注入が簡単に
    • TC インスタンスは、Liskov 置換原則に従って変更できます。 
    • コンポーネントが TC に依存している場合、モック化された TC をテスト目的で簡単に挿入できます。 

カウンター表示

すべてのハンマーは、さまざまな問題に対応できるように設計されています。

型クラスは動作上の問題に対応するものであり、データの継承には使用しないでください。そのためにコンポジションを使用します。

通常のサブタイピングはより簡単です。コードベースを所有しており、拡張性を目指していない場合、型クラスは過剰になる可能性があります。

たとえば、Scala コアには ` があります。Numeric` 型クラス:

スカラ

trait Numeric[T] extends Ordering[T] {
 def plus(x: T, y: T): T
 def minus(x: T, y: T): T
 def times(x: T, y: T): T

このような型クラスを使用することは、Scala に埋め込まれた型 (Int、BigInt など) だけでなく、ユーザー定義型 (`ComplexNumber` たとえば)。

一方、Scala コレクションの実装では、ほとんどの場合、型クラスの代わりにサブタイプが使用されます。この設計はいくつかの理由から理にかなっています。

  • コレクション API は完全で安定している必要があります。実装によって継承された特性を通じて一般的な動作を公開します。拡張性が高いことは、ここでは特別な目標ではありません。
  • 使い方は簡単でなければなりません。 TC は、エンド ユーザー プログラマに精神的なオーバーヘッドを追加します。
  • TC では、パフォーマンスにわずかなオーバーヘッドが発生する可能性もあります。これはコレクション API にとって重要な場合があります。
  • ただし、コレクション API は、サードパーティのライブラリによって定義された新しい TC を通じて拡張可能です。

まとめ

TC は大きな問題を解決する単純なパターンであることがわかりました。 Scala の豊富な構文のおかげで、TC パターンをさまざまな方法で実装して使用できます。 TC パターンは関数型プログラミングのパラダイムに沿っており、クリーンなアーキテクチャのための素晴らしいツールです。特効薬はなく、適合する場合は TC パターンを適用する必要があります。

このドキュメントを読んで知識を得ていただければ幸いです。 

コードは次の場所で入手できます https://github.com/jprudent/type-class-article。ご質問やご意見がございましたら、お気軽にお問い合わせください。必要に応じて、リポジトリ内の課題やコード コメントを使用できます。


ジェローム・プルデント

ソフトウェアエンジニア

タイムスタンプ:

より多くの 元帳