@KarlBielefeldtの答えを拡張するために、Haskellでベクター(静的に既知の要素数を持つリスト)を実装する方法の完全な例を示します。帽子を握って...
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE TypeFamilies #-}
import Prelude hiding (foldr, zipWith)
import qualified Prelude
import Data.Type.Equality
import Data.Foldable
import Data.Traversable
LANGUAGE
ディレクティブの長いリストからわかるように、これは最近のバージョンのGHCでのみ機能します。
型システム内で長さを表現する方法が必要です。定義により、自然数はゼロ(Z
)であるか、他の自然数の後継です(S n
)。したがって、たとえば、3という数字が書き込まれS (S (S Z))
ます。
data Nat = Z | S Nat
DataKinds拡張、このdata
宣言は紹介一種と呼ばれるNat
と、2 種類のコンストラクタが呼ばれるとS
し、Z
他の言葉で我々が持っている- タイプレベルの自然数を。タイプS
にZ
はメンバー値がないことに注意してください*
。種類のタイプにのみ値が存在します。
次に、既知の長さのベクトルを表すGADTを紹介します。種類の署名に注意してください。種類の種類(a またはtype)が長さを表すVec
必要があります。Nat
Z
S
data Vec :: Nat -> * -> * where
VNil :: Vec Z a
VCons :: a -> Vec n a -> Vec (S n) a
deriving instance (Show a) => Show (Vec n a)
deriving instance Functor (Vec n)
deriving instance Foldable (Vec n)
deriving instance Traversable (Vec n)
ベクトルの定義はリンクリストの定義に似ていますが、その長さに関する追加の型レベルの情報がいくつかあります。ベクトルはVNil
、長さがZ
(ero)の場合、またはVCons
別のベクトルに項目を追加するセルの場合、その長さは他のベクトルよりも1つ長くなりS n
ます()。typeのコンストラクター引数がないことに注意してくださいn
。コンパイル時に長さを追跡するために使用され、コンパイラがマシンコードを生成する前に消去されます。
その長さの静的な知識を運ぶベクトル型を定義しました。いくつかVec
のsのタイプを照会して、それらがどのように機能するかを感じてみましょう。
ghci> :t (VCons 'a' (VCons 'b' VNil))
(VCons 'a' (VCons 'b' VNil)) :: Vec ('S ('S 'Z)) Char -- (S (S Z)) means 2
ghci> :t (VCons 13 (VCons 11 (VCons 3 VNil)))
(VCons 13 (VCons 11 (VCons 3 VNil))) :: Num a => Vec ('S ('S ('S 'Z))) a -- (S (S (S Z))) means 3
内積は、リストの場合と同じように進みます。
-- note that the two Vec arguments are declared to have the same length
vap :: Vec n (a -> b) -> Vec n a -> Vec n b
vap VNil VNil = VNil
vap (VCons f fs) (VCons x xs) = VCons (f x) (vap fs xs)
zipWith :: (a -> b -> c) -> Vec n a -> Vec n b -> Vec n c
zipWith f xs ys = fmap f xs `vap` ys
dot :: Num a => Vec n a -> Vec n a -> a
dot xs ys = foldr (+) 0 $ zipWith (*) xs ys
vap
、関数のベクトルを引数のベクトルに 'zippily'で適用する 'はVec
applicative <*>
です。面倒になるApplicative
ので、インスタンスに入れませんでした。また、コンパイラが生成したのインスタンスを使用していることに注意してください。foldr
Foldable
試してみましょう:
ghci> let v1 = VCons 2 (VCons 1 VNil)
ghci> let v2 = VCons 4 (VCons 5 VNil)
ghci> v1 `dot` v2
13
ghci> let v3 = VCons 8 (VCons 6 (VCons 1 VNil))
ghci> v1 `dot` v3
<interactive>:20:10:
Couldn't match type ‘'S 'Z’ with ‘'Z’
Expected type: Vec ('S ('S 'Z)) a
Actual type: Vec ('S ('S ('S 'Z))) a
In the second argument of ‘dot’, namely ‘v3’
In the expression: v1 `dot` v3
すばらしいです!dot
長さが一致しないベクターを試みると、コンパイル時エラーが発生します。
ベクトルを連結する関数の試みは次のとおりです。
-- This won't compile because the type checker can't deduce the length of the returned vector
-- VNil +++ ys = ys
-- (VCons x xs) +++ ys = VCons x (concat xs ys)
出力ベクトルの長さは、2つの入力ベクトルの長さの合計になります。型チェッカーにNat
s を加算する方法を教える必要があります。このために、型レベルの関数を使用します:
type family (n :: Nat) :+: (m :: Nat) :: Nat where
Z :+: m = m
(S n) :+: m = S (n :+: m)
このtype family
宣言は、呼び出される型に関する関数を導入します。:+:
つまり、型チェッカーが2つの自然数の合計を計算するためのレシピです。再帰的に定義されています-左のオペランドがZ
ero より大きい場合は、出力に1を追加し、再帰呼び出しで1減らします。(2を掛ける型関数を書くのは良い練習Nat
です。)これで+++
コンパイルを行うことができます:
infixr 5 +++
(+++) :: Vec n a -> Vec m a -> Vec (n :+: m) a
VNil +++ ys = ys
(VCons x xs) +++ ys = VCons x (concat xs ys)
使用方法は次のとおりです。
ghci> VCons 1 (VCons 2 VNil) +++ VCons 3 (VCons 4 VNil)
VCons 1 (VCons 2 (VCons 3 (VCons 4 VNil)))
これまでのところとても簡単です。連結の反対を行い、ベクトルを2つに分割する場合はどうでしょうか。出力ベクトルの長さは、引数のランタイム値に依存します。次のようなものを書きたいと思います。
-- this won't work because there aren't any values of type `S` and `Z`
-- split :: (n :: Nat) -> Vec (n :+: m) a -> (Vec n a, Vec m a)
残念ながら、Haskellはそれを許可しません。引数の値を戻り値の型(これは一般に依存関数またはpi型と呼ばれます)に表示するには、「完全なスペクトル」の依存型が必要になりますが、型コンストラクタの昇格のみが提供されます。別の言い方をすれば、型コンストラクタは値レベルでは表示されません。特定の。*の実行時表現のシングルトン値を決定する必要があります。n
DataKinds
S
Z
Nat
data Natty (n :: Nat) where
Zy :: Natty Z -- pronounced 'zed-y'
Sy :: Natty n -> Natty (S n) -- pronounced 'ess-y'
deriving instance Show (Natty n)
n
(kindを持つNat
)与えられた型に対して、typeの項が正確に1つありNatty n
ます。我々はのための実行時の証人としてシングルトン値を使用することができますn
:について学習するNatty
のを私たちに教えn
、その逆も。
split :: Natty n ->
Vec (n :+: m) a -> -- the input Vec has to be at least as long as the input Natty
(Vec n a, Vec m a)
split Zy xs = (Nil, xs)
split (Sy n) (Cons x xs) = let (ys, zs) = split n xs
in (Cons x ys, zs)
試してみましょう:
ghci> split (Sy (Sy Zy)) (VCons 1 (VCons 2 (VCons 3 VNil)))
(VCons 1 (VCons 2 VNil), VCons 3 VNil)
ghci> split (Sy (Sy Zy)) (VCons 3 VNil)
<interactive>:116:21:
Couldn't match type ‘'S ('Z :+: m)’ with ‘'Z’
Expected type: Vec ('S ('S 'Z) :+: m) a
Actual type: Vec ('S 'Z) a
Relevant bindings include
it :: (Vec ('S ('S 'Z)) a, Vec m a) (bound at <interactive>:116:1)
In the second argument of ‘split’, namely ‘(VCons 3 VNil)’
In the expression: split (Sy (Sy Zy)) (VCons 3 VNil)
最初の例では、位置2で3要素ベクトルを正常に分割しました。その後、ベクトルを終了位置を超えて分割しようとしたときに型エラーが発生しました。シングルトンは、Haskellの値に型を依存させるための標準的な手法です。
* singletons
ライブラリにはNatty
、お好みのシングルトン値を生成するためのTemplate Haskellヘルパーが含まれています。
最後の例。ベクトルの次元を静的に知らない場合はどうですか?たとえば、リスト形式の実行時データからベクトルを構築しようとしている場合はどうなりますか?入力リストの長さに依存するベクトルのタイプが必要です。別の言い方をすれば、出力ベクトルのタイプはフォールドの反復ごとに変化するため、ベクトルの構築には使用できません。ベクターの長さをコンパイラーから秘密にしておく必要があります。foldr VCons VNil
data AVec a = forall n. AVec (Natty n) (Vec n a)
deriving instance (Show a) => Show (AVec a)
fromList :: [a] -> AVec a
fromList = Prelude.foldr cons nil
where cons x (AVec n xs) = AVec (Sy n) (VCons x xs)
nil = AVec Zy VNil
AVec
ある実存タイプ:型変数は、n
の戻り値の型には表示されませんAVec
データコンストラクタ。我々シミュレートするためにそれを使用している依存のペアを:fromList
静的あなたのベクトルの長さを伝えることはできませんが、それはあなたが上のパターン照合することができます何かを返すことができます学ぶベクトルの長さを- Natty n
タプルの最初の要素に。コナーマクブライドは、関連する答え、「あなたはあることを見て、そうすることで別のことを学ぶ」と答えています。
これは、実存的に数量化された型の一般的な手法です。タイプがわからないデータでは実際には何もできないので(関数の記述を試してください)、data Something = forall a. Sth a
実在物はしばしばパターンマッチングテストを実行して元のタイプを回復できるGADTの証拠にバンドルされています。存在のその他の一般的なパターンには、タイプを処理するための関数のパッケージ化(data AWayToGetTo b = forall a. HeresHow a (a -> b)
)があります。これは、ファーストクラスモジュールを実行するための適切な方法ですdata AnOrd = forall a. Ord a => AnOrd a
。
ghci> fromList [1,2,3]
AVec (Sy (Sy (Sy Zy))) (VCons 1 (VCons 2 (VCons 3 Nil)))
依存ペアは、データの静的プロパティがコンパイル時に利用できない動的情報に依存する場合に便利です。ここだfilter
ベクターについて:
filter :: (a -> Bool) -> Vec n a -> AVec a
filter f = foldr (\x (AVec n xs) -> if f x
then AVec (Sy n) (VCons x xs)
else AVec n xs) (AVec Zy VNil)
dot
2 AVec
秒、我々はそれらの長さが等しいことをGHCに証明する必要があります。Data.Type.Equality
型引数が同じ場合にのみ構築できるGADTを定義します。
data (a :: k) :~: (b :: k) where
Refl :: a :~: a -- short for 'reflexivity'
パターンマッチをオンにするとRefl
、GHCはそれを認識しa ~ b
ます。このタイプでの作業を支援する関数もいくつかあります。gcastWith
同等のタイプ間で変換TestEquality
を行い、2つNatty
のsが等しいかどうかを判断するために使用します。
2つNatty
のsの等価性をテストするには、2つの数値が等しい場合、その後継者も等しいという事実を利用する必要があります(以上:~:
が一致S
)。
congSuc :: (n :~: m) -> (S n :~: S m)
congSuc Refl = Refl
Refl
左側のパターンマッチングはGHCにそれを知らせますn ~ m
。その知識があれば、それは簡単なことなS n ~ S m
ので、GHCを使えばRefl
すぐに新しいものを返すことができます。
これでTestEquality
、単純な再帰によってのインスタンスを作成できます。両方の数値がゼロの場合、それらは等しくなります。両方の数値に先行がある場合、先行が等しい場合、それらは等しくなります。(それらが等しくない場合は、単にを返しNothing
ます。)
instance TestEquality Natty where
-- testEquality :: Natty n -> Natty m -> Maybe (n :~: m)
testEquality Zy Zy = Just Refl
testEquality (Sy n) (Sy m) = fmap congSuc (testEquality n m) -- check whether the predecessors are equal, then make use of congruence
testEquality Zy _ = Nothing
testEquality _ Zy = Nothing
これで、断片を長さ不明のdot
ペアにまとめることができAVec
ます。
dot' :: Num a => AVec a -> AVec a -> Maybe a
dot' (AVec n u) (AVec m v) = fmap (\proof -> gcastWith proof (dot u v)) (testEquality n m)
まず、AVec
コンストラクターでパターンマッチングを行い、ベクターの長さのランタイム表現を引き出します。次に、testEquality
これらの長さが等しいかどうかを判断するために使用します。もしそうなら、我々は持っているでしょうJust Refl
。gcastWith
その平等証明を使用してdot u v
、暗黙のn ~ m
仮定を破棄することにより、適切に型付けされていることを確認します。
ghci> let v1 = fromList [1,2,3]
ghci> let v2 = fromList [4,5,6]
ghci> let v3 = fromList [7,8]
ghci> dot' v1 v2
Just 32
ghci> dot' v1 v3
Nothing -- they weren't the same length
その長さの静的な知識のないベクトルは基本的にリストであるため、リストバージョンのを効果的に再実装したことに注意してくださいdot :: Num a => [a] -> [a] -> Maybe a
。違いは、このバージョンはベクトルの観点から実装されているということですdot
。ここでのポイントは次のとおりです。型チェッカーは、あなたが呼び出すことができます前にdot
、あなたがテストしている必要があり、入力リストが使用して同じ長さを持っているかどうかtestEquality
。if
-statementsを間違った方法で取得する傾向がありますが、依存関係のあるタイプの設定ではありません!
ランタイムデータを処理する場合、システムのエッジで存在ラッパーを使用することは避けられませんが、入力検証を実行する場合、システム内のすべての場所で依存型を使用し、エッジで存在ラッパーを保持できます。
Nothing
あまり有益ではないので、失敗の場合にのタイプをさらに改良して、長さが等しくないという証拠を(差が0ではないという形で)dot'
返すことができます。これは、エラーメッセージを返すために使用される可能性のある標準のHaskellテクニックにかなり似ていますが、証明用語は文字列よりもはるかに計算上便利です!Either String a
このようにして、依存型のHaskellプログラミングで一般的ないくつかのテクニックのこのホイッスルストップツアーを終了します。Haskellでのこのようなタイプのプログラミングは本当にクールですが、同時に非常に厄介です。すべての依存データを、同じことを意味する多くの表現に分割する- Nat
タイプ、Nat
種類、Natty n
シングルトン-は、ボイラープレートを支援するコードジェネレーターの存在にもかかわらず、非常に面倒です。また、現在、タイプレベルに昇格できるものには制限があります。それは興味をそそる!考えは可能性を揺るがします-文献には、強く型付けされたprintf
データベースインターフェイス、UIレイアウトエンジンのHaskellの例があります...
さらに読みたい場合は、依存型付けされたHaskellに関する出版物とStack Overflowのようなサイトの両方で増加する文献があります。良好な出発点であるHasochismの紙 -紙をある程度詳細に痛みを伴う部品を議論、(とりわけ)この非常に例を通過します。シングルトン紙は、(例えば、シングルトン値の手法を示して)。一般的な依存型付けの詳細については、Agdaチュートリアルを開始するのに適した場所です。また、イドリスは「依存型を持つハスケル」になるように(大まかに)設計された開発中の言語です。Natty