依存型付けされたHaskell、今?
Haskellは、ある程度、依存型付け言語です。型レベルのデータの概念があり、今DataKinds
ではのおかげでより賢く型付けされていGADTs
ます。また、型レベルのデータにランタイム表現を与えるためのいくつかの手段()があります。したがって、ランタイムのものの値は効果的に型で表示されます。これは、言語が依存して型付けされることを意味します。
単純なデータ型は、種類レベルに昇格されるため、それらに含まれる値を型で使用できます。したがって、典型的な例
data Nat = Z | S Nat
data Vec :: Nat -> * -> * where
VNil :: Vec Z x
VCons :: x -> Vec n x -> Vec (S n) x
可能になり、それにより、次のような定義
vApply :: Vec n (s -> t) -> Vec n s -> Vec n t
vApply VNil VNil = VNil
vApply (VCons f fs) (VCons s ss) = VCons (f s) (vApply fs ss)
いいですね。その長さn
はその関数の純粋に静的なものであり、入力と出力のベクトルが同じ長さであることを保証することに
注意してくださいvApply
。これとは対照的に、それは作る機能を実装するために(すなわち、不可能)非常にトリッキーだn
与えられたのコピーをx
(されるであろうpure
にvApply
さん<*>
)
vReplicate :: x -> Vec n x
実行時に作成するコピーの数を把握することが重要であるためです。シングルトンを入力します。
data Natty :: Nat -> * where
Zy :: Natty Z
Sy :: Natty n -> Natty (S n)
昇格可能なタイプの場合は、その値の実行時の複製が存在する、昇格されたタイプにインデックスが付けられたシングルトンファミリを構築できます。Natty n
type-levelのランタイムコピーのタイプですn
:: Nat
。私たちは今書くことができます
vReplicate :: Natty n -> x -> Vec n x
vReplicate Zy x = VNil
vReplicate (Sy n) x = VCons x (vReplicate n x)
したがって、ランタイムレベルの値に型レベルの値が連結されています。ランタイムコピーを検査すると、タイプレベルの値の静的な知識が絞り込まれます。用語とタイプは分離されていますが、シングルトン構造を一種のエポキシ樹脂として使用することにより、依存型の方法で作業でき、フェーズ間に結合を作成します。これは、型で任意の実行時式を許可するまでには長い道のりですが、それは何もではありません。
厄介なのは何ですか?何が欠けていますか?
このテクノロジーに少し圧力をかけて、何がぐらつくのかを見てみましょう。シングルトンはもう少し暗黙的に管理可能である必要があるという考えを得るかもしれません
class Nattily (n :: Nat) where
natty :: Natty n
instance Nattily Z where
natty = Zy
instance Nattily n => Nattily (S n) where
natty = Sy natty
たとえば、
instance Nattily n => Applicative (Vec n) where
pure = vReplicate natty
(<*>) = vApply
これは機能しますが、これは、元のNat
タイプが3つのコピー(種類、シングルトンファミリー、シングルトンクラス)を生成したことを意味します。明示的なNatty n
値とNattily n
辞書を交換するためのかなり不格好なプロセスがあります。さらに、そうでNatty
はありませんNat
。実行時の値に何らかの依存関係がありますが、最初に考えた型には依存していません。完全に依存型付けされた言語が依存型をこれほど複雑にすることはありません!
一方、Nat
昇進はVec
できますが、できません。インデックス付きの型でインデックスを付けることはできません。依存型言語に完全に依存することはそのような制限を課しません。そして、依存型ショーオフとしての私のキャリアの中で、私は話に2層インデックスの例を含めることを学びました。カードの家のように折りたたまれるのを期待するのは難しいですが可能です。どうしたの?平等。GADTは、コンストラクターに特定の戻り値の型を明示的な等式要求に与えるときに暗黙的に達成する制約を変換することによって機能します。このような。
data Vec (n :: Nat) (x :: *)
= n ~ Z => VNil
| forall m. n ~ S m => VCons x (Vec m x)
2つの方程式のそれぞれで、両側に種類がありNat
ます。
次に、ベクトルにインデックスを付けたものに対して同じ翻訳を試みます。
data InVec :: x -> Vec n x -> * where
Here :: InVec z (VCons z zs)
After :: InVec z ys -> InVec z (VCons y ys)
なる
data InVec (a :: x) (as :: Vec n x)
= forall m z (zs :: Vec x m). (n ~ S m, as ~ VCons z zs) => Here
| forall m y z (ys :: Vec x m). (n ~ S m, as ~ VCons y ys) => After (InVec z ys)
そして今、私たちは、2つの側が構文的に異なる(ただし、等しい可能性がある)種類の間でas :: Vec n x
、
VCons z zs :: Vec (S m) x
どこに方程式の制約を形成します。現在、GHCコアはそのようなコンセプトには対応していません。
他に何が欠けていますか?まあ、Haskellのほとんどは型レベルにありません。宣伝できる用語の言語には、実際には変数と非GADTコンストラクターしかありません。それらを取得したら、type family
機械でタイプレベルのプログラムを記述できます。それらのいくつかは、用語レベルで記述することを検討する関数に非常に似ている場合があります(たとえば、Nat
追加機能を備えているため、追加するのに適したタイプを指定できますVec
)。 、しかしそれは単なる偶然です!
もう1つ欠けているのは、実際には、値によって型にインデックスを付ける新しい機能を利用するライブラリです。この勇敢な新しい世界で何が起こりFunctor
、どうMonad
なるのでしょうか?考えていますが、まだやることがたくさんあります。
タイプレベルのプログラムの実行
Haskellは、ほとんどの依存型付きプログラミング言語と同様に、2つの
操作上のセマンティクスを持っています。ランタイムシステムがプログラムを実行する方法(閉じた式のみ、型の消去後、高度に最適化)と、型チェッカーがプログラムを実行する方法(型ファミリー、「型クラスProlog」、開いた式)があります。Haskellでは、実行されるプログラムが異なる言語であるため、通常は2つを混同しないでください。依存型付けされた言語には、同じプログラム言語の実行時モデルと静的実行モデルが別々にありますが、心配する必要はありません。実行時モデルでは、型消去と実際の証明消去を実行できます。それがCoqの抽出です。メカニズムはあなたに与える; これは少なくとも、Edwin Bradyのコンパイラーが行うことです(ただし、Edwinは不必要に重複した値、および型と証明を消去します)。フェーズの区別は
、もはや構文カテゴリの区別ではないかもしれませんが、それは健在です。
依存して型付けされた言語は、全体として、タイプチェッカーが長い待機よりも悪いことを恐れずにプログラムを実行できるようにします。Haskellがより依存的に型付けされるようになると、静的実行モデルがどうあるべきかという疑問に直面します。1つのアプローチとして、静的実行を関数全体に制限することもできます。これにより、同じ自由に実行できますが、データとコデータを(少なくとも型レベルのコードでは)区別しなければならないため、終了または生産性を強化する。しかし、それが唯一のアプローチではありません。プログラムを実行することに消極的なはるかに弱い実行モデルを自由に選択できますが、計算だけで得られる方程式が少なくなります。そして実際には、それがGHCが実際に行っていることです。GHCコアのタイピングルールは実行について言及していません
プログラム、ただし方程式の証拠をチェックするためだけ。コアに変換すると、GHCの制約ソルバーはタイプレベルのプログラムを実行しようとし、指定された式がその正規形に等しいという証拠の小さな銀色の痕跡を生成します。この証拠生成方法は、少し予測不可能であり、必然的に不完全です。たとえば、恐ろしい再帰の恥ずかしがり屋と戦いますが、それはおそらく賢明です。心配する必要のないことの1つIO
は、タイプチェッカーでの計算の実行launchMissiles
です。タイプチェッカーは、ランタイムシステムが行うのと同じ意味を与える必要がないことに注意し
てください。
ヒンドリー・ミルナー文化
Hindley-Milner型システムは、4つの異なる区別の本当に驚くべき一致を実現しますが、不幸な文化的副作用により、多くの人々は区別を区別できず、一致が避けられないと想定できません。私は何を話しているのですか?
- 用語とタイプ
- 明示的に書かれたものと暗黙的に書かれたもの
- 実行時に存在対実行時前に消去
- 非依存抽象化と依存定量化
私たちは用語を書き、タイプを推測されたままにして、それから消去することに慣れています。私たちは、対応する型の抽象化と型変数の定量化に慣れており、アプリケーションはサイレントで静的に発生します。
これらの区別が外れる前に、バニラHindley-Milnerからあまり遠くに向きを変える必要はありません。それは悪いことではありません。まず、いくつかの場所でそれらを記述したい場合は、より興味深いタイプを使用できます。一方、オーバーロードされた関数を使用する場合、型クラスの辞書を記述する必要はありませんが、これらの辞書は実行時に確実に存在(またはインライン化)されます。依存型付き言語では、実行時に型だけでなく、(型クラスと同様に)暗黙的に推論された値の一部が消去されないことを期待しています。たとえば、vReplicate
の数値引数は、目的のベクトルのタイプから推測できることがよくありますが、実行時にそれを知る必要があります。
これらの偶然はもはや成立しないため、どの言語設計の選択を検討する必要がありますか?たとえば、Haskellがforall x. t
数量詞を明示的にインスタンス化する方法を提供しないことは正しいですか?x
タイプチェッカーがunifiying t
で推測できない場合、他に何x
があるべきかを言う方法はありません。
より広義には、「型推論」を、すべてを持っているか、まったく持っていないモノリシックな概念として扱うことはできません。まず、「一般化」の側面(ミルナーの「レット」ルール)を分離する必要があります。これは、存在するタイプの制限に大きく依存して、「特殊化」の側面(ミルナーの「var」 "ルール)これは、制約ソルバーと同じくらい効果的です。トップレベルの型は推測するのが難しくなると予想できますが、その内部の型情報はかなり簡単に伝播されます。
Haskellの次のステップ
種類と種類のレベルが非常によく似ています(そして、それらはすでにGHCの内部表現を共有しています)。それらをマージすることもできます。* :: *
できれば取るのは楽しいでしょう。ボトムを許可したときに論理的健全性をずっと以前に失って
いましたが、タイプ
健全性は通常、より弱い要件です。確認する必要があります。明確なタイプ、種類などのレベルが必要な場合は、少なくともタイプレベル以上のすべてを常に昇格できるようにすることができます。種類レベルでポリモーフィズムを再発明するのではなく、タイプですでに使用しているポリモーフィズムを再利用することは素晴らしいことです。
との種類が構文的に同一でない異種の方程式を許可することにより、現在の制約システムを単純化および一般化する必要a ~ b
がa
あり
b
ます(ただし、同等であることが証明できます)。それは依存関係をはるかに扱いやすくする古い技法(私の論文では前世紀)です。GADTで式の制約を表現できるため、昇格できるものの制限を緩和できます。
依存関数タイプを導入することにより、シングルトン構築の必要性を排除する必要がありpi x :: s -> t
ます。そのようなAタイプを持つ関数が適用され得る明示的タイプのいずれかの式にs
に住んでいる交差点(詳細は後に来るようにと、そう、変数、コンストラクタ)タイプと用語の言語。対応するラムダとアプリケーションは実行時に消去されないため、次のように記述できます。
vReplicate :: pi n :: Nat -> x -> Vec n x
vReplicate Z x = VNil
vReplicate (S n) x = VCons x (vReplicate n x)
交換なしNat
でNatty
。のドメインはpi
任意の昇格可能なタイプにすることができるため、GADTを昇格できる場合は、従属数量詞シーケンス(またはde Briuijnがそれらを呼び出した「望遠鏡」)を書き込むことができます。
pi n :: Nat -> pi xs :: Vec n x -> ...
必要な長さに。
これらのステップの要点は、弱いツールや不格好なエンコーディングを使用する代わりに、より一般的なツールを直接操作することで複雑さを排除することです。現在の部分的な賛同により、Haskellのある種の依存型の利点は、必要以上に高くなります。
あまりにもハード?
依存型は多くの人を緊張させます。彼らは私を緊張させますが、私は緊張するのが好きです、または少なくとも私はとにかく緊張しないのは難しいと思います。しかし、それはトピックの周りにかなりの無知の霧があることを助けません。その一部は、私たち全員がまだ学ぶべきことがたくさんあるという事実によるものです。しかし、それほど過激でないアプローチの支持者は、常に事実が完全に彼らにあることを確認せずに、依存型の恐れを引き起こすことが知られています。名前は付けません。これらの「決定不可能な型チェック」、「不完全なチューリング」、「フェーズの区別なし」、「型の消去なし」、「どこでも証明」などの神話は、たとえごみでも残っています。
依存して型付けされたプログラムが常に正しいことが証明されなければならないというのは、確かにそうではありません。プログラムの基本的な衛生状態を改善し、完全な仕様に至るまで行くことなく、型に追加の不変条件を適用することができます。この方向の小さなステップは、追加の証明義務がほとんどまたはまったくない、非常に強力な保証をもたらすことがよくあります。依存して型付けされたプログラムが必然的に証明でいっぱいになることは真実ではありません。実際、私は通常、コードに証明の存在を見つけて、自分の定義に疑問を投げかけます。
なぜなら、アーティキュラシーの増加と同様に、フェアなだけでなくファウルな新しいことを自由に言うことができるからです。たとえば、二分探索木を定義するための不必要な方法はたくさんありますが、それは良い方法がないという意味ではありません。たとえそれがエゴにそれを認めさせるようにへこませたとしても、悪い経験は改善することができないと思い込まないことが重要です。従属定義の設計は学習を必要とする新しいスキルであり、Haskellプログラマーであっても自動的にエキスパートになるわけではありません!そして、いくつかのプログラムが不正であったとしても、なぜ他の人が公正である自由を否定するのでしょうか?
なぜまだHaskellに悩むのですか?
依存型が本当に好きですが、私のハッキングプロジェクトのほとんどはまだHaskellにあります。どうして?Haskellには型クラスがあります。Haskellには便利なライブラリがあります。Haskellには、効果を備えたプログラミングの(理想的とは言えませんが)実行可能な処理があります。Haskellには、強力なコンパイラーがあります。依存型付けされた言語は、コミュニティとインフラストラクチャの成長のかなり早い段階にありますが、メタプログラミングやデータ型のジェネリックスなどによって、可能なことの世代交代が現実のものとなります。しかし、依存型へのHaskellのステップの結果として人々が何をしているのかを見回して、現在の世代の言語を前進させることによって得られる多くの利点があることも確認する必要があります。