この質問によると、Scalaの型システムはチューリング完全です。初心者がタイプレベルのプログラミングの力を利用できるようにするために、どのようなリソースが利用できますか?
これまでに見つけたリソースは次のとおりです。
これらのリソースは素晴らしいですが、基本が足りないように感じるので、構築するための確固たる基盤がありません。たとえば、型定義の概要はどこにありますか?タイプに対してどのような操作を実行できますか?
優れた紹介リソースはありますか?
この質問によると、Scalaの型システムはチューリング完全です。初心者がタイプレベルのプログラミングの力を利用できるようにするために、どのようなリソースが利用できますか?
これまでに見つけたリソースは次のとおりです。
これらのリソースは素晴らしいですが、基本が足りないように感じるので、構築するための確固たる基盤がありません。たとえば、型定義の概要はどこにありますか?タイプに対してどのような操作を実行できますか?
優れた紹介リソースはありますか?
回答:
概観
型レベルのプログラミングには、従来の値レベルのプログラミングと多くの類似点があります。ただし、実行時に計算が行われる値レベルのプログラミングとは異なり、型レベルのプログラミングでは、計算はコンパイル時に行われます。値レベルでのプログラミングと型レベルでのプログラミングの間に類似点を描こうとします。
パラダイム
型レベルのプログラミングには、「オブジェクト指向」と「関数型」という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
ことを保証するには、次のタイプが存在すること:subst
、apply
、およびeval
。trait App extends Lambda
2つのタイプでパラメーター化されています(S
およびT
、両方とものサブタイプである必要がありますLambda
)、trait Lam extends Lambda
1つのタイプでパラメーター化されています(T
)、およびtrait X extends Lambda
(パラメーター化されていません)。#
(.
値のドット演算子と非常によく似ています)。形質にApp
ラムダ計算例の、タイプはeval
次のように実装されますtype eval = S#eval#apply[T]
。これは基本的eval
に、特性のパラメーターのタイプをS
呼び出し、結果にapply
パラメーターを指定T
して呼び出します。注は、S
持っていることが保証されeval
たパラメータは、サブタイプのように、それを指定するためのタイプをLambda
。同様に、抽象traitで指定されているように、のサブタイプとして指定されているため、の結果にeval
はapply
タイプが必要です。Lambda
Lambda
機能パラダイムは、特性にグループ化されていない多くのパラメーター化された型コンストラクターを定義することで構成されます。
値レベルのプログラミングと型レベルのプログラミングの比較
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]
assert(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 = null
(null
型のSucc[Succ[Succ[_0]]]
)値がある場合、をtoInt(x)
返し3
ます。
を実装toInt
するには、次のクラスを使用します。
class TypeToValue[T, VT](value : VT) { def getValue() = value }
我々は以下を参照するように、そこクラスから構築対象となるTypeToValue
それぞれのためNat
の_0
(例えば)まで_3
、それぞれが対応するタイプ(すなわち、の値表現を格納するTypeToValue[_0, Int]
値を格納する0
、TypeToValue[Succ[_0], Int]
値を格納する1
、など)。TypeToValue
は、T
およびの2つのタイプでパラメーター化されていますVT
。T
値を割り当てようとしているタイプ(この例では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
_0ToInt
getValue
0
今考えてみましょうtoInt(y)
。今回は、コンパイラーttv
はタイプの暗黙の引数を探します(タイプがであるTypeToValue[Succ[_0], Int]
ため)。適切なタイプ()のオブジェクトを返すことができる関数を見つけて評価します。この関数自体は、型の暗黙の引数()を取ります(つまり、最初の型パラメーターのは1つ少なくなります)。コンパイラーは(上記の評価で行ったように)供給し、valueで新しいオブジェクトを構築します。繰り返しますが、明示的にアクセスすることはできないため、コンパイラがこれらの値をすべて暗黙的に提供していることに注意することが重要です。y
Succ[_0]
succToInt
TypeToValue[Succ[_0], Int]
v
TypeToValue[_0, Int]
TypeToValue
Succ[_]
_0ToInt
toInt(z)
succToInt
TypeToValue
1
あなたの仕事をチェックする
型レベルの計算が期待どおりに機能していることを確認するには、いくつかの方法があります。ここにいくつかのアプローチがあります。確認する2つのタイプA
とB
が等しいことを確認します。次に、次のコンパイルを確認します。
Equal[A, B]
Equal[T1 >: T2 <: T2, T2]
(apocolispから取得)implicitly[A =:= B]
または、タイプを値に変換し(上記を参照)、値の実行時チェックを行うこともできます。たとえばassert(toInt(a) == toInt(b))
、a
はタイプでA
あり、b
はタイプB
です。
追加のリソース
利用可能なコンストラクトの完全なセットは、Scalaリファレンスマニュアル(pdf)のタイプセクションにあります。
Adriaan Moorsは、タイプコンストラクターに関するいくつかの学術論文と、scalaの例を含む関連トピックを持っています。
Apocalispは、Scalaでの型レベルのプログラミングの多くの例が含まれているブログです。
ScalaZは非常にアクティブなプロジェクトであり、さまざまなタイプレベルのプログラミング機能を使用してScala APIを拡張する機能を提供しています。これは非常に興味深いプロジェクトで、大きな支持を得ています。
MetaScalaはScalaのタイプレベルライブラリで、自然数、ブール値、単位、HListなどのメタタイプを含みます。これはJesper Nordenberg(彼のブログ)によるプロジェクトです。
Michid(ブログ)は、(他の回答から)Scalaではタイプレベルのプログラミングのいくつかの素晴らしい例があります:
Debasish Ghosh(ブログ)にもいくつかの関連する投稿があります:
(私はこの主題についていくつかの調査を行っており、これは私が学んだことです。私はまだそれを知らないので、この回答の不正確さを指摘してください。)
ここにある他のリンクに加えて、Scalaのタイプレベルのメタプログラミングに関する私のブログ投稿もあります。
Twitterで提案されているように:Shapeless: Miles SabinによるScalaでのジェネリック/ポリタイププログラミングの探索。
Scalazにはソースコード、wiki、例があります。