haskellで「ディメンションを型にベイク」することは可能ですか?


20

ベクトルと行列を扱うライブラリを書きたいとします。ディメンションを型にベイクして、互換性のないディメンションの操作がコンパイル時にエラーを生成することは可能ですか?

たとえば、ドット積の署名を次のようにしたい

dotprod :: Num a, VecDim d => Vector a d -> Vector a d -> a

ここで、d型には単一の整数値(これらのベクターの次元を表す)が含まれます。

これは、各整数に対して個別の型を(手で)定義し、それらをで呼ばれる型クラスにグループ化することで実現できると思いますVecDim。そのような型を「生成」するメカニズムはありますか?

または、おそらく同じことを達成するためのより良い/簡単な方法はありますか?


3
はい、正しく覚えていれば、この基本的なレベルの依存型付けをHaskellで提供するライブラリがあります。良い答えを提供するのに十分な知識はありません。
Telastyn

周りを見ると、と思われるtensorライブラリは、この非常にエレガントな再帰的な使用して達成されるdata:定義を noaxiom.org/tensor-documentation#ordinals
mitchus

これはscalaであり、haskellではありませんが、依存型を使用して次元の不一致やベクトルの「型」の不一致を防ぐことに関するいくつかの関連概念があります。chrisstucchio.com/blog/2014/…–
デニス

回答:


32

@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他の言葉で我々が持っている- タイプレベルの自然数を。タイプSZはメンバー値がないことに注意してください*。種類のタイプにのみ値が存在します。

次に、既知の長さのベクトルを表すGADTを紹介します。種類の署名に注意してください。種類の種類(a またはtype)が長さを表すVec必要があります。NatZS

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'で適用する 'はVecapplicative <*>です。面倒になるApplicativeので、インスタンスに入れませんでした。また、コンパイラが生成したのインスタンスを使用していることに注意してください。foldrFoldable

試してみましょう:

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つの入力ベクトルの長さの合計になります。型チェッカーにNats を加算する方法を教える必要があります。このために、型レベルの関数を使用します

type family (n :: Nat) :+: (m :: Nat) :: Nat where
    Z :+: m = m
    (S n) :+: m = S (n :+: m)

このtype family宣言は、呼び出される型に関する関数を導入します。:+:つまり、型チェッカーが2つの自然数の合計を計算するためのレシピです。再帰的に定義されています-左のオペランドがZero より大きい場合は、出力に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型と呼ばれます)に表示するには、「完全なスペクトル」の依存型が必要になりますが、型コンストラクタの昇格のみが提供されます。別の言い方をすれば、型コンストラクタは値レベルでは表示されません。特定の。*の実行時表現のシングルトン値を決定する必要があります。nDataKindsSZNat

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) 

dot2 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 ReflgcastWithその平等証明を使用して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あなたがテストしている必要があり、入力リストが使用して同じ長さを持っているかどうかtestEqualityif-statementsを間違った方法で取得する傾向がありますが、依存関係のあるタイプの設定ではありません!

ランタイムデータを処理する場合、システムのエッジで存在ラッパーを使用することは避けられませんが、入力検証を実行する場合、システム内のすべての場所で依存型を使用し、エッジで存在ラッパーを保持できます。

Nothingあまり有益ではないので、失敗の場合にのタイプをさらに改良して、長さが等しくないという証拠を(差が0ではないという形で)dot'返すことができます。これは、エラーメッセージを返すために使用される可能性のある標準のHaskellテクニックにかなり似ていますが、証明用語は文字列よりもはるかに計算上便利です!Either String a


このようにして、依存型のHaskellプログラミングで一般的ないくつかのテクニックのこのホイッスルストップツアーを終了します。Haskellでのこのようなタイプのプログラミングは本当にクールですが、同時に非常に厄介です。すべての依存データを、同じことを意味する多くの表現に分割する- Natタイプ、Nat種類、Natty nシングルトン-は、ボイラープレートを支援するコードジェネレーターの存在にもかかわらず、非常に面倒です。また、現在、タイプレベルに昇格できるものには制限があります。それは興味をそそる!考えは可能性を揺るがします-文献には、強く型付けされたprintfデータベースインターフェイス、UIレイアウトエンジンのHaskellの例があります...

さらに読みたい場合は、依存型付けされたHaskellに関する出版物とStack Overflowのようなサイトの両方で増加する文献があります。良好な出発点であるHasochismの -紙をある程度詳細に痛みを伴う部品を議論、(とりわけ)この非常に例を通過します。シングルトン紙は、(例えば、シングルトン値の手法を示して)。一般的な依存型付けの詳細については、Agdaチュートリアルを開始するのに適した場所です。また、イドリスは「依存型を持つハスケル」になるように(大まかに)設計された開発中の言語です。Natty


@ベンジャミンFYI、最後にイドリスのリンクが壊れているようです。
エリックエイド

@ErikEidtおっと、指摘してくれてありがとう!更新します。
ベンジャミンホジソン

14

それは依存型と呼ばれます。名前がわかれば、望んでいたよりも多くの情報を見つけることができます。また、ネイティブに使用するIdrisと呼ばれる興味深いhaskellに似た言語もあります。作者は、YouTubeで見つけることができるトピックに関するいくつかの非常に良いプレゼンテーションを行っています。


それはまったく依存するタイピングではありません。依存型付けは実行時に型について話しますが、型に次元を組み込むことはコンパイル時に簡単に行えます。
DeadMG

4
これに対し、およそ依存タイピング会談@DeadMG コンパイル時にタイプ実行時には反射ではなく、依存タイピングです。私の答えからわかるように、一般的なディメンションでは、ディメンションに型を組み込むことは簡単ではありません。(定義することなどもできますnewtype Vec2 a = V2 (a,a)newtype Vec3 a = V3 (a,a,a)、それは質問の質問ではありません。)
ベンジャミンホジソン

さて、値は実行時にのみ表示されるため、停止時の問題を解決したい場合を除き、コンパイル時に値について実際に話すことはできません。私が言っているのは、C ++でさえ、次元をテンプレート化するだけでよく、うまく機能するということです。Haskellには同等のものがありませんか?
DeadMG

4
@DeadMG "フルスペクトル"依存型付き言語(Agdaなど)は、実際、型言語での任意の用語レベルの計算を許可します。あなたが指摘するように、これは停止問題を解決しようとする危険にあなたを置きます。ほとんどの依存型システムafaikは、チューリング完全ではないことにより、この問題をパントします。私はC ++の男ではありませんが、テンプレートを使用して依存型をシミュレートできることは驚くことではありません。テンプレートはあらゆる種類の創造的な方法で悪用される可能性があります。
ベンジャミンホジソン

4
@BenjaminHodgson pi型をシミュレートできないため、テンプレートで依存型を実行できません。「標準的な」依存型Pi (x : A). Bは、関数AB xどこから関数xの引数であるかを要求する必要があります。ここで、関数の戻り値の型は、引数として指定された式に依存しています。しかし、このすべてを消去することができ、それはコンパイル時にのみだ
ダニエルGratzer
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.