🦀

RustにおけるVariance(変性)をふんわり理解する

2021/10/18に公開約5,900字

はじめに

Rustの学習を進めていくとVariance(変性)という概念に出会うかもしれません。多分これは学術的なもので正確に理解するのは難しそうです。少なくとも私はふわっとしか理解していません。この記事では「ふんわりと理解すればいいや」という人向けにRustにおける変性について説明したいと思います。

Variance(変性)とは

wikipediaによると変性とは

「より複雑な型の間のサブタイピングが、その構成要素の間のサブタイピングとどのように関連するかを意味する」

とあります。変性とは「型同士の何らかの関係」と「それら型から構成されたより複雑な型同士の関係」についての関連のこと、と言っています。なんのこっちゃ?という感じですね。まずサブタイピングとはなにかという話ですが、これもwikipdeiaを見ると

サブタイピング(Subtyping)派生型とは、計算機科学またはプログラミング言語理論において、データ型S が他のデータ型T とis-a関係にあるとき、S をT の派生型(はせいがた、subtype)であるという。またT はS の基本型(きほんがた、supertype)であるという

とあります。is-a関係のことだよ!と言っていますね。(ほぼ同じことを言い換えているだけでは。。🤔)JavaとかでいうCat型はAnimal型の特性をすべて持っているので、Cat型はAnimal型を継承するみたいな話がありますが、あれデス。このときCat型はAnimal型のサブタイプといいます。一般にAがBのサブタイプならBが期待されているところでAを使うことができます

この説明ではよく分からないと思うので、ここではJan Gjengsetさんがおっしゃっていたふんわり定義を使うことにします。

例えば、Cat型はAnimal型のサブタイプですが、これはCat型はAnimal型ができることは何でもできるので、Cat型はAnimal型と少なくとも同じくらい便利です。JavaだとCat型の引数を期待しているところにAnimal型を渡せますね。

次に構成要素とより複雑な型ですが、これはA型とB型が構成要素だとすると、「AのリストとBのリスト」、「Aを引数にとる関数とBを引数にとる関数」等がより複雑な型です。

つまり、変性というのはAとBがあるサブタイピングの関係を持っている時、AのリストとBのリスト、Aを引数にとる関数とBを引数にとる関数のサブタイピングの関係はどうなるんだい?という話です。
Rustの変性はライフタイムが絡んできます。Rustの文脈でライフタイムが全く関係しない変性の例は少なくとも私は見たことがありません。

なぜ変性を学ぶ必要があるのか

変性についてあまり聞き馴染みのない方もいるかもしれません。
こんなの知っている必要があるのか?と思うかもしれませんが、たぶんないです(特にゆるふわプログラマーにとっては)。ただ知っていると少しは役立つこともあるかもしれません。例えば以下のコードはコンパイルできるでしょうか?

fn foo<'a>(s: &mut &'a str, x: &'a str) {
    *s = x;
}

#[test]
fn is_it_work() {
    let mut s: &'static str = "Hello, world";
    let x = "Bar".to_string(); 
    foo(&mut s, &x);
    assert_eq!(s, x);
}

これはコンパイルできません。以下のようなエラーが生じます。

`x` does not live long enough

borrowed value does not live long enough

これは&mut TTに対してInvariace(反変)であることが理由の1つです。コンパイルエラーになったら直せばええがな!という人もいると思いますし、私もそれでいいと思いますが、なぜエラーになるのか訳を知りたいという人も少しはいると思うので、この記事ではRustにおけるVariance(変性)について説明したいと思います。

記法

ます記法を定義します。このような記法を使います。

あとは以下の記法も導入しときます。

変性の種類

Variance(変性)にはInvariance(不変性)Covariance(共変性)Contravariance(反変性)Bivariance(双変性)の4つがあります。この内双変性は、Rustでは私が見た記事の中には双変性である例がなかったので省略します。

Covariance(共変性)

共変であるとは構成要素A, Bのサブタイピングの関係をより複雑な型F<A>F<B>がそのまま引き継ぐということです。つまり

といえます。Rustではほとんどのものは共変です。

Rustにおける共変性

例1

fn work<'a>(s: &'a str) -> &'a str{
    s
}

fn main() {
    let x: &'static str = "Hello";
    let y = work(x);
    println!("{}", y);
}

まず'static'aのサブタイプです。なぜなら'static'aより長生きするからです。
ここでの問題は'static'aのサブタイプのとき、&'static&'aの関係がどうなるのかという話ですが、Rustでは&'static : &'aになっています。
つまり'static'aのサブタイプのとき、&'static&'aのサブタイプです。
よって上記のコードはwork関数は&'a strという型の引数を期待していて、そこに&'static strという型の変数を渡していますが、問題なくコンパイルできます。

例2

fn work<'a>(f: fn() -> &'a str) -> &'a str 
{
    f()
}

fn main() {
    let z = work(|| {
        let x: &'static str = "Hello";
        x
    });
    println!("{}", z);
}

work&'aの返り値を期待している関数を期待していますが、実際に渡している関数は&'staticの返り値を期待しています。これは動きます。何故なら、fn() -> Tは共変だからです。

Contravariance(反変性)

反変であるとは構成要素A, Bのサブタイピングの関係をより複雑な型F<A>F<B>が反転して引き継ぐということです。つまり

といえます。Rustで反変なものは現在のところ一つしかありません(fn(T) -> UはTに関して反変です)。The Rustonomiconには

反変性の唯一のソースは関数の引数であり、そのため実際にはあまり出てきません。反変性を引き起こすには特定のライフタイムを持つ関数ポインタを伴った高階プログラミングが必要になります(通常の任意のライフタイムとは異なり、サブタイピングとは独立に働く、高位のライフタイムが必要になります)。

と難しいことが書いてあるので、ふんわりRust勢はあまり気にしなくても良いかもしれません。反変性によりコンパイルできないコードの例はこのページをご覧ださい。

fn work<'a>(f: impl Fn(&'a str) -> ()) {
   f("hello");
}

fn foo1(a: &'static str) -> () {
    println!("{}", a);
}

fn main() {
    work(foo1);
}

Invariance(不変性)

不変性は共変でも反変でもないもので、指定された型そのものを要求します。Rustでは&'a mut TTに対して不変です。

冒頭のコード例はなぜ動かなかったのか

前のセクションで載せたコードを再掲します。このコードがなぜコンパイルできないのかを説明します。

fn foo<'a>(s: &mut &'a str, x: &'a str) {
    *s = x;
}

#[test]
fn is_it_work() {
    let mut s: &'static str = "Hello, world";
    let x = "Bar".to_string(); 
    foo(&mut s, &x);
    assert_eq!(s, x);
}

このfoo関数のライフタイム'aの決まり方を考えます。引数sの型である&mut &'a str不変です。
そこに&'static strという型をもつsを代入しているわけですから、'a'staticでなければいけません。
なのでfoo関数の引数xの型は&'static strになります。is_it_work関数内で渡しているxのライフタイムは'staticではないので、

`x` does not live long enough

borrowed value does not live long enough

というエラーが出たというわけです。もしこのコードがコンパイルできたら'staticな参照を持っていたs'staticでない参照をもってしまいダングリングポインタになりかねないので、この挙動はリーズナブルに思えます。

Rustにおける変性

Rustにおける変性をテーブルで見たい場合は、この記事この記事を御覧ください。

終わりに

この記事ではRustにおける変性についてふわっと解説しました。おそらく型システム入門を読んでいる人からすると、間違いが多すぎてため息な内容でしょう(私も読みたいとは思っているが時間と知力が足りない。。)。あと思ったのはRustが得意になるには借用チェッカーのお気持ちになることが大事だなということです。(ただ現状は私は借用チェッカーの振る舞いが全く分かってませんが😩)

参考文献

GitHubで編集を提案

Discussion

ログインするとコメントできます