依存型付き Haskell、今?
Haskell は、ある程度、依存型言語です。のおかげでより賢明に型付けされた型レベル データの概念があり、型レベル データにランタイム表現を与えるDataKinds
いくつかの手段 ( ) があります。GADTs
したがって、実行時のものの値は効率的に typesに表示されます。これは、言語が依存的に型付けされることを意味します。
単純なデータ型は種類レベルに昇格されるため、それらに含まれる値を型で使用できます。したがって、典型的な例
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
。対照的に、与えられた( toの)の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
is not Nat
: 実行時の値に何らかの依存関係がありますが、最初に考えた型にはありません。完全に依存型付けされた言語で、依存型がこれほど複雑になることはありません。
一方、Nat
昇進はできますが、Vec
できません。インデックス付きの型でインデックスを作成することはできません。完全に依存型付けされた言語にはそのような制限はありません。また、依存型付けの誇示としての私のキャリアの中で、1 層のインデックス作成を行った人々に教えるために、2 層のインデックス作成の例を講演に含めることを学びました。トランプの家のように折りたたむことを期待しないのは難しいですが、可能性があります。どうしたの?平等。GADT は、コンストラクターに特定の戻り値の型を与えるときに暗黙的に達成した制約を、明示的な等式の要求に変換することによって機能します。このような。
data Vec (n :: Nat) (x :: *)
= n ~ Z => VNil
| forall m. n ~ S m => VCons x (Vec m x)
2 つの方程式のそれぞれで、両側に kind があり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)
そして今、両辺が構文的に異なる (しかし、おそらく等しい) 種類を持つ等式制約を形成しas :: Vec n x
ます
VCons z zs :: Vec (S m) x
。GHCコアは現在、そのようなコンセプトを備えていません!
他に何が欠けていますか?まあ、Haskell のほとんどは型レベルから欠落しています。プロモートできる用語の言語には、実際には変数と非 GADT コンストラクターしかありません。それらがあれば、type family
機械によって型レベルのプログラムを書くことができます: それらのいくつかは、用語レベルで書くことを検討する関数に非常に似ているかもしれません (例えば、Nat
加算を装備することで、 に追加する適切な型を与えることができますVec
) 。 、しかしそれはただの偶然です!
もう 1 つ欠けているのは、実際には、型を値でインデックス化する新しい機能を利用するライブラリです。この素晴らしい新しい世界で何をし、何Functor
になるのでしょうか? Monad
考え中ですが、まだまだやることはたくさんあります。
タイプレベルのプログラムの実行
ほとんどの依存型プログラミング言語と同様に、Haskell には2 つの
操作上のセマンティクスがあります。ランタイム システムがプログラムを実行する方法 (閉じた式のみ、型消去後、高度に最適化) と、タイプチェッカーがプログラムを実行する方法 (型ファミリ、「型クラス プロローグ」、開いた式) があります。Haskell の場合、実行されるプログラムが異なる言語であるため、通常、この 2 つを混同することはありません。依存型付けされた言語には、同じプログラム言語に対して個別のランタイム モデルと静的実行モデルがありますが、心配する必要はありません。メカニズムはあなたに与えます; これは、少なくとも Edwin Brady のコンパイラが行っていることです (ただし、Edwin は不必要に重複した値や、型や証明を消去します)。フェーズの区別は、もはや構文カテゴリの区別ではないかもしれませんが、
健在です。
依存型付けされた言語は、全体として、タイプチェッカーが長い待ち時間よりも悪いことを恐れずにプログラムを実行できるようにします。Haskell がより従属的に型付けされるにつれて、その静的実行モデルはどうあるべきかという問題に直面します。1 つのアプローチは、静的な実行をすべての関数に制限することです。これにより、同じ自由な実行が可能になりますが、(少なくとも型レベルのコードでは) データと codata を区別する必要が生じる可能性があります。終了または生産性を強制します。しかし、それが唯一のアプローチではありません。計算だけで得られる式が少なくなるという代償を払って、プログラムを実行するのをためらう、はるかに弱い実行モデルを自由に選択できます。実際、それが GHC が実際に行っていることです。GHC コアの型付け規則では、実行について言及されていません
ただし、方程式の証拠をチェックするためだけです。コアに変換するとき、GHC の制約ソルバーは型レベルのプログラムを実行しようとし、与えられた式がその正規形に等しいという証拠の小さな銀色の跡を生成します。この証拠生成方法は少し予測不可能であり、必然的に不完全です。たとえば、恐ろしく見える再帰を恥ずかしがり屋で戦いますが、それはおそらく賢明です。心配する必要のないことの 1 つは、タイプチェッカーでの計算の実行です。タイプチェッカーは、ランタイム システムと同じ意味IO
を与える必要がないことを覚えておいて
ください。launchMissiles
ハインドリー・ミルナー文化
Hindley-Milner 型システムは、4 つの異なる区別の真に驚くべき偶然の一致を実現しますが、残念なことに、多くの人々が区別の違いを認識できず、偶然が必然であると想定するという文化的な副作用があります! 私は何について話しているのですか?
- 用語とタイプ
- 明示的に書かれたものと暗黙的に書かれたもの
- 実行時の存在vs実行前の消去
- 非依存の抽象化と依存の定量化
私たちは、用語を書き、型を推論して消去することに慣れています。私たちは、対応する型の抽象化と適用が黙って静的に行われる型変数の量化に慣れています。
これらの区別が一致しなくなる前に、バニラの Hindley-Milner から大きく離れる必要はありません。それは悪いことではありません。手始めに、いくつかの場所に書く気があれば、もっと興味深い型を持つことができます。一方、オーバーロードされた関数を使用する場合、型クラス辞書を作成する必要はありませんが、それらの辞書は実行時に必ず存在 (またはインライン化) されます。依存型付け言語では、実行時に型だけでなくそれ以上のものを消去することを期待していますが、(型クラスと同様に) 一部の暗黙的に推論された値は消去されません。たとえば、vReplicate
の数値引数は、多くの場合、目的のベクトルの型から推測できますが、実行時にそれを知る必要があります。
これらの偶然が成り立たなくなったため、どの言語設計の選択を検討する必要がありますか? たとえば、Haskell がforall x. t
量指定子を明示的にインスタンス化する方法を提供していないというのは正しいでしょうか? x
タイプチェッカーが unifiyingで推測できない場合、t
何x
が必要かを判断する方法は他にありません。
もっと広く言えば、「型推論」をモノリシックな概念として扱うことはできません。まず、「一般化」の側面 (Milner の「let」規則) を分割する必要があります。これは、「特殊化」の側面 (Milner の「var " ルール) は、制約ソルバーと同じくらい効果的です。トップレベルの型は推測が難しくなると予想できますが、内部の型情報は依然としてかなり簡単に伝播されます。
Haskell の次のステップ
type と kind のレベルが非常に似通ったものになっているのを私たちは見ています (そして、それらは GHC の内部表現をすでに共有しています)。それらをマージすることもできます。できれば面白いと思います:かなり前に底を許したときに論理的な* :: *
健全性を失い
ましたが、型の
健全性は通常、より弱い要件です。確認する必要があります。異なるタイプ、種類などのレベルが必要な場合は、少なくともタイプ レベル以上のすべてを常に昇格できるようにすることができます。種類レベルでポリモーフィズムを再発明するのではなく、型に対してすでに持っているポリモーフィズムを再利用するだけでよいでしょう。
との種類が構文的に同一で
はない (ただし、等しいことが証明できる)異種方程式を許可することにより、現在の制約システムを単純化して一般化する必要があります。これは、依存関係への対処をはるかに容易にする古い手法です (私の論文では、前世紀)。GADT で式に対する制約を表現できるため、昇格できるものに対する制限を緩和できます。a ~ b
a
b
従属関数型 を導入することで、シングルトン構築の必要性を排除する必要がありpi x :: s -> t
ます。そのような型を持つ関数は、型言語とターム言語 (変数、コンストラクターなど)の共通部分に存在する型の任意の式に明示的に適用できます。対応するラムダとアプリケーションは実行時に消去されないため、次のように記述できます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 のステップの結果として、人々が何をしているかを見回して、現在の世代の言語を前進させることによっても多くの利益が得られることを確認する必要があります。