1. 概要

Scalaには強い型のシステムがあり、コンパイル時に制限やチェックを増やしてコードを記述できます。 型システムでロジックをエンコードすることにより、ランタイムにエラーを導入することなく、コンパイル時にエラーを検出できます。

特に1つの機能は、パス依存型と呼ばれる一種の依存型です。この記事では、パス依存型とそのユースケースを紹介します。

2. パス依存型

依存型は、定義が値に依存する型です。 Listのサイズに制約があるList、たとえばNonEmptyListを記述できるとします。 また、AtLeastTwoMemberListのような別のタイプを想定します。
パス依存型は、依存値がパスである特定の種類の依存型です。 Scalaには、値に依存する型の概念があります。 この依存関係は、型シグネチャではなく、型配置で表されます。

2.1. タイプメンバーとパス依存型

Scalaでは、トレイトはタイプのメンバーを持つことができます。

trait Input {
  type Output
  val value: Output
}

ここで、InputトレイトにはOutputタイプのメンバーがあります。 値のタイプはOutput、であり、パスに依存します。 これは、 value のタイプが、Input特性の実装に基づいて変化することを意味します。

依存型は型パラメーターとして使用できます。 次の関数は、依存関数です。これは、関数の出力がその入力に依存しているためです。

def dependentFunc(i: Input): i.Output = i.value

Input トレイトからいくつかのインスタンスを作成し、dependentFuncの動作を調べてみましょう。

def valueOf[T](v: T) = new Input {
  type Output = T
  val value: T = v
}

val intValue    = valueOf(1)
val stringValue = valueOf("One")

assert(dependentFunc(intValue) == 1)
assert(dependentFunc(stringValue) == "One")

ご覧のとおり、dependentFuncの出力タイプは入力ごとに異なります。

2.2. 内部クラスとパス依存型

Scalaでは、クラスはメンバーとして他のクラスを持つことができます。

class Foo {
  class Bar
}

ここで、BarクラスはFooクラスのメンバーです。 Innerクラスの型は外部オブジェクトにバインドされ、その型は値(Fooクラスの任意のインスタンス)に依存するため、Bar型はパス依存型になります。のインスタンスを作成しましょうFooおよびBarクラスを使用して、それらのタイプを確認します。

val f1 = new Foo 
val b1: f1.Bar = new f1.Bar 
val f2 = new Foo 
val b2: f2.Bar = new f2.Bar
assert(b1 != b2) 

b1b2のタイプは同じではありません。 b1のタイプはf1.Barで、b2のタイプはf2.Barです。 したがって、これらをパス依存型と呼びます。 このタイプの依存関係は、内部クラスが親インスタンスにバインドされるクラスを作成するのに役立ちます。

3. 例

3.1. 型付きKey-Valueデータストア

Key-Valueデータストアがあると仮定します。 すべてのキーはString、ですが、各KeyValueTypeは他のキーとは異なる場合があります。 各キー値のValueTypeKeyタイプでエンコードできます。 キー値を設定する場合、 key.ValueType。の適切なエンコーダーを使用して値をエンコードできるため、key.ValueTypeをパス依存型にします。

まず、キーの名前と値の型を含む抽象クラスを作成しましょう。

abstract class Key(val name: String) {
  type ValueType
}

Key、のインスタンスがあるときはいつでも、 ValueType メンバーを参照することにより、そのキーの値型にアクセスできます。

次に、 Operations taitで、一般的なキー値ストアの2つの一般的な操作であるsetとgetを紹介します。

trait Operations {
  def set(key: Key)(value: key.ValueType)(implicit enc: Encoder[key.ValueType]): Unit
  def get(key: Key)(implicit decoder: Decoder[key.ValueType]): Option[key.ValueType]
}

key パラメーターに加えて、 set メソッドには2番目のパラメーターグループがあり、 key、key.ValueTypeの値型を取得します。 。 また、3番目のパラメーターセクションでは、エンコーダーを暗黙的なパラメーターとして取得しています。 この場合、エンコーダーは key-value Decoder [key.ValueType] )のValueTypeに依存します。

get メソッドには微妙な違いがあり、getメソッドの出力タイプはOption[k.ValeType] であり、出力はに依存します。 key値。 set メソッドは、依存関数です。

次に、Databaseクラスおよび必要なその他のユーティリティにそれらを実装します。

case class Database() extends Operations {

  private val db = mutable.Map.empty[String, Array[Byte]]

  def set(k: Key)(v: k.ValueType)(implicit enc: Encoder[k.ValueType]): Unit =
    db.update(k.name, enc.encode(v))

  def get(
    k: Key
  )(implicit decoder: Decoder[k.ValueType]): Option[k.ValueType] = {
    db.get(k.name).map(x => decoder.encode(x))
  }

}

コンパニオンオブジェクトにキーを作成するためのヘルパー関数を追加しましょう。

object Database {
  def key[Data](v: String) =
  new Key(v) {
    override type ValueType = Data
  }
}

Key-Valueストアに値を格納するには、値を Byte[Array]にエンコードするEncoder型クラスが必要です。

trait Encoder[T] {
  def encode(t: T): Array[Byte]
}

StringおよびDoubleインスタンスのEncoderが必要です。これをEncoderコンパニオンオブジェクトで定義しましょう。

object Encoder {
  implicit val stringEncoder: Encoder[String] = new Encoder[String] {
    override def encode(t: String): Array[Byte] = t.getBytes
  }

  implicit val doubleEncoder: Encoder[Double] = new Encoder[Double] {
    override def encode(t: Double): Array[Byte] = {
      val bytes = new Array[Byte](8)
      ByteBuffer.wrap(bytes).putDouble(t)
      bytes
    }
  }
}

また、 Decoder 型クラスを定義して、値を Byte[Array]から元のT型に変換するインスタンスを作成しましょう。

trait Decoder[T] {
  def encode(d: Array[Byte]): T
}

object Decoder {
  implicit val stringDecoder: Decoder[String] = (d: Array[Byte]) =>
    new String(d)
  implicit val intDecoder: Decoder[Double] = (d: Array[Byte]) =>
    ByteBuffer.wrap(d).getDouble
}

これで、Key-Valueデータベースを作成し、putおよびget操作をテストする準備が整いました。

val db = Database()
import Database._
val k1 = key[String]("key1")
val k2 = key[Double]("key2")

db.set(k1)("One")
db.set(k2)(1.0)
assert(db.get(k1).contains("One"))
assert(db.get(k2).contains(1.0))

この例では、依存型を使用することで、重複した不要なコードを記述できなくなり、アドホック多相性をsetおよびgetAPIに適用するのに役立ちます。

3.2. 親の賞と罰しつけ

このセクションでは、依存型の別のユースケースを紹介します。 罰と報酬の親の分野をモデル化したいとします。 すべての親はどの子供にも報酬を与えることができますが、他の子供を罰することはできません

case class Parent(name: String) {
  class Child

  def child = new this.Child

  def punish(c: this.Child): Unit =
    println(s"$name is punishing $c")

  def reward(c: Parent#Child): Unit =
    println(s"$name is rewarding $c")
}

punish メソッドの引数は、 this.Child 型に依存しているため、型に依存する引数になります。 したがって、 Parent インスタンスが2つある場合、 john、 scarlet、 j ohn は自分の子供を罰することはできますが、彼は罰することはできません。 s carletのの子:

val john = Parent("John")
val scarlet = Parent("Scarlet")

john.punish(john.child)
// john.punish(scarlet.child) //Compile time error

punish 引数に依存型を使用すると、コンパイル時に無関係なオブジェクトでpunishが実行されるのを防ぐことができます。

4. 結論

この記事では、Scalaのパス依存型について説明しました。 Scalaのパス依存型は、実行時にこれらのバグを減らし、型システムでロジックをエンコードするための良い方法です。これにより、手動テストを作成する際の余分な労力が軽減されます。

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