Rust勉強メモ/トレイトとジェネリクス
?dyn
&mut dyn Write
Writeトレイトを実装している任意の値への可変参照
「プログラミングRust 第2版」p.232
dynの効果とは?
vtableを使うようになる?
トレイトの効率性
型にトレイトを追加しても余分なメモリを使わないのはなぜか
どうしたら仮想メソッド呼び出しのオーバーヘッドを被らずにトレイトを使うことができるか
「プログラミングRust 第2版」p.232
Haskellから来た3つの機能
Self型
関連関数
関連型
?制約(bound)
fn min<T: Ord>(value1: T, value2: T) -> T {
Ordトレイトを実装する型T
このような型に対する要請を制約(bound)と呼ぶ。Tとして使える型の境界を決めるからだ。
「プログラミングRust 第2版」p.232
制約ならconstraintが普通だと思うが、なぜboundを選んだ?
トレイトの2つの用途
&mut dyn Write
<T: Write>
CloneとCopy
std::clone::Clone を実装した値は、自分のクローンをメモリ上に作ることができる。
「プログラミングRust 第2版」p.233
CloneとCopyはどう違う?
code:Clone.rs
pub trait Clone {
fn clone(&self) -> Self;
fn clone_from(&mut self, source: &Self) { ... }
}
code:Copy.rs
pub trait Copy: Clone { }
CloneとCopyの違い
Copyは暗黙的に起きる
例えば y = x の代入で発生
動作をオーバーロード不能で、必ずビットワイズコピーとなる
Cloneは明示的に起きる
x.clone()
型固有のClone動作を定義可能
Stringのcloneは、ポインタのコピーではなく、ヒープ上のバイト列を複製する
?CloneとCopyをセットでderiveする意味
code:Rust
struct MyStruct;
のように、Copyをderiveするときは必ずCloneも指定されるが、それは何故?
トレイトをスコープに入れる
トレイトメソッドを利用するには、そのトレイトをスコープに入れなければならない
code:Rust
use std::io::Write;
let mut buf: Vec<u8> = vec![];
buf.write_all(b"hello")?; // ok
「プログラミングRust 第2版」p.233
何故この仕様?
トレイトによって、型にメソッドが「追加」されるから
トレイトをスコープに入れない限りメソッドは追加されず、使えない
CloneとIteratorはプレリュードに含まれるので、何もしなくても使える
トレイトメソッドの効率性
buf.write_allの呼び出しは静的に解決される
仮想関数のような仕組みではない
普通のメソッドと同じ呼び出しコスト
dynを使うと動的ディスパッチとなる
トレイト型はunsized
code:Rust
use std::io::Write;
let mut buf: Vec<u8> = vec![];
let writer: dyn Write = buf; // error: Write does not have a constant size
dyn Writeはコンパイル時に大きさが決まらない
いろんな型が代入されうるから?
トレイト型は常に参照で扱う
この辺はC++の抽象クラスの扱いと同じ
?ダウンキャスト
&mut dyn Writeから、例えばVec<u8>のような実際の型にダウンキャストすることもできない。
「プログラミングRust 第2版」p.235
JavaやC++ではダウンキャストできるが、Rustでは無理?
本当にVec<u8>であることを確認した後でダウンキャストするのはサポートしてもいい気がするのだが
ダウンキャストすると、より具体的な型として扱えるので、より最適なコードを書ける可能性がある
?トレイトオブジェクト
メモリ表現はファットポインタ
実際のオブジェクト(Vec<u8>)へのポインタ
仮想関数テーブルvtableへのポインタ
C++では、vtableは構造体自体が持つ
class A: public B
Aのメモリ構造の中にvtableへのポインタが入る
これはA*がファットポインタにならない(できない?)のと表裏一体
Rustではトレイトオブジェクトへのポインタはファットポインタになり、vtableへのポインタを持てる
?トレイトオブジェクト vs ジェネリクス
fn say_hello(out: &mut dyn Write)(トレイトオブジェクトを引数に取る普通の関数)
fn say_hello<W: Write>(out: &mut W)(ジェネリック関数)
それぞれ、どんなメリット、デメリットがあるのか?
トレイトオブジェクトはvtableのオーバーヘッドがある?
ジェネリック関数は、型の種類の数だけ実装が生成されるので、機械語が膨れる?
単相化
「プログラミングRust 第2版」p.236
say_hello(&mut local_file)?;とすると単相化される
Wは推論される
let v1 = (0 .. 1000).collect();とは書けない
collectの型パラメータを推論できないから
?トレイトの積集合
「プログラミングRust 第2版」p.237
fn top_ten<T: Debug + Hash + Eq>(values: &Vec<T>) { ... }
なぜ&やandじゃなく+なの?
where節
「プログラミングRust 第2版」p.238
code:型パラメータ制約.rs
fn run_query<M: Mapper + Serialize, R: Reducer + Serialize>(
data: &DataSet, map: M, reduce: R) -> Results
code:where節.rs
fn run_query<M, R>(data: &DataSet, map: M, reduce: R) -> Results
where M: Mapper + Serialize,
R: Reducer + Serialize
型制約を書ける場所すべてでwhere節を使用可能
生存期間パラメータ
<'a, 'b, W>のように、型パラメータより先に置く必要があるらしい
?非型パラメータ
fn dot_product<const N: usize>(a: [f64; N], b: [f64; N]) -> f64 {
「プログラミングRust 第2版」p.238
配列を、参照ではなく値で引数に渡す?
ということは、移動が起きる?
こんな大きな配列を引数に渡すと、機械語レベルではどうなるのだろう
トレイトオブジェクトとジェネリック関数、どちらを使うべきか
複数の型が混ざったコレクション→トレイトオブジェクトが正しい
もし、ジェネリクスを使ってサラダを作ると……
code:Rust
trait Vegetable { ... }
struct Salad<V: Vegetable> {
veggies: Vec<V>
}
個々のサラダは常に1種類の野菜で作られることになる
著者の一人は、Salad<IcebergLettuce>に14ドルも払ってしまったことがあり、まだその経験を乗り越えられずにいる。
「プログラミングRust 第2版」p.239
https://gyazo.com/d20e16fab8e7120f015cbebb3f11a572
トレイトオブジェクトのサイズ
code:Rust
struct Salad {
veggies: Vec<dyn Vegetable> // error: dyn Vegetable does
// not have a constant size
}
dyn Vegetableのサイズが決まっていないらしい
あれ?ファットポインタなのでは?
ファットポインタになるのは参照&dyn Vegetableとしたときか?
コードの肥大化
ジェネリック関数は型毎に機械語が生成される
コードが肥大化する
トレイトオブジェクトなら肥大化を防げる
メモリを気にしなくて良い環境であれば、3つの利点があるジェネリック関数が選ばれることが多い
ジェネリクスの3つの利点
「プログラミングRust 第2版」p.240
スピード
コンパイル時に具体的な型が分かるから最適化できる
トレイトによってはトレイトオブジェクトをサポートしない
例えば型関連関数(T::new()みたいな)
型制約の組み合わせが容易
<T: Debug + Hash + Eq>と書ける
&mut (dyn Debug + Hash + Eq)のような指定はできない
サブトレイトを使えばできるらしい
トレイトの定義
code:Rust
trait トレイト名 {
fn f(...);
}
トレイトの実装
code:Rust
impl トレイト名 for 型 {
fn f(...) { g(); }
}
ここに書けるのは、トレイトで定義されているメソッドだけ
他のメソッドは、別のimplブロックで定義
code:Rust
impl 型 {
fn g() { ... }
}
デフォルトの実装
「プログラミングRust 第2版」p.243
トレイトのメソッドにはデフォルト実装を与えられる
Iteratorトレイトはnext()を与えるだけで、その他何十ものメソッドを使える
既存の型にトレイトを実装する
「プログラミングRust 第2版」p.243
既存の型に対して、トレイトを新たに導入できる
既存の型に、既存のトレイトを実装することはできないぽい?
code:Rust
trait IsEmoji {
fn is_emoji(&self) -> bool;
}
impl IsEmoji for char {
fn is_emoji(&self) -> bool { ... }
}
拡張トレイト
「プログラミングRust 第2版」p.244
既存の型にメソッドを追加するためのトレイト
むしろ、拡張トレイトではないトレイトがある?
同じメソッドを持つ複数のトレイト
もしトレイトA、Bがメソッドf()を持っていたら?
code:Rust
impl A for T {
fn f() { ... }
}
impl B for T {
fn f() { ... }
}
f()が別の実装になってしまう?
ジェネリックimplブロック
「プログラミングRust 第2版」p.244
code:Rust
trait WriteHtml {
fn write_html(&mut self, html: &HtmlDocument) -> io::Result<()>;
}
impl<W: Write> WriteHtml for W { ... }
Writeを実装する全ての型Wに対してWriteHtmlを実装する
孤児ルール(orphan rule)
「プログラミングRust 第2版」p.245
トレイトを実装する際には、トレイトか型のどちらかが、そのクレートで新たに定義されたものでなければならない
もし、既存トレイトを、既存の型に実装できてしまったら?
impl Write for u8
同じWriteトレイトに対する実装が複数できてしまう
どのwrite()実装を呼び出せば良いか不明
トレイトがそのクレートで定義されるケース
impl 独自トレイト for 既存型
独自トレイトには、そのスコープで既存型に実装されたメソッドは実装できない?
もし実装できてしまうと、やはり同名メソッドの見分けが付かなくなりそう
code:Rust
impl MyTrait for str {
fn to_string(&self) -> String { ... }
}
fn main() {
println!("abc".to_string()); // どの to_string メソッド?
}
Self型
「プログラミングRust 第2版」p.245
トレイトの中ではSelfは、そのトレイトを実装した具体的な型となる
code:Rust
pub trait Clone {
fn clone(&self) -> Self;
...
}
xがStringならx.clone()もString
Self型を使うトレイトには、トレイトオブジェクトは使えない。
サブトレイト
「プログラミングRust 第2版」p.246
trait Creature: Visible {
全ての生き物は可視である
Creatureはサブトレイト、Visibleはスーパートレイト
Creatureを実装した型には、Visibleも実装しないとエラー
code:Rust
impl Visible for Broom { ... }
impl Creature for Broom { ... }
?トレイトとコンストラクタ
「プログラミングRust 第2版」p.247
トレイトに型関連関数を定義できる
code:Rust
trait StringSet {
fn new() -> Self;
fn from_slice(strings: &&str) -> Self; StringSetを実装する型には、この2つのコンストラクタを定義する必要がある
このような制約はJavaやC++では表せない
トレイトオブジェクトと型関連関数
「プログラミングRust 第2版」p.248
&dyn StringSetを使いたい場合、SelfにSized制約を付ける
code:Rust
trait StringSet {
fn new() -> Self where Self: Sized;
fn from_slice(strings: &&str) -> Self where Self: Sized; トレイトオブジェクト&dyn StringSetは、これら2つのコンストラクタをサポートしなくてもよい
なぜこれで上手くいくのだろうか?
いろいろなメソッド呼び出し
「プログラミングRust 第2版」p.249
"hello".to_string()
str::to_string("hello") 修飾メソッド呼び出し
ToString::to_string("hello") 修飾メソッド呼び出し
<str as ToString>::to_string("hello") 完全修飾メソッド呼び出し
?完全修飾メソッドの使いどころ
「プログラミングRust 第2版」p.250
関数そのものを関数値として使う場合
let words: Vec<String> =
line.split_whitespace() // iterator produces &str values &str の値を生成するイテレータ
.map(ToString::to_string) // ok
.collect();
ここで完全修飾名<str as ToString>::to_string を用いて、.map() に渡したい特定の関数を表している。
とあるが、mapに渡しているのはToString::to_stringである……
誤字?
完全修飾メソッドはどんなところで役立つのだろう?
関連型
「プログラミングRust 第2版」p.250
code:Rust
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
...
}
type Item;が関連型(associated type)
Iteratorを実装するそれぞれの型が関連型を指定する
code:Rust
impl Iterator for Args {
type Item = String;
fn next(&mut self) -> Option<String> {
...
関連型をジェネリック関数で使う
「プログラミングRust 第2版」p.251
code:Rust
fn collect_into_vector<I: Iterator>(iter: I) -> Vec<I::Item> {
let mut results = Vec::new();
for value in iter {
results.push(value);
}
results
}
戻り値の型で関連型I::Itemを使っている
?関連型に制約を付ける
「プログラミングRust 第2版」p.252
code:Rust
use std::fmt::Debug;
fn dump<I>(iter: I) where I: Iterator, I::Item: Debug
{
for (index, value) in iter.enumerate() {
println!("{}: {:?}", index, value);
}
}
制約の部分は where I: Iterator<Item=Debug> とも書ける?
関連型の例
「プログラミングRust 第2版」p.253
code:Rust
trait Pattern {
type Match;
fn search(&self, string: &str) -> Option<Self::Match>;
}
impl Pattern for char {
type Match = usize; // 文字が見つかった位置を表す
fn search(&self, string: &str) -> Option<usize> {
...
}
}
?関連型の数
関連型は、それぞれの実装に関係する型が1つしかない場合には適している。
「プログラミングRust 第2版」p.253
関連型が複数あるような定義をしたらどうなるの?
code:Rust
trait A {
type T1;
type T2;
...
?乗算演算子のオーバーロード
「プログラミングRust 第2版」p.253
code:Rust
pub trait Mul<RHS> {
type Output;
fn mul(self, rhs: RHS) -> Self::Output;
}
impl<T> Mul<WindowSize> for Vec<T>はどういう意味?
任意の型Tに対して「Vec<T> × WindowSize」が定義できる?
impl<T>は9.7節で初出
型が複雑になる例
「プログラミングRust 第2版」p.254
code:型を真面目に書いた.rs
use std::iter;
use std::vec::IntoIter;
fn cyclical_zip(v: Vec<u8>, u: Vec<u8>) ->
iter::Cycle<iter::Chain<IntoIter<u8>, IntoIter<u8>>> {
v.into_iter().chain(u.into_iter()).cycle()
}
これは何をやる関数?
一番後ろまで取り出すと先頭に戻り、無限に要素が出てくるイテレータ?
IntoIterってなに?
impl Trait
「プログラミングRust 第2版」p.254
型が複雑なときに「トレイトXを実装した型」という指定が出来る機能
トレイトオブジェクトで置き換えると、ヒープの割り当てや動的ディスパッチのオーバーヘッドがある
code:トレイトオブジェクトを使った.rs
fn cyclical_zip(v: Vec<u8>, u: Vec<u8>) -> Box<dyn Iterator<Item=u8>> {
Box::new(v.into_iter().chain(u.into_iter()).cycle())
}
impl Traitを使うと静的に解決される
code:impl Traitを使った.rs
fn cyclical_zip(v: Vec<u8>, u: Vec<u8>) -> impl Iterator<Item=u8> {
v.into_iter().chain(u.into_iter()).cycle()
}
実装詳細ではなく「何らかのイテレータを返す」となり、将来の変更に強くなる
動的ディスパッチが必要な場面ではimpl Traitを使えない
「プログラミングRust 第2版」p.255
code:Rust
fn make_shape(shape: &str) -> impl Shape {
match shape {
"circle" => Circle::new(),
"triangle" => Triangle::new(), // error: incompatible types
"shape" => Rectangle::new(),
}
}
コンパイル時に型が決まらないから
ジェネリック関数とimpl Trait
「プログラミングRust 第2版」p.256
以下2つは等価
code:ジェネリック関数.rs
fn print<T: Display>(val: T) {
println!("{}", val);
}
code:impl Trait.rs
fn print(val: impl Display) {
println!("{}", val);
}
ただしジェネリック関数ではprint::<i32>(42)のように型パラメータを明示できる
トレイトの関連定数
「プログラミングRust 第2版」p.256
初期値有り、無し両方の定数を定義できる
次のコードはコンパイルできなかった
code:Rust
trait A {
const MSG: &'static str = "hello";
const NAME: Self;
fn say(&self) {
println!("{} {}{}", Self::MSG, Self::NAME, self);
}
}
impl A for char {
const NAME: char = 'c';
}
制約のリバースエンジニアリング
「プログラミングRust 第2版」p.257
トレイト制約を正しく付けるのは難しいよねという話
お題:内積を計算するコードをジェネリックにする
code:Rust
fn dot(v1: &i64, v2: &i64) -> i64 { let mut total = 0;
for i in 0 .. v1.len() {
total = total + v1i * v2i; }
total
}
C++的な解法
「プログラミングRust 第2版」p.258
code:Rust
fn dot<N>(v1: &N, v2: &N) -> N { let mut total: N = 0;
for i in 0 .. v1.len() {
total = total + v1i * v2i; }
total
}
Nが+と*をサポートしていないのでエラー
AddとMulを制約に加える
「プログラミングRust 第2版」p.258
code:Rust
fn dot<N: Add + Mul + Default>(v1: &N, v2: &N) -> N { let mut total = N::default();
for i in 0 .. v1.len() {
total = total + v1i * v2i; }
total
}
MulはN * NがNとは異なる型になる想定のためエラー
例えば、行列同士の乗算は異なる型になる
Defaultは様々な型の「0」を表すのに必要
演算後の型について制約を加える
「プログラミングRust 第2版」p.259
code:Rust
fn dot<N: Add<Output=N> + Mul<Output=N> + Default>(v1: &N, v2: &N) -> N whereを用いると
code:Rust
fn dot<N>(v1: &N, v2: &N) -> N where N: Add<Output=N> + Mul<Output=N> + Default
コピー可能の制約を加える
「プログラミングRust 第2版」p.259
v1[i]で値を移動できない、というエラー
Nがコピー可能である必要がある
code:Rust
fn dot<N>(v1: &N, v2: &N) -> N where N: Add<Output=N> + Mul<Output=N> + Default + Copy
制約との戦い
「プログラミングRust 第2版」p.260
コンパイラを用いた、Nの制約のリバースエンジニアリング
使いたいトレイトが無いときは大変な作業
今回のケースではnum::Numを使うと次のように書ける
code:Rust
use num::Num;
fn dot<N: Num + Copy>(v1: &N, v2: &N) -> N { let mut total = N::zero();
for i in 0 .. v1.len() {
total = total + v1i * v2i; }
total
}