Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ロジックへの変換

ここでの重要な観察は、Rust のトレイトシステムが基本的に 一種の論理であり、標準的な論理的推論規則へ対応付けられる、 という点です。そのうえで、たとえば Prolog ソルバーが動作する方法と 非常によく似た形で、それらの推論規則に対する解を探すことができます。 ただし、Prolog の規則(Horn 節とも呼ばれます)をそのまま使うことは 完全にはできず、やや表現力の高い変種が必要になることがわかっています。

Rust のトレイトと論理

最初に得られる観察の 1 つは、Rust のトレイトシステムが 基本的に一種の論理であるということです。そのため、struct、trait、 impl の宣言を論理的推論規則へ対応付けることができます。大部分において、 これらは基本的に Horn 節ですが、Rust の表現力を完全に捉えるには、 特にジェネリックプログラミングをサポートするには、標準的な Horn 節より 少し先へ進む必要があることを見ていきます。

この対応付けがどのように機能するかを見るために、例から始めましょう。 次のようにトレイトといくつかの impl を宣言するとします。

#![allow(unused)]
fn main() {
trait Clone { }
impl Clone for usize { }
impl<T> Clone for Vec<T> where T: Clone { }
}

これらの宣言は、Prolog 風の記法で書かれたいくつかの Horn 節へ、 次のように対応付けることができます。

Clone(usize).
Clone(Vec<?T>) :- Clone(?T).

// `A :- B` という記法は「B が真なら A は真である」ことを意味します。
// 別の言い方をすれば、B は A を含意します。

Prolog の用語では、Clone(Foo)(ここで Foo は何らかの Rust の型です)は、型 FooClone を実装しているという考えを表す 述語である、と言えるでしょう。これらの規則はプログラム節です。 つまり、その述語を証明できる(すなわち、真とみなせる)条件を述べています。 そのため、最初の規則は単に「usize に対して Clone が実装されている」と 言っています。次の規則は「任意の型 ?T について、?T に対して clone が 実装されているなら、Vec<?T> に対して Clone が実装されている」と 言っています。したがって、たとえば Clone(Vec<Vec<usize>>) を証明したい場合、 規則を再帰的に適用することでそれを行います。

  • Clone(Vec<Vec<usize>>) が証明可能であるのは、次の場合です。
    • Clone(Vec<usize>) が証明可能である場合で、それは次の場合です。
      • Clone(usize) が証明可能である場合です。(これは証明可能なので、問題ありません。)

しかし今度は、Clone(Vec<Bar>) を証明しようとしたとしましょう。 これは失敗します(結局のところ、Bar に対する Clone の impl は 与えていません)。

  • Clone(Vec<Bar>) が証明可能であるのは、次の場合です。
    • Clone(Bar) が証明可能である場合です。(しかし、適用可能な規則がないため、これは証明可能ではありません。)

上の例は、複数の入力型を持つジェネリックなトレイトを扱うように 簡単に拡張できます。そこで、Eq<T> トレイトを考えてみましょう。 これは、Self が型 T の値と等価比較可能であることを宣言します。

trait Eq<T> { ... }
impl Eq<usize> for usize { }
impl<T: Eq<U>> Eq<Vec<U>> for Vec<T> { }

これは次のように対応付けることができます。

Eq(usize, usize).
Eq(Vec<?T>, Vec<?U>) :- Eq(?T, ?U).

ここまでは順調です。

通常の関数の型チェック

さて、トレイトがいつ実装されるかを表現し、関連型を扱うことができる いくつかの論理規則を定義したので、少し焦点を型チェックへ 移しましょう。型チェックが興味深いのは、証明する必要があるゴールを 与えてくれるものだからです。つまり、ここまで見てきたことはすべて、 プログラム内のトレイトと impl から、ゴールを証明するための規則を どのように導出するかに関するものでした。しかし、私たちは証明する必要がある ゴールをどのように導出するかにも関心があります。そしてそれらは 型チェックから得られます。

ここで、関数 foo() の型チェックを考えてみましょう。

fn foo() { bar::<usize>() }
fn bar<U: Eq<U>>() { }

もちろん、この関数は非常に単純です。行っているのは bar::<usize>() を 呼び出すことだけです。さて、bar() の定義を見ると、 U: Eq<U> という where 節が 1 つあることがわかります。つまり、 foo() が型引数として usize を指定して bar() を呼び出せることを 示すには、usize: Eq<usize> を証明する必要がある、ということです。

望むなら、bar() を呼び出せる条件を定義する Prolog の述語を書くことも できます。ここでは、その条件を「well-formed」であることと呼ぶことにします。

barWellFormed(?U) :- Eq(?U, ?U).

すると、bar::<usize> への参照(つまり、型 usize に適用された bar())が well-formed であれば、foo() は型チェックに通る、と 言うことができます。

fooTypeChecks :- barWellFormed(usize).

ゴール fooTypeChecks を証明しようとすると、成功します。

  • fooTypeChecks が証明可能であるのは、次の場合です。
    • barWellFormed(usize) の場合で、これは次の場合に証明可能です。
      • Eq(usize, usize) の場合で、これは impl があるため証明可能です。

ここまでは順調です。より複雑な関数の型チェックへ進みましょう。

ジェネリック関数の型チェック: Horn 節を超えて

前のセクションでは、標準的な Prolog の Horn 節(Rust の型等価性の概念で 拡張したもの)を使用して、いくつかの単純な Rust 関数を型チェックしました。 しかし、それが機能するのは、非ジェネリック関数を型チェックしている場合に 限られます。ジェネリック関数を型チェックしたい場合、Prolog が提供できるものより 強いゴールの概念が必要になることがわかります。私が何を言っているのかを見るために、 前の例を改造して foo をジェネリックにしてみましょう。

fn foo<T: Eq<T>>() { bar::<T>() }
fn bar<U: Eq<U>>() { }

foo の本体を型チェックするには、型 T を「抽象的」に保てる必要があります。 つまり、特定の何らかの型についてだけではなく、すべての型 T に対して foo の本体が型安全であることをチェックする必要があります。これは次のように 表現できるでしょう。

fooTypeChecks :-
  // すべての型 T について...
  forall<T> {
    // ...Eq(T, T) が証明可能であると仮定するなら...
    if (Eq(T, T)) {
      // ...`barWellFormed(T)` が成り立つことを証明できる。
      barWellFormed(T)
    }
  }.

ここで使っている記法は、私がプロトタイプ実装で使ってきた記法です。 標準的な数学の記法に似ていますが、少し Rust 風になっています。いずれにせよ、 問題は、標準的な Horn 節ではゴール内で全称量化(forall)や含意(if)を 許可しないことです(ただし、多くの Prolog エンジンは拡張としてそれらを サポートしています)。このため、「一階 hereditary Harrop」(FOHH)節と呼ばれるものを 受け入れる必要があります。この長い名前は、基本的には「本体内に forallif を 持つ標準的な Horn 節」という意味です。しかし、正式な名前を知っておくのはよいことです。 FOHH 節を効率的に扱う方法を説明した研究が数多くあるからです。たとえば、 Gopalan Nadathur による優れた 「A Proof Procedure for the Logic of Hereditary Harrop Formulas」Chalk Book の参考文献で参照してください。

FOHH のサポートは、実際にはそれほど難しいものではないことがわかっています。 そして、それができるようになれば、foo のようなジェネリック関数の 型チェック規則を、私たちのロジックで簡単に記述できます。

出典

このページは、 Nicholas Matsakis によるブログ記事を軽く改変したものです。