RustにおけるVariance(変性)をふんわり理解する
はじめに
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 T
がT
に対して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 T
はT
に対して不変
です。
冒頭のコード例はなぜ動かなかったのか
前のセクションで載せたコードを再掲します。このコードがなぜコンパイルできないのかを説明します。
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が得意になるには借用チェッカーのお気持ちになることが大事だなということです。(ただ現状は私は借用チェッカーの振る舞いが全く分かってませんが😩)
参考文献
- https://www.youtube.com/watch?v=iVYWDIW71jk
- https://doc.rust-lang.org/nomicon/subtyping.html
- https://doc.rust-lang.org/reference/subtyping.html
- https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)
- https://ja.wikipedia.org/wiki/派生型
- https://qnighy.hatenablog.com/entry/2018/01/14/220000
Discussion