1. 概要

ジェネリックプログラミングでは、コードを繰り返さずにプログラムを作成できます。 Scalaでは、シェイプレスライブラリは、値レベルから型レベルの計算を行うことにより、一般的なデータ型、型クラス、および操作を導入します。

この記事では、シェイプレスのいくつかのユースケースを学習しますが、ジェネリックプログラミングは幅広いトピックであるため、すべてを網羅するわけではありません。

2. 一般的およびタイプレベルのプログラミング

ジェネリックプログラミングは、型をプログラミングし、後で指定する具体的な型を延期する方法です。

たとえば、IntListおよびStringListデータ型を記述する代わりに、汎用 List [T] データ型を記述し、それにいくつかの汎用操作とコンビネーターを定義できます。 、ヘッドテールマップサイズなど。 これは、1回の書き込みで、任意のデータ型で複数回再利用するのに役立ちます。

val intList: List[Int] = List(1, 2, 3)
val stringList: List[String] = List("foo", "bar", "baz")

assert(intList.head == 1)
assert(stringList.head == "foo")

Shapelessは、型レベルプログラミングを広範囲に使用して、ジェネリックデータ型と型クラスを提供し、ジェネリックプログラミングを容易にします。 では、型レベルのプログラミングとは何ですか? 型レベルプログラミングは、型システムで、コンパイル時に評価される計算をエンコードする手法です。

情報を値レベルから型レベルのプログラミングに移すとき、エンコードされたロジックはコンパイル時にチェックされます。 これはバグのないプログラムを書くのに役立つので、プログラムがコンパイルされれば、実行時に正しく動作します。

タイプレベルのプログラミングで解決できる実際の問題を見てみましょう。 異なるタイプのリストがあると仮定します。

val list: List[Any] = List(1, 1.0, "One", false)

では、 head 関数を使用してリストの最初の要素を抽出するとどうなりますか? 数値1Intデータ型として返すと予想されますが、 Any として返されるため、最初の要素の型にアクセスできません。 。

では、Scalaでこれを適切に行うにはどうすればよいでしょうか? タイプレベルのプログラミングの助けを借りて、メンバータイプをリスト定義にエンコードする異種リストを作成できます。これは別のトピックであるため、これについて深く掘り下げることはしません。代わりに、シェイプレスのHList HList は、実行時に各要素タイプを保持できる異種リストです。

shapelessが提供するデータ型と型クラスはたくさんあります。 この記事では、HList、Coproduct、Generic、LabelledGeneric、ポリモーフィック関数などのより重要な関数の一部のみを取り上げます。

3. ジェネリックス

圏論のすべての構造には二重があり、製品タイプには余積(または合計タイプ)と呼ばれる二重があります。 形のないライブラリでは、製品の構造は HList そのデュアルは名前が付けられています副産物。

3.1. 異種リスト( HList

HList は、リストとタプルの両方の特性の組み合わせです。

  • タプルは、コンパイル時に異なるタイプの要素の固定長です。 タプルのアリティを修正すると、それらに固執します。 一方、タプルの各要素は異なるタイプにすることができますが、定義後、タプルのアリティを拡張することはできません。
  • リストは、すべて同じタイプの要素の可変長シーケンスです

HList には、タプルとリストの機能が組み合わされています。 タプルから異なるタイプのシーケンスをキャプチャし、リストから要素の可変長シーケンスをキャプチャします。 したがって、 HListは、異なるタイプの要素の可変長シーケンスです

import shapeless._
import HList._
val hlist = 1 :: 1.0 :: "One" :: false ::‌ HNil

ここで、 hlist のタイプは、Scalaの標準コレクションライブラリにある一般的なListタイプとは大きく異なります。 そのタイプは Int :: Double :: String :: Boolean :: HNill 。 変! それでは、このデータ型の意味を解読しましょう。 シェイプレスでのHListの定義は次のとおりです。

sealed trait HList extends Product with Serializable

final case class ::[+H, +T <: HList](head : H, tail : T) extends HList

sealed trait HNil extends HList {
  def ::[H](h : H) = shapeless.::(h, this)
}

case object HNil extends HNil

HListは再帰的なデータ構造です。 各HListは、空のリスト(HNil)か、HTタイプのペアのいずれかです。 X120X](ヘッド)は任意のタイプであり、 T (テール)は別のHListです。Scalaでは、 :: [H、T]のような任意のペアタイプを記述できます。 H:: T、のようなより人間工学的な方法であるため、hlistのタイプはInt:: Double :: String :: Boolean::HNillのいずれかです。 または::[Int、:: [Double、:: [String、:: [Boolean、HNill]]]]

標準ライブラリと同様に、 map flatMap head などのコンビネーターの束をHListで呼び出すことができます。テール

assert(list.head == 1)
assert(list.take(2) == 1 :: 1.0 :: HNil)
assert(list.tail == 1.0 :: "One" :: false :: HNil)

HListには多くのユースケースがあります。 たとえば、 Generic 型クラスを使用すると、任意のケースクラスを HList、に変換してから、 HList を反復処理して、CSVエンコーダーを作成できます。そのために。

3.2. 副産物

標準ライブラリでは、通常、封印された特性を使用して副産物を作成します。 たとえば、信号機を次のようにモデル化できます。

selead trait TrafficLight
case class Green() extends TrafficLight
case class Red() extends TrafficLight
case class Yellow() extends TrafficLight

ご覧のとおり、信号機の考えられる各状態は、個別のケースクラスとしてモデル化されており、それぞれが封印されたTrafficLight特性を拡張しています。

シェイプレスには、 Coproduct データ型があり、合計型を定義するためのより強力な機能を提供します。 Shapelessの副産物も、HListのような再帰的なデータ構造です。Shapelessを使用して信号機をモデル化してみましょう。

import shapeless._
object Green
object Red
object Yellow
type Light = Green.type :+: Red.type :+: Yellow.type :+: CNil

これで、LightタイプのRedインスタンスを作成できます。

val light: Light = Coproduct[Light](Red)

次に、ライトが赤かどうかを確認しましょう。

assert(light.select[Red.type] == Some(Red)
assert(light.select[Green.type] == None)

head tail drop map など、このデータ型で実行できるコンビネーターと操作はたくさんあります。 。

4. 一般的な型クラス

Generic タイプクラスは、一般的な製品/副産物タイプ(具体的なケースクラス/タプルまたはクラス/特性の封印されたファミリ)を対応するジェネリックタイプに変換できます。 Generic トレイトには、tofromの2つの方法があります。

trait Generic[T] extends Serializable {
  type Repr
  def to(t : T) : Repr
  def from(r : Repr) : T
}

内部型Reprは、型の一般的な表現を含むパス依存型です。 T。 言い換えれば、 担当者タイプは、インスタンス化するタイプによって異なります。 ジェネリック Generic をケースクラスでインスタンス化すると、表現は HList になり、封印されたクラスのファミリーでインスタンス化すると、表現はCoproductになります。

4.1. 製品変換( HList

タプルやケースクラスなど、 T のすべての製品タイプについて、 Generic タイプクラスのインスタンスは、を使用してその製品タイプをHListに変換できます。 to メソッド:

import shapeless._
case class User(name: String, age: Int)
val user = User("John", 25)
val userHList = Generic[User].to(user)
assert(userHList == "John" :: 25 :: HNil)

また、userHListUserケースクラスに戻すこともできます。

val userRecord: User = Generic[User].from(userHList)
assert(user == userRecord)

このようにして、任意の製品タイプで機能する汎用CSVシリアライザー/デシリアライザーを作成できます。 任意のケースクラス/タプルをHListに変換してから、 HList をCSVレコードに、またはその逆にシリアル化できます。

4.2. 副産物変換

同様に、TrafficLightを形状のないCoproductに変換できます。

val gen = Generic[TrafficLight]
val green = gen.to(Green())
val red = gen.to(Red())
val yellow = gen.to(Yellow())

TrafficLight の各ファミリメンバーは、ネストされたInlおよびInrにエンコードされます。

assert(green == Inl(Green()))
assert(red == Inr(Inl(Red())))
assert(yellow == Inr(Inr(Inl(Yellow()))))

5. LabelledGenericタイプクラス

Generic 型クラスは、ケースクラスのフィールド名を記憶していませんが、LabelledGeneric型クラスは次のことを記憶しています。

import shapeless._
import record._
val user = User("John", 25)
val userGen = LabelledGeneric[User]
val userLaballedRecord = userGen.to(user)

LabelledGeneric は、タイプレベルの計算でフィールド名をラベルとしてエンコードします。

assert(userLaballedRecord('name) == "John")
assert(keys() == 'name :: 'age :: HNil)

これは、任意のケースクラスをJSONASTに変換する汎用JSONエンコーダーを作成するのに役立ちます。 Circe、Argonount、PlayJSONなどの多くのScalaJSONライブラリは、このアプローチで自動派生を実行します。

6. ポリモーフィック関数

リストがあると仮定します。

val list = List("foo", "bar")

そして、リストをマッピングして各要素のサイズを決定するとします。 どうすればいいですか? length 関数を使用して、リストをマップできます。

val list: List[String] = List("foo", "bar")
def length: String => Int = _.length
val lengthList = list.map(length)

lengthは、入力が文字列に対してのみ機能するため、単相関数です。 しかし、異種のリストがある場合はどうなりますか?

val list = List(1, 2) :: "123" :: Array(1, 2, 3, 4) :: HNil

このリストの各項目には異なるタイプがありますが、マップ関数のタイプは何ですか? Any => Int Anyを使用することはお勧めできません。 List[Int]またはStringまたはArray[String]を入力として受け入れることができる関数を定義できるとしたらどうでしょうか。 この関数の出力は、入力パラメーターのタイプによって異なります。 Shapelessは、ポリモーフィック関数を作成するためのPolyというタイプを提供します

長さポリモーフィック関数をPolyで記述してみましょう。

import shapeless._
object polyLength extends Poly1 {
  implicit val listCase = at[List[Int]](i => i.length)
  implicit val stringCase = at[String](d => d.length)
  implicit val arrayCase = at[Array[Int]](d => d.length)
}

polyLengthapplyメソッドはさまざまなデータ型を受け入れ、それぞれの場合、その暗黙のケースの1つが呼び出されます。

assert(polyLength.apply(List(1, 2)) == 2)
assert(polyLength.apply("123") == 3)
assert(polyLength.apply(Array(1, 2, 3, 4)) == 4)
assert(list.map(polyLength) == 2 :: 3 :: 4 :: HNil)

7. 結論

このチュートリアルでは、タイプレベルのプログラミングのコンテキストでジェネリックプログラミングとは何か、そしてシェイプレスによってジェネリックプログラムを簡単に作成できることを学びました。

最初に、HListおよびCoproductデータ型を使用して、形状のない異種製品および副産物を作成する方法を学習しました。

次に、GenericおよびLabelledGeneric型クラスに精通しました。これらは、一般的な製品および副産物の型を対応する一般的な型に、またはその逆に変換するのに役立ちます。

最後に、ポリモーフィック関数とは何か、そしてシェイプレスライブラリがこの機構をどのように提供するかを学びました。

いつものように、コードはGitHubから入手できます。