Scalaタイプのプログラミングリソース


102

この質問によると、Scalaの型システムはチューリング完全です。初心者がタイプレベルのプログラミングの力を利用できるようにするために、どのようなリソースが利用できますか?

これまでに見つけたリソースは次のとおりです。

これらのリソースは素晴らしいですが、基本が足りないように感じるので、構築するための確固たる基盤がありません。たとえば、型定義の概要はどこにありますか?タイプに対してどのような操作を実行できますか?

優れた紹介リソースはありますか?


個人的には、Scalaで型レベルのプログラミングをしたい人は、Scalaでプログラミングを行う方法をすでに十分に理解しているという前提に私は気づきました。それは私はあなたが:-)にリンクされ、これらの記事の言葉を理解していないことを意味していても
イェルクWミッターク

回答:


140

概観

型レベルのプログラミングには、従来の値レベルのプログラミングと多くの類似点があります。ただし、実行時に計算が行われる値レベルのプログラミングとは異なり、型レベルのプログラミングでは、計算はコンパイル時に行われます。値レベルでのプログラミングと型レベルでのプログラミングの間に類似点を描こうとします。

パラダイム

型レベルのプログラミングには、「オブジェクト指向」と「関数型」という2つの主要なパラダイムがあります。ここからリンクされているほとんどの例は、オブジェクト指向のパラダイムに従います。

オブジェクト指向のパラダイムでの型レベルのプログラミングの非常に単純で優れた例は、ここに複製されたラムダ計算の apocalispの実装にあります。

// Abstract trait
trait Lambda {
  type subst[U <: Lambda] <: Lambda
  type apply[U <: Lambda] <: Lambda
  type eval <: Lambda
}

// Implementations
trait App[S <: Lambda, T <: Lambda] extends Lambda {
  type subst[U <: Lambda] = App[S#subst[U], T#subst[U]]
  type apply[U] = Nothing
  type eval = S#eval#apply[T]
}

trait Lam[T <: Lambda] extends Lambda {
  type subst[U <: Lambda] = Lam[T]
  type apply[U <: Lambda] = T#subst[U]#eval
  type eval = Lam[T]
}

trait X extends Lambda {
  type subst[U <: Lambda] = U
  type apply[U] = Lambda
  type eval = X
}

この例からわかるように、型レベルのプログラミングのオブジェクト指向パラダイムは次のように進行します。

  • まず、さまざまな抽象型フィールドで抽象特性を定義します(抽象フィールドについては、以下を参照してください)。これは、特定の型フィールドが実装を強制せずにすべての実装に存在することを保証するためのテンプレートです。ラムダ計算例では、この対応するtrait Lambdaことを保証するには、次のタイプが存在すること:substapply、およびeval
  • 次:抽象特性を拡張し、さまざまな抽象型フィールドを実装するサブ特性を定義する
    • 多くの場合、これらのサブトレイトは引数でパラメーター化されます。ラムダ計算の例では、サブタイプはtrait App extends Lambda2つのタイプでパラメーター化されています(SおよびT、両方とものサブタイプである必要がありますLambda)、trait Lam extends Lambda1つのタイプでパラメーター化されています(T)、およびtrait X extends Lambda(パラメーター化されていません)。
    • タイプフィールドは、サブトレイトのタイプパラメータを参照することで実装されることが多く、ハッシュ演算子を介してタイプフィールドを参照することもあります#.値のドット演算子と非常によく似ています)。形質にAppラムダ計算例の、タイプはeval次のように実装されますtype eval = S#eval#apply[T]。これは基本的evalに、特性のパラメーターのタイプをS呼び出し、結果にapplyパラメーターを指定Tして呼び出します。注は、S持っていることが保証されevalたパラメータは、サブタイプのように、それを指定するためのタイプをLambda。同様に、抽象traitで指定されているように、のサブタイプとして指定されているため、の結果にevalapplyタイプが必要です。LambdaLambda

機能パラダイムは、特性にグループ化されていない多くのパラメーター化された型コンストラクターを定義することで構成されます。

値レベルのプログラミングと型レベルのプログラミングの比較

  • 抽象クラス
    • 値レベル: abstract class C { val x }
    • タイプレベル: trait C { type X }
  • パス依存型
    • C.x (オブジェクトCのフィールド値/関数xを参照)
    • C#x (特性Cのフィールドタイプxを参照)
  • 関数シグネチャ(実装なし)
    • 値レベル: def f(x:X) : Y
    • タイプレベル:(type f[x <: X] <: Yこれは「タイプコンストラクター」と呼ばれ、通常は抽象的な特性で発生します)
  • 関数の実装
    • 値レベル: def f(x:X) : Y = x
    • タイプレベル: type f[x <: X] = x
  • 条件付き
  • 同等性のチェック
    • 値レベル: a:A == b:B
    • タイプレベル: implicitly[A =:= B]
    • 値レベル:実行時のユニットテストを介してJVMで発生します(つまり、実行時エラーはありません)。
      • 本質的には断言です: assert(a == b)
    • 型レベル:型チェックを介してコンパイラで発生します(つまり、コンパイラエラーはありません)。
      • 本質的には型の比較です:例 implicitly[A =:= B]
      • A <:< B、のAサブタイプである場合にのみコンパイルしますB
      • A =:= B、場合にのみ、コンパイルAのサブタイプであるBBのサブタイプでありますA
      • A <%< B、( "viewable as")は、がAとして表示可能な場合にのみコンパイルされますB(つまり、Aからのサブタイプへの暗黙的な変換がありますB)。
      • その他の比較演算子

タイプと値の間の変換

  • 多くの例では、トレイトを介して定義された型は抽象的であり、かつシールされていることが多いため、直接インスタンス化することも、匿名サブクラスを介してインスタンス化することもできません。したがって、nullあるタイプの対象を使用して値レベルの計算を行う場合、プレースホルダー値として使用するのが一般的です。

    • 例:気になるタイプはval x:A = nullどこAですか
  • 型消去のため、パラメーター化された型はすべて同じに見えます。さらに、(上記のように)作業している値はすべてになる傾向があるnullため、オブジェクトタイプの条件付け(たとえば、matchステートメントによる)は効果がありません。

秘訣は、暗黙の関数と値を使用することです。基本ケースは通常暗黙的な値であり、再帰的ケースは通常暗黙的な関数です。確かに、型レベルのプログラミングは暗黙のうちに頻繁に使用されます。

次の例を考えてみてください(metascalaおよびapocalisp から取得):

sealed trait Nat
sealed trait _0 extends Nat
sealed trait Succ[N <: Nat] extends Nat

ここに自然数のペアノエンコーディングがあります。つまり、負でない整数ごとに型があります。0の特別な型、つまり_0; ゼロより大きい各整数には、の形式のタイプがありますSucc[A]。ここAで、は、より小さい整数を表すタイプです。たとえば、2を表す型は次のようになりますSucc[Succ[_0]](ゼロを表す型に後続が2回適用されます)。

より便利な参照のために、さまざまな自然数をエイリアスできます。例:

type _3 = Succ[Succ[Succ[_0]]]

(これはval、関数の結果であると定義するのとよく似ています。)

ここで、の型にエンコードされた自然数に準拠し、それを返す整数を返すdef toInt[T <: Nat](v : T)引数値vを受け取る値レベルの関数を定義するNatとしますv。たとえば、val x:_3 = nullnull型のSucc[Succ[Succ[_0]]])値がある場合、をtoInt(x)返し3ます。

を実装toIntするには、次のクラスを使用します。

class TypeToValue[T, VT](value : VT) { def getValue() = value }

我々は以下を参照するように、そこクラスから構築対象となるTypeToValueそれぞれのためNat_0(例えば)まで_3、それぞれが対応するタイプ(すなわち、の値表現を格納するTypeToValue[_0, Int]値を格納する0TypeToValue[Succ[_0], Int]値を格納する1、など)。TypeToValueは、Tおよびの2つのタイプでパラメーター化されていますVTT値を割り当てようとしているタイプ(この例ではNat)にVT対応し、値を割り当てようとしているタイプ(この例では)に対応していますInt

ここで、次の2つの暗黙の定義を作成します。

implicit val _0ToInt = new TypeToValue[_0, Int](0)
implicit def succToInt[P <: Nat](implicit v : TypeToValue[P, Int]) = 
     new TypeToValue[Succ[P], Int](1 + v.getValue())

そしてtoInt、次のように実装します。

def toInt[T <: Nat](v : T)(implicit ttv : TypeToValue[T, Int]) : Int = ttv.getValue()

どのようにtoInt機能するかを理解するために、いくつかの入力でそれが何をするかを考えてみましょう:

val z:_0 = null
val y:Succ[_0] = null

を呼び出すtoInt(z)と、コンパイラは(の型であるため)ttv型の暗黙の引数を探します。それはオブジェクトを見つけ、このオブジェクトのメソッドを呼び出して戻ります。注意すべき重要な点は、使用するオブジェクトをプログラムに指定しなかったということです。コンパイラはそれを暗黙的に見つけました。TypeToValue[_0, Int]z_0_0ToIntgetValue0

今考えてみましょうtoInt(y)。今回は、コンパイラーttvはタイプの暗黙の引数を探します(タイプがであるTypeToValue[Succ[_0], Int]ため)。適切なタイプ()のオブジェクトを返すことができる関数を見つけて評価します。この関数自体は、型の暗黙の引数()を取ります(つまり、最初の型パラメーターのは1つ少なくなります)。コンパイラーは(上記の評価で行ったように)供給し、valueで新しいオブジェクトを構築します。繰り返しますが、明示的にアクセスすることはできないため、コンパイラがこれらの値をすべて暗黙的に提供していることに注意することが重要です。ySucc[_0]succToIntTypeToValue[Succ[_0], Int]vTypeToValue[_0, Int]TypeToValueSucc[_]_0ToInttoInt(z)succToIntTypeToValue1

あなたの仕事をチェックする

型レベルの計算が期待どおりに機能していることを確認するには、いくつかの方法があります。ここにいくつかのアプローチがあります。確認する2つのタイプABが等しいことを確認します。次に、次のコンパイルを確認します。

または、タイプを値に変換し(上記を参照)、値の実行時チェックを行うこともできます。たとえばassert(toInt(a) == toInt(b))aはタイプでAあり、bはタイプBです。

追加のリソース

利用可能なコンストラクトの完全なセットは、Scalaリファレンスマニュアル(pdf)のタイプセクションにあります。

Adriaan Moorsは、タイプコンストラクターに関するいくつかの学術論文と、scalaの例を含む関連トピックを持っています。

Apocalispは、Scalaでの型レベルのプログラミングの多くの例が含まれているブログです。

  • Scalaの型レベルのプログラミングは、ブール値、自然数(上記のとおり)、2進数、異種リストなどを含む、いくつかの型レベルのプログラミングの素晴らしいガイド付きツアーです。
  • Scala Typehackeryは、上記のラムダ計算の実装です。

ScalaZは非常にアクティブなプロジェクトであり、さまざまなタイプレベルのプログラミング機能を使用してScala APIを拡張する機能を提供しています。これは非常に興味深いプロジェクトで、大きな支持を得ています。

MetaScalaはScalaのタイプレベルライブラリで、自然数、ブール値、単位、HListなどのメタタイプを含みます。これはJesper Nordenberg(彼のブログ)によるプロジェクトです。

Michid(ブログ)は、(他の回答から)Scalaではタイプレベルのプログラミングのいくつかの素晴らしい例があります:

Debasish Ghosh(ブログ)にもいくつかの関連する投稿があります:

(私はこの主題についていくつかの調査を行っており、これは私が学んだことです。私はまだそれを知らないので、この回答の不正確さを指摘してください。)


12

興味深いブログに感謝したいだけです。私はしばらくそれに従っていて、特に上記の最後の投稿は、オブジェクト指向言語の型システムが持つべき重要なプロパティの私の理解を鋭くしました。ほんとありがと!
Zach Snow



弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.