「タイプレベルのプログラミング」の意味がわかりません。また、Googleを使用して適切な説明を見つけることもできません。
誰かが型レベルのプログラミングを示す例を提供してもらえますか?パラダイムの説明および/または定義は有用であり、高く評価されます。
ほとんどの静的に型付けされた言語には、値レベルと型レベルの2つの「ドメイン」があります(一部の言語にはさらに多くのドメインがあります)。型レベルのプログラミングには、コンパイル時に評価される型システムでのエンコーディングロジック(多くの場合、関数の抽象化)が含まれます。いくつかの例は、テンプレートメタプログラミングまたはHaskellタイプファミリーです。
Haskellでこの例を実行するには、いくつかの言語拡張が必要ですが、今のところそれらを無視して、型族を関数であるが型レベルの数値(Nat
)であると見なします。
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
import GHC.TypeLits
import Data.Proxy
-- value-level
odd :: Integer -> Bool
odd 0 = False
odd 1 = True
odd n = odd (n-2)
-- type-level
type family Odd (n :: Nat) :: Bool where
Odd 0 = False
Odd 1 = True
Odd n = Odd (n - 2)
test1 = Proxy :: Proxy (Odd 10)
test2 = Proxy :: Proxy (Odd 11)
ここでは、自然数valueが奇数かどうかをテストする代わりに、自然数typeが奇数かどうかをテストし、タイプレベルのブール値に減らしています。コンパイル時。このプログラムを評価すると、test1
およびtest2
のタイプはコンパイル時に次のように計算されます。
λ: :type test1
test1 :: Proxy 'False
λ: :type test2
test2 :: Proxy 'True
これが型レベルのプログラミングの本質です。言語によっては、さまざまな用途を持つ型レベルで複雑なロジックをエンコードできる場合があります。たとえば、値レベルで特定の動作を制限したり、リソースのファイナライズを管理したり、データ構造に関する詳細情報を保存したりします。
42 :: Int
や'a' :: Char
などの値を操作する「値レベル」のプログラミングについてはすでにご存知でしょう。 Haskell、Scala、その他多くの言語では、型レベルのプログラミングにより、Int :: *
やChar :: *
などの型を操作できます。ここで、*
は種類です。 具象型(Maybe a
または[a]
は具象型ですが、種類が[]
のMaybe
または* -> *
ではありません)。
この関数を検討してください
foo :: Char -> Int
foo x = fromEnum x
ここで、foo
はtypeChar
のvalueを取り、の新しい値を返します。 Int
のEnum
インスタンスを使用してChar
と入力します。この関数は値を操作します。
次に、foo
を、TypeFamilies
言語拡張で有効にしたこのタイプファミリーと比較します。
type family Foo (x :: *)
type instance Foo Char = Int
ここで、Foo
はtypeofkind*
を取り、単純なマッピング*
を使用した新しいタイプのChar -> Int
。これは、型を操作する型レベルの関数です。
これは非常に単純な例であり、これがどのように役立つのか疑問に思われるかもしれません。より強力な言語ツールを使用して、コードの正しさの証明を型レベルでエンコードし始めることができます(これについて詳しくは、 Curry-Howard 対応を参照してください)。
実用的な例は、タイプレベルのプログラミングを使用して、ツリーの不変条件が保持されることを静的に保証する赤黒木です。
赤黒木には、次の単純なプロパティがあります。
非常に強力な型レベルのプログラミングの組み合わせであるDataKinds
とGADTs
を使用します。
{-# LANGUAGE DataKinds, GADTS, KindSignatures #-}
import GHC.TypeLits
まず、色を表すいくつかのタイプ。
data Colour = Red | Black -- promoted to types via DataKinds
これは、Colour
とRed
の2つのタイプが存在する新しい種類Black
を定義します。これらのタイプにはvalues(ボトムスを無視)はありませんが、とにかくそれらは必要ないことに注意してください。
赤黒木ノードは、次のGADT
で表されます。
-- 'c' is the Colour of the node, either Red or Black
-- 'n' is the number of black child nodes, a type level Natural number
-- 'a' is the type of the values this node contains
data Node (c :: Colour) (n :: Nat) a where
-- all leaves are black
Leaf :: Node Black 1 a
-- black nodes can have children of either colour
B :: Node l n a -> a -> Node r n a -> Node Black (n + 1) a
-- red nodes can only have black children
R :: Node Black n a -> a -> Node Black n a -> Node Red n a
GADT
を使用すると、Colour
およびR
コンストラクターのB
を型で直接表現できます。
木の根はこんな感じ
data RedBlackTree a where
RBTree :: Node Black n a -> RedBlackTree a
現在、上記の4つのプロパティのいずれかに違反する適切に型指定されたRedBlackTree
を作成することは不可能です。
Colour
に存在するタイプは2つだけです。RedBlackTree
の定義から、ルートは黒です。Leaf
コンストラクターの定義から、すべての葉は黒です。R
コンストラクターの定義から、その子は両方ともBlack
ノードである必要があります。同様に、各サブツリーの黒い子ノードの数は同じです(同じn
が左右のサブツリーのタイプで使用されます)これらの条件はすべて、コンパイル時にGHCによってチェックされます。つまり、赤黒木に関する仮定を無効にする不正なコードからランタイム例外が発生することはありません。重要なのは、これらの追加の利点に関連するランタイムコストがなく、すべての作業がコンパイル時に行われることです。
他の答えはとてもいいですが、私は一つの点を強調したいと思います。 termsのプログラミング言語理論は、ラムダ計算に強く基づいています。 「純粋な」LISPは、(多かれ少なかれ)糖度の高い型なしラムダ計算に対応します。プログラムの意味は、プログラムの実行時にラムダ計算の項がどのように削減されるかを示す評価ルールによって定義されます。
型付き言語では、用語に型を割り当てます。すべての評価ルールには、評価によってタイプがどのように保持されるかを示す対応するタイプルールがあります。型システムに応じて、型が互いにどのように関連するかを定義する他のルールもあります。十分に興味深い型システムを取得すると、typesとそのルールシステムもラムダ計算の変形に対応することがわかります。
現在、ラムダ計算をプログラミング言語と考えるのが一般的ですが、元々は論理システムとして設計されていました。これが、プログラミング言語の用語の種類について推論するのに役立つ理由です。しかし、ラムダ計算のプログラミング言語の側面により、型チェッカーによって評価されるプログラムを書くことができます。
「型レベルのプログラミング」が「用語レベルのプログラミング」と実質的に異なるものではないことがわかるといいのですが、型システムに十分に強力な言語を使用することは今ではあまり一般的ではありません。その中にプログラムを書く理由。