C++ ときどき ごはん、わりとてぃーぶれいく☆

USAGI.NETWORKのなかのひとのブログ。主にC++。

Rust の impl で pub る fn の第1引数の定義の方法と効果のメモ

主題

Rust の impl な fn の第1引数、つまり:

struct S { }
impl S
{
 fn f(❤ここ❤) { }
}

❤ここ❤ の部分の書き方と、書き方に応じてどのような効果、意図として扱われうるかを整理します。

  1. fn f()
  2. fn f(self)
  3. fn f(mut self)
  4. fn f(&self)
  5. 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を産み落としたり、 selfSelf も使わない謎のヴォイド () を生じる闇のメンバー関数はプログラミングソースコードの巨大な迷宮を作るのが目的(そういうゲームもあるかもしれません、遊びなら楽しそうですね🤣)でなければ素直に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 );
}

この実装「ならば」意図通りの動作と「同じ結果」(=効果)を生じているかもしれません。

f:id:USAGI-WRP:20200814170517p:plain

本当に 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 );
}

f:id:USAGI-WRP:20200814171651p:plain

はい、死にました。

^ 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__ まで付けるのはやり過ぎ感もありますが、意図が伝わらないよりは合理性も高いかもしれません。ちなみに selfmut 修飾されていないので 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_messages.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() );
 // ↑💀

↑は↓のように死にます。

f:id:USAGI-WRP:20200814184409p:plain

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! など何かしらにも使えます。

f:id:USAGI-WRP:20200814185650p:plain

ふつーの setter ではこんなの面倒なだけなので、こんな設計にはしません💀 このパターンでは呼び出し元の世界の smut でなくてもナカミを実質的に変更できるという利点はありますが、そんなモナド感のある実装は一般的な setter ではおおよそ使われません。(note: 一般的な setter は次の次の &mut self パターンを使います。)

4. fn f(&self)

関数スコープに参照 &self を持ってきます。 &self なのでオリジナルは呼び出し元に在るままです。呼び出し元の世界には通常は効果を及ぼさずに &self で副作用なくできる事をする場合に使います。一般的に多くの pub なメンバー関数はこのパターンの妥当性の高いかもしれません。MOVEしない getter もこれが最適です。

fn get_my_something(&self) -> &String { &self.my_something }

f:id:USAGI-WRP:20200814192231p:plain

すべてがおおよそユーザーの期待しそうな挙動を示すようになりました。🙌 でもまだ 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 の implpubfn 、つまりたいていアクセサーなどの一般的な実装で第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() );
}

f:id:USAGI-WRP:20200814193719p:plain

つまり、だいじなことは?

Rust の impl で pub る fn の第1引数も他の引数も修飾と借用と参照の考え方、効果はフリー関数を設計、実装する場合と同じです。迷ったら、Rustでどう実装するのかとかそういった事ではなく、何をしたいのか設計から入力と出力の修飾を考えるだけでよい、という事です。アタリマエではあるけれど、それがアタリマエって大事だと思うのでメモを書き残しました。

参考