Rust の impl で pub る fn の第1引数の定義の方法と効果のメモ
主題
Rust の impl な fn の第1引数、つまり:
struct S { } impl S { fn f(❤ここ❤) { } }
❤ここ❤
の部分の書き方と、書き方に応じてどのような効果、意図として扱われうるかを整理します。
fn f()
fn f(self)
fn f(mut self)
fn f(&self)
fn f(&mut self)
すべて異なる効果を持ちえます。だいじなこと、同じ効果になるパターンはありません。同じ効果に派生できる組み合わせならあります。
1. fn f()
一番簡単なやつ。呼び出しは S::f()
で行う。関数呼び出しのフルネームに struct のシンボル名が付く。実質的にフリー関数と同じ。
このパターンを作る場合は、
fn new() -> Self { }
とかfn my_awesome_construct() -> Self { ... }
のような構築パターンfn holy_sit() -> i64 { 42 }
とかfn my_void() { }
のような「それ、フリー関数でよいのでは」的なその他のパターン
の何れか。前者は Self
を使い、呼び出し時にユーザーが型を強く意識し、その型または少なくとも関連を持つ型が構築される意図の疎通を図りやすいです。
後者の self
(object) も Self
(type) も使わない聖なるUNKOを産み落としたり、 self
も Self
も使わない謎のヴォイド ()
を生じる闇のメンバー関数はプログラミングソースコードの巨大な迷宮を作るのが目的(そういうゲームもあるかもしれません、遊びなら楽しそうですね🤣)でなければ素直にimpl
の外でフリー関数として適切な mod
へ放り込むのが妥当かもしれません。
2. fn f(self)
struct のオブジェクト自身を基準とした操作を行いたい場合、意図で使います。self
は参照でもなんでもないので self
でなければならない強い効果やその意図を醸し出します。
struct S { my_something: String } impl S { fn get_my_something(self) -> Self { self.my_something } }
さて、この実装は get_
プリフィックスの付いたアクセサー風のメンバー関数から関数の仕様者に伝わる意図に対して期待通りの動作をするでしょうか?もちろん、実装詳細を確認して使えば何でもワカルという話はそのとおりですが、未知の機械にレバーが付いていれば人はレバーを上げ下げしたりすると何か機械を操作できるハズ、という意図はデザイン=設計によって伝わるものです。伝わってくれるのか、伝わってしまうのかはさておき。
fn main() { let s = S{ my_something: "サクレ・レモンは最高に美味しい氷菓です🍋".to_string() }; let awesome_message = s.get_my_something(); ptinrln!( "おめでとうございます。あなたは「{}」と暗示を受け始めました。", awesome_message ); }
この実装「ならば」意図通りの動作と「同じ結果」(=効果)を生じているかもしれません。
本当に self
の実装は呼び出しを記述するユーザーが get_
アクセサーの見た目から意図した効果を及ぼす実装でしょうか?
fn main() { let s = S{ my_something: "サクレ・レモンは最高に美味しい氷菓です🍋".to_string() }; let awesome_message = s.get_my_something(); ptinrln!( "おめでとうございます。あなたは「{}」と暗示を受け始めました。", awesome_message ); let more_message = s.get_my_something(); println!( "だいじな事なのでもう一度お伝えします: 『{}』!!", more_message ); }
はい、死にました。
^ value moved here
とか
^ value used here after move
とか言われます。訓練された Rust 技師なら「あらあらうふふ」程度の事です。初心者には手荒い洗礼に感じられるかもしれません。
S
のオブジェクト s
のフィールド my_something
の move は本当にユーザーがライブラリーに意図した実装でしょうか?あるいは、 move を意図する関数名として現代の高級言語の多くでは shallow で light な雰囲気の漂う get_
は妥当性の高い命名だったでしょうか?おそらく何れもそうではないでしょう。
本当に、何らかの効率や要求に対応する手段として s
のメンバーを move して取り出せるようにライブラリーを設計する場合、
/// ⚠ この関数は MOVE 効果を生じます。意図しない場合は呼び出してはいけません。必要に応じて参照バージョンまたは複製バージョンのアクセサーの使用を検討して下さい。 fn __danger__move_my_something(self) -> { self.my_awesome }
これくらいうるさく意図が伝わりやすくしてもよいかもしれません。__danger__
まで付けるのはやり過ぎ感もありますが、意図が伝わらないよりは合理性も高いかもしれません。ちなみに self
は mut
修飾されていないので setter 的な実装は次項の mut self
のパターンになります。
3. fn f(mut self)
mut
修飾した mut self
のパターンでは setter 的な事に使うような気がしたでしょうか?ふつうは使いません。流れで修飾の少ない順にメモを残しているだけです💀 もちろん、 setter を書けないわけではありません:
fn set_my_something( self, new_value: String ) { self.my_message = new_value; }
↑こうすると、 my_new_message
は s.my_awesome
へ MOVE されます。 s.my_something
が "サクレ・レモン!サクレ・レモン!サクレ・レモン!"
を保持する String
のメモリーに望まれる、"最終的に予定された運命" のような場合は少なくないように思います。 struct 等へ値を入れたら、入れる元のスコープにオリジナルが残ってくれる必要がない、そんな場合。↑の set_my_something
は翻訳も通ります。では何が問題でここまで setter として使う事は通常は無い空気をメモに漂わせているかというと、
let my_new_message = "サクレ・レモン!サクレ・レモン!サクレ・レモン!".to_string(); s.set_my_something( my_new_message ); // ↑ここまでなら一見、意図された設計、ユーザーにもそれほど不便でもなくMOVEになってくれる効率よい方法、そう思えるかもしれません。 println!( "👉{}👈", s.get_my_something() ); // ↑💀
↑は↓のように死にます。
error[E0382]: use of moved value:
s
つまり、set_my_something
の関数スコープへ MOVE された self
は関数内で self.my_something = new_value;
したあと、この世界には痕跡(s
というかつてself
が在ったところ…)だけを残して実体は完全に消失しています。そういう実装です。このパターンを一般的な setter の用途で使いたい場合は、
fn set_my_something( mut self, new_value: String ) -> Self { self.my_something = new_value; self }
↑こんな具合で Self
型の return を関数シグニチャーへ設定し、関数本体で最後に self
(または return self;
) し、 関数スコープへ MOVE された self
を関数スコープの呼び出し元へ return する設計に変更し、呼び出し元で、
let s = s.set_my_something( my_new_message );
↑こんな具合に s.set_my_something
で関数スコープへ飛んで行ってしまう s
を関数スコープの return から回収して新たな let s
として再定義しています。こうすると mut self
パターンの setter でも 呼び出し元のスコープに対しては実体が失われず旅行の末に帰ってきてくれます。"それ"はそこに実際に帰ってきて"在る"のでその後に println!
など何かしらにも使えます。
ふつーの setter ではこんなの面倒なだけなので、こんな設計にはしません💀 このパターンでは呼び出し元の世界の s
が mut
でなくてもナカミを実質的に変更できるという利点はありますが、そんなモナド感のある実装は一般的な setter ではおおよそ使われません。(note: 一般的な setter は次の次の &mut self
パターンを使います。)
4. fn f(&self)
関数スコープに参照 &
で self
を持ってきます。 &self
なのでオリジナルは呼び出し元に在るままです。呼び出し元の世界には通常は効果を及ぼさずに &self
で副作用なくできる事をする場合に使います。一般的に多くの pub なメンバー関数はこのパターンの妥当性の高いかもしれません。MOVEしない getter もこれが最適です。
fn get_my_something(&self) -> &String { &self.my_something }
すべてがおおよそユーザーの期待しそうな挙動を示すようになりました。🙌 でもまだ setter は面倒さが残っています。
5. fn f(&mut self)
fn set_my_something( mut self, new_value: String ) -> Self { self.my_something = new_value; self }
関数スコープを &mut self
で受ける実装にすると、一般的には呼び出し元のオリジナルの"存在"までは効果を及ぼさず、 S
がフィールドとして持つ何かしらの変更を行う実装と意図を著せます。Rustでは impl
な関数スコープでもフリー関数でも借用(Borrow)と参照(Reference)は修飾による効果はすべて同じという事を理解すると Rust の impl
で pub
る fn
、つまりたいていアクセサーなどの一般的な実装で第1引数や続くパラメーターの修飾、return の修飾、関数本体の実装に困らなくなるかもしれません。
struct S { my_something: String } impl S { //fn get_my_something( self ) -> String { self.my_something } fn get_my_something( &self ) -> &String { &self.my_something } //fn set_my_something( mut self, new_value: String ) { self.my_something = new_value; } //fn set_my_something( mut self, new_value: String ) -> Self { self.my_something = new_value; self } fn set_my_something( &mut self, new_value: String ) { self.my_something = new_value } } fn main() { let mut s = S{ my_something: "サクレ・レモンは最高に美味しい氷菓です🍋".to_string() }; let awesome_message = s.get_my_something(); println!( "おめでとうございます。あなたは「{}」と暗示を受け始めました。", awesome_message ); let more_message = s.get_my_something(); println!( "だいじな事なのでもう一度お伝えします: 『{}』!!", more_message ); let my_new_message = "サクレ・レモン!サクレ・レモン!サクレ・レモン!".to_string(); s.set_my_something( my_new_message ); //let s = s.set_my_something( my_new_message ); println!( "👉{}👈", s.get_my_something() ); }
つまり、だいじなことは?
Rust の impl で pub る fn の第1引数も他の引数も修飾と借用と参照の考え方、効果はフリー関数を設計、実装する場合と同じです。迷ったら、Rustでどう実装するのかとかそういった事ではなく、何をしたいのか設計から入力と出力の修飾を考えるだけでよい、という事です。アタリマエではあるけれど、それがアタリマエって大事だと思うのでメモを書き残しました。