RustのOption::take()
too many linked list っていうRustで書きづらいようなデータ構造を頑張って書いて行こうぜみたいなblog postを最近読んでいた.
その中ででてきて面白かったのが Option::take()
という関数.
/// Takes the value out of the option, leaving a [`None`] in its place. /// /// # Examples /// /// ``` /// let mut x = Some(2); /// let y = x.take(); /// assert_eq!(x, None); /// assert_eq!(y, Some(2)); /// /// let mut x: Option<u32> = None; /// let y = x.take(); /// assert_eq!(x, None); /// assert_eq!(y, None); /// ```
Optionに実装されているものはこんな感じで、Optionの中身がSomeだった場合は中のvalueを取り出して返し、元のOption型の変数にはDefault値 (OptoinではNone) を詰める. OptionがNoneだった際はそのままNoneが返り、元のOption変数もNoneのまま.
なぜ面白いと感じたかというと、このtake()を使えば、下記のようなコードを書いてもborrow checkerに怒られることがないからだ. (前提となる全てのコードは載せてません. 詳しくはblog postを覗いてみてください)
pub fn push(&mut self, elem: T) { let newlink = Link::Some(Box::new(Node { elem: elem, next: self.head.take(), })); self.head = newlink; }
もしtakeがよくあるようなselfの所有権を奪ってしまう関数だったら、ここでnew_linkにselfの所有権が移ってしまうことでエラーが発生してしまうと思う. しかしOption::take()ではエラーにはならない.
ではなぜエラーにはならないのだろう? これはRustが低レベルでunsafeな操作をいい感じにwrapしてくれていて、その結果take()が可変参照のメソッドとして提供できているからだと考えている.*1 つまり使用者はtake()によってオブジェクトが変更されうるという仕様さえ把握して、それを意識したコーディングをしていれば安全性は担保される.
Rustの静的チェックは基本的に保守的だと言われている. つまり、所有権エラーによって弾かれた箇所は、実際に実行させた時に必ずエラーになるとは限らない. ただ、今回のOption::wrap()のように所々で使用者にその保守的なチェックをうまく回避するためのAPIをRustは用意してくれている.
Rustお勉強メモ
copy, cloneあたりの記事を読んで、そのメモ 意外と基礎が抜けていたり.
Moveについて
- moveは基本 shallow copy. objectごとにcopyしたりはしない
Copyについて
- コンパイラによって自動的に実装される.
- stack上にcopyが置かれる
- heapを指している(ものを含んでいる)ような型には実装できない
- スマートポインタ系 (Box, Cell, Rc etc...) や Vec, Stringなど
- heapを指している型を含んでる型を複製したかったら、Cloneを実装する
- copyを生成する際は、元のObjectを参照しない. (Cloneの部分にその逆を書いた)
Cloneについて
- &selfを材料にして、Userによって振る舞いを定義できる
- heapを指しているような型にも実装できる
- (デフォルトの動きとしては)指してる先のheap上のObjectも複製した上で、stack上にそのデータを指す変数を新たに生成する.
raw pointerとデリファレンス
- row pointer自体は幾つでも複製できる. (幾つでもrawpointerの所有者を作れる)
- ただし、それをdereferenceしようとすると、Copyを実装していないといけない. (元の実態が勝手に複製されている、みたいなことはあってはならない)
参考
RustでImmutable Stackを使おうとした際に遭遇した難しさ
最近下記の記事を読んで、Immutable Stackなるものを知った.
qiita.com
この記事の本質はArc/Rcの仕組みを紐解くところあるのだけれど、Immutable Stackの概念が妙に新鮮かつ(状態を持たずにstackを実現している部分が)とても美しいと思った.
この記事を参考にして、Immutable Stackを普通のプログラミング時に使えないかと思って色々試行錯誤してみたけど、結論あまりうまくできていない.
struct Stack<T>(Option<Rc<(T, Stack<T>)>>) struct Info {} struct User { s: Stack<Info> } impl User { fn pop_info(&mut self) { self.s.pop(); // ... }
模擬コードはこんな感じで、Userが何かしらのStackデータ構造を持っていて、その実現にImmutable Stackを用いてみた、といった形だ. 目指していたゴールとしては、
- Stackの更新時に一切Copyを発生させない.
だった. しかし今のところ下記のような理由でこれはうまくいっていない. (もしかしたらまだ思いついていない解決策があるかもしれない.)
- Stackの更新には、古いStackが必要. (例えば pop()の実装は
fn pop(self) -> (Self, Option<T>)
みたいな感じになっている) - つまり、Stackの所有権を1度捨てる必要がある.
- しかしこれはUserの所有物であるStackを消費することになり、引き続きUserを使用する場合は新規でUserを作成してそれを返してあげる必要がある.
このようにImmutable Stackをある構造体の中に入れて管理しようとすると、Stackを更新する際に結局User自身を更新する必要が出てくる. これを許容するには、Stackを更新するUserのメソッドでも、self(User)を返してあげる必要がある.
ただそうすると、Userは一般にImmutableなデータ構造であるとは限らないので、結局Userのコピーが強いられることになりかねなくてetc...
Rustのちょっとわかりづらいmove error
突然ですが、
struct A {} impl A { fn foo(self) -> Self{ Self{} } } struct B { a: A } impl B { fn new(a:A) ->Self { Self {a: a} } fn baz(&self) { let _ = self.a.foo(); } }
このコードは L15で次のようなコンパイルエラーを吐きます.
cannot move out of
self.a
which is behind a shared reference move occurs becauseself.a
has typeA
, which does not implement theCopy
trait
aがmoveしてしまってるぞ!的なErrorですが、一瞬みただけだとこのerrorの原因ってちょっとわかりづらくないないですか. (慣れてる人はすぐわかると思いますが自分は初めてみた時少し理解に時間がかかりました.)
errorが発生してる baz()
のコードを紐解いて、少し状況を整理してみましょう.
fn baz(&self) { let _ = self.a.foo(); }
このコードは、下記のように書き換え可能です.
fn baz(&self) { let _a = self.a let _ = _a.foo(); }
さらに、メソッドfoo
fn foo(self) -> Self{ Self{} }
は、(通常の)関数
fn foo_fn(s: A) -> A{ A{} }
と等しいことから、baz()
は
fn baz(&self) { let _a = self.a let _ = foo_fn(_a); }
こんな感じの関数と等価と言えそうです. このような形に変形してやると、foo_fnが_a
をOwnしてる様がわかりやすくなると思います.
さらにいきましょう. このbazも(methodではなく、)普通の関数と見立ててしまって、
fn baz_fn(b: &B) { let _a = b.a let _ = foo_fn(_a); }
とかけそうです. ここまで来るとerrorになる原因がかなりわかりやすくなったでしょう. そうです、他所でOwnされてるBの、メンバであるaを、foo_fnで move させてしまってますね. Bは他所で生きているにもかかわらず、Bが所有してるメンバ a がdropされてしまうために、コンパイラはerrorを吐いていたのです.
まとめ
methodでチェーンしてたりすル部分で所有権エラーが発生すると一瞬原因がわかりづらくなったりしますが、こんな感じで1つ1つ丁寧に処理を追っていくと割と状況を掴みやすい気がしてます.
(自戒) Vecの不用意なindexアクセスでcloneを呼ばないようにしようね
どうも、最近RustのCloneの使用を極力減らしていきたい症候群に駆られてるmox692です.
内容はタイトルのままです.
Rustを触り始めて間もない頃はVecに対して安易に↓のようなコードを書きがちじゃないでしょうか.
#[derive(Clone, Copy)] struct A{} fn main() { let a1 = A{}; let a2 = A{}; let v = vec![a1,a2]; let b1 = v[0]; let b2 = v[1]; }
型A
の変数をVecに詰めて、それに対してindexアクセスしています. 他のプログラミング言語でもよく見るような構文ですね.
ここで一回、struct A{} の #[derive(Clone, Copy)]
を消してしまいましょう. するとどうなるでしょうか
cannot move out of index of `Vec<A>` --> src/main.rs:9:14 | 9 | let b1 = v[0]; | ^^^^ | | | move occurs because value has type `A`, which does not implement the `Copy` trait | help: consider borrowing here: `&v[0]`
let b1 = v[0];
の行で怒られています.
もうお察しの方もいると思いますが、そうです. この v[0];
のようなindexアクセスを記述した場合、デフォルトではVecの要素がCopyされるようになっているのです. *1
その証拠にこのようなコードを動かしてみます.
#[derive(Clone, Copy)] struct A{} fn main() { let a1 = A{}; let a2 = A{}; println!("a1: {:p}", &a1); println!("a2: {:p}", &a2); let v = vec![a1,a2]; let b1 = v[0]; let b2 = v[1]; println!("b1: {:p}", &b1); println!("b2: {:p}", &b2); }
結果は
a1: 0x7ffcba70c490 a2: 0x7ffcba70c498 b1: 0x7ffcba70c548 b2: 0x7ffcba70c550
のようになり、b1, b1は元のa1, a2とは別のメモリ領域にある別の変数になっていることがわかると思います.
[T].get(...) の使用
cloneしても問題なければcloneしても大丈夫だと思いますが、取得したdataを変更しないことがわかってるのであればslice の get() を使用した方が 行儀がいいです.
まとめ
- 他のプログラミング言語でよくあるような
v[i]
のようなindexアクセスによる代入は、コピーを発生させる. - コピーを発生させたくないのであれば、sliceのget methodを用いて参照を得た方が行儀がいい
- 日々のコードで所有権がどのように移り変わっていくのかをもっと観察すべし (自戒!!!)
*1:インデックスアクセスでは参照を返すべきだ!みたいにいう人もいるかもしれませんし、実際そういうデザインの選択肢もありだったのかもしれません. ただそれだとさすがに不便 or 不自然だとRustチームは判断したんでしょうかねどうなんだろう
これを理解すればRust中級者を名乗れるのでは?的なTraitたち
「Rustのコードを読んでいるとたまに出会うけど、直接今書いてるコードとは関係ないしまあ理解は後回しでいいか...」的な感じで、自分の中で理解を後回しにしていた概念(主にTrait)達を少しまとめようと思う.
- std::borrow::Borrow
- std::borrow::Cow
- std::borrow::ToOwned
- std::convert::AsRef
- std::convert::From
- std::convert::Into
- std::ops::Deref
GWで少し時間もあるので、こいつらと向き合ってみる. これらのTraitの理解が深まったらまたブログ記事を書くかもしれない.
rustのthread::spawn
rustのthread生成関数、std::thread::spawn()
にて、docコメントに以下の記述があった.
/// In terms of [atomic memory orderings], the completion of the associated /// thread synchronizes with this function returning. In other words, all /// operations performed by that thread are ordered before all /// operations that happen after `join` returns.
アトミックメモリの順序に関しては、関連付けられたスレッドの完了は、この関数が戻るのと同期します。 つまり、そのスレッドによって実行されるすべての操作は、
join
が戻った後に発生するすべての操作の前に順序付けられます。
おおおこの感じ、javaのhappens-before制約やん!ってなった.
つまり、
let j1 = thread::spawn(|| { // something... }) j1.join().expect("fail1") let j2 = thread::spawn(|| { // something... }) j2.join().expect("fail2")
と書けば、j1 -> j2 というorderingは確約される、ってことかな.