30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

Drop トレイトと Deref トレイトについて

この記事では、Rust のDropトレイトとDerefトレイトについて説明していく。
それぞれdropメソッドとderefのメソッドを必須としているが、これらのメソッドを明示的に呼び出すことは稀で、多くの場合暗黙的に呼び出される。

トレイトそのものの初歩については以下を参照。

Drop トレイト

以下のコードでは、MyBoxというタプル構造体にDropトレイトを実装している。
Dropuseを使ってインポートする必要はなく、そのまま使える。
そしてDropトレイトを実装している型はdropメソッドが必須であり、このメソッドはselfへのミュータブルな参照を引数として取る。

struct MyBox(String);

impl Drop for MyBox {
    fn drop(&mut self) {
        println!("drop {}", self.0);
    }
}

dropメソッドは、スコープを抜けたときに実行される。そのため以下のような結果になる。

struct MyBox(String);

impl Drop for MyBox {
    fn drop(&mut self) {
        println!("drop {}", self.0);
    }
}

fn main() {
    let _x = MyBox(String::from("X"));
    {
        let _y = MyBox(String::from("Y"));
    } // drop Y
} // drop X

この例ではprintln!を実行しているだけだが、Dropトレイトは主に、不要になったリソース(メモリなど)を解放するために使われる。
スコープを抜けたということはその値が使われることはもうないため、このタイミングでリソースを解放すると都合がよい。また、スコープを抜けたときに自動的に呼び出されるため、リソースを解放し忘れる恐れもない。

dropメソッドを手動で呼び出そうとすると、コンパイルエラーになる。

fn main() {
    let x = MyBox(String::from("X"));
    x.drop(); // explicit destructor calls not allowed
}

std::mem::drop関数を使うことで、任意のタイミングでdropメソッドを呼び出すことができる。
この関数もDropトレイトと同様、useを使ってインポートする必要はない。

以下のコードを実行するとdrop Xendの順番で表示されるので、std::mem::drop関数を実行したタイミングでdropメソッドが呼び出されていることがわかる。

fn main() {
    let x = MyBox(String::from("X"));
    drop(x);
    println!("end")
}

ドロップしたあとに値を使おうとするとコンパイルエラーになる。

fn main() {
    let x = MyBox(String::from("X"));
    drop(x);
    println!("{}", x.0); // value borrowed here after move
}

ドロップする順番

複数の値が同時にスコープを抜けた場合、宣言された順番とは逆の順番でドロップしていく。

struct MyBox(String);

impl Drop for MyBox {
    fn drop(&mut self) {
        println!("drop {}", self.0);
    }
}

fn main() {
    let _x = MyBox(String::from("X"));
    let _y = MyBox(String::from("Y"));
    let _z = MyBox(String::from("Z"));
}
// drop Z
// drop Y
// drop X

Dropトレイトを実装した型が入れ子になっている場合は、まず親がドロップされる。子については、宣言された順番にドロップされる。

struct Child(i32);
struct Parent(Child, Child);

impl Drop for Child {
    fn drop(&mut self) {
        println!("drop {}", self.0);
    }
}

impl Drop for Parent {
    fn drop(&mut self) {
        println!("drop Parent");
    }
}

fn main() {
    let _x = Parent(Child(4), Child(5));
}
// drop Parent
// drop 4
// drop 5

Deref トレイト

*演算子を使うと、参照が指している値にアクセスすることができる。これを「参照外し」(dereference)という。

fn main() {
    let x = 1;
    let y = &x;
    println!("{}", x == *y); // true
    println!("{}", x == y); // can't compare `{integer}` with `&{integer}`
}

構造体に対しては参照外しできない。

struct MyBox {}

fn main() {
    let x = MyBox {};
    let y = *x; // type `MyBox` cannot be dereferenced
}

Derefトレイトを使うことで、構造体に対して参照外しができるようになる。

まずはシンプルな例を示す。

struct MyBox {}

use std::ops::Deref;

impl Deref for MyBox {
    type Target = i32;

    fn deref(&self) -> &Self::Target {
        &9
    }
}

fn main() {
    let x = MyBox {};
    let y = *x;
    println!("{}", y); // 9
    println!("{}", y == 9); // true
}

MyBoxのインスタンスであるxに対して参照外しをしたところ、9を得られた。

Deref トレイトの実装方法

DerefDropと異なり、useでインポートする必要がある。

use std::ops::Deref;

Derefトレイトを実装する型にはtype Targetderefメソッドが必須。

まずtype Targetだが、参照外しを行った際に得られる型を設定する。先程の例ではtype Target = i32;と書いたので、参照外しするとi32型の値が得られることになる。
そしてderefメソッドで、実際に得られる値の参照を返す。値ではなく参照を返すので、先程の例では9ではなく&9を返している。

構造体がジェネリックを使っている場合は以下のように書く。

struct MyBox<T> {
    value: T,
}

use std::ops::Deref;

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.value
    }
}

fn main() {
    let x = MyBox { value: 9 };
    let y = MyBox { value: "abc" };
    println!("{}", *x); // 9
    println!("{}", *y); // abc
}

なぜ Deref トレイトを実装すると参照外しできるようになるのか

xDerefを実装してるとき、xに対して参照外しを行おうとすると、つまり*xと書くと、コンパイラはそれを*(x.deref())に変換する。Derefトレイトは必ずderefメソッドを持っているためそれが実行され、値を得られるのである。

xが以下のMyBoxのインスタンスであるとき、*x*(x.deref())となり、derefメソッドが実行された結果*(&9)となり、それは最終的に9になる。

impl Deref for MyBox {
    type Target = i32;

    fn deref(&self) -> &Self::Target {
        &9
    }
}
fn main() {
    let x = MyBox {};
    println!("{}", x.deref() == &9); // true
    println!("{}", *(&9) == 9); // true
    let y = *x;
    println!("{}", y == 9); // true
}

参照外し型強制

Derefトレイトを実装している型の参照を関数に渡すと、「参照外し型強制」と呼ばれる処理が発生することがある。

以下のis_foois_i32はそれぞれ、Fooi32の参照を引数として受け取る。そのためis_i32Fooの参照を渡せば、当然コンパイルエラーになる。

#[derive(Debug)]
struct Foo {}

fn is_foo(arg: &Foo) {
    println!("{:?}", arg);
}

fn is_i32(arg: &i32) {
    println!("{}", arg);
}

fn main() {
    is_i32(&7); // 7
    let x = Foo {};
    is_foo(&x); // Foo
    is_i32(&x); // expected `i32`, found struct `Foo`
}

FooDerefトレイトを実装してderefメソッドが&9を返すようにすると、is_i32Fooの参照を渡せるようになる。
コンパイラが暗黙的にderefを呼び出して&Foo&9に変換しているため、このような結果になる。この挙動が、参照外し型強制である。

#[derive(Debug)]
struct Foo {}

use std::ops::Deref;

impl Deref for Foo {
    type Target = i32;

    fn deref(&self) -> &Self::Target {
        println!("deref executed!");
        &9
    }
}

fn is_foo(arg: &Foo) {
    println!("{:?}", arg);
}

fn is_i32(arg: &i32) {
    println!("{}", arg);
}

fn main() {
    is_i32(&7); // 7
    let x = Foo {};
    is_foo(&x); // Foo
    is_i32(&x); // 9
}

上記を実行してみると、deref executed!は一度しか表示されないため、参照外し型強制は必要なときにしか発生しないことが分かる。
そのため、&Foois_fooに渡したときは参照外し型強制は発生せず、is_fooはそのまま&Fooを受け取る。

整理すると、参照外し型強制は以下の条件を全て満たしたときに発生する。

  1. 何らかの参照を関数に渡す
  2. 関数の引数の型が、渡された型と一致しない
  3. 渡された型がDerefトレイトを実装している

参照外し型強制は、必要に応じて何度も繰り返される。
そのため以下のケースでは、&Foois_i32に渡した際に参照外し型強制が 2 回発生している。まず&Foo&Barに変換され、さらに&Bar&9に変換される。

#[derive(Debug)]
struct Foo {}

#[derive(Debug)]
struct Bar {}

use std::ops::Deref;

impl Deref for Foo {
    type Target = Bar;

    fn deref(&self) -> &Self::Target {
        println!("deref executed in Foo!");
        &Bar {}
    }
}

impl Deref for Bar {
    type Target = i32;

    fn deref(&self) -> &Self::Target {
        println!("deref executed in Bar!");
        &9
    }
}

fn is_foo(arg: &Foo) {
    println!("{:?}", arg);
}

fn is_bar(arg: &Bar) {
    println!("{:?}", arg);
}

fn is_i32(arg: &i32) {
    println!("{}", arg);
}

fn main() {
    let x = Foo {};

    // Foo
    is_foo(&x);

    // deref executed in Foo!
    // Bar
    is_bar(&x);

    // deref executed in Foo!
    // deref executed in Bar!
    // 9
    is_i32(&x);
}

String のケース

Stringも、Derefトレイトを実装している型のひとつ。
そのため以下のis_deref関数に渡すことができる。boolDerefトレイトを実装していないため、渡すとコンパイルエラーになる。

struct Foo {}

use std::ops::Deref;

impl Deref for Foo {
    type Target = i32;

    fn deref(&self) -> &Self::Target {
        &9
    }
}

fn is_deref<T: Deref>(_: T) {
    println!("is deref!");
}

fn main() {
    is_deref(Foo {}); // is deref!
    is_deref(String::from("abc")); // is deref!
    is_deref(true); // expected an implementor of trait `std::ops::Deref`
}

StringDerefトレイトの実装はtype Target = strになっているため、derefメソッドは&strを返す。
そのため、&strを引数として受け取る関数hello&Stringを渡すことができる。

fn hello(name: &str) {
    println!("Hello, {}!", name);
}

fn main() {
    let x = String::from("abc");
    hello(&x); // Hello, abc!
}

DerefMut

Derefトレイトに加えてDerefMutトレイトも実装すると、ミュータブルな参照も返せるようになる。
DerefMutトレイトを実装する型には、deref_mutメソッドが必須となる。

#[derive(Debug)]
struct MyBox {
    value: i32,
}

use std::ops::Deref;
use std::ops::DerefMut;

impl Deref for MyBox {
    type Target = i32;

    fn deref(&self) -> &Self::Target {
        &self.value
    }
}

impl DerefMut for MyBox {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.value
    }
}

fn main() {
    let mut x = MyBox { value: 9 };
    *x = 8;
    println!("{:?}", x); // MyBox { value: 8 }
    println!("{}", *x == 8); // true
}

状況に応じてderefderef_mutのいずれかを呼び出す。

#[derive(Debug)]
struct MyBox {
    value: i32,
}

use std::ops::Deref;
use std::ops::DerefMut;

impl Deref for MyBox {
    type Target = i32;

    fn deref(&self) -> &Self::Target {
        println!("deref");
        &self.value
    }
}

impl DerefMut for MyBox {
    fn deref_mut(&mut self) -> &mut Self::Target {
        println!("deref_mut");
        &mut self.value
    }
}

fn main() {
    let mut x = MyBox { value: 9 };
    *x = 8; // deref_mut
    let _y = *x; // deref
}

なぜ組織の透明性が大切なのか

個人的に、組織の透明性というものに関心を持っている。自分にとって大切なことだし、組織にとっても大切だと思っている。
この記事では、透明性に対する現時点での考えを書いていく。今の自分の頭のなかのスナップショットのようなものなので、あまり整理されていない。

大きく分けて、なぜ透明性が大切なのか、そして透明性を実現するために大切だと思っていることについて、書いていく。
透明性とは何か、透明性が高いとは具体的にどういう状況のことなのか、といった話は扱わない。取り敢えず、情報や意思決定のプロセスがオープンになっており誰でも制限なくアクセスできる、くらいの意味で書いている。本当はそれだけでは不十分で、情報のメンテナンスやサマライズ、適切な通知やアナウンス、なども必要になってくるが。

なぜ透明性が大切なのか

透明性に問題があると何が起こるのか、という角度から述べていく。

モチベーションが下がる

もしかしたらこれは人によるのかもしれないが、少なくとも自分は、「何のためにそれをやるのか」という目的が見えていないタスクだと、モチベーションが出ない。タスクをやるにあたり納得感が欲しい。
情報がオープンになっていればタスクの背景やそこに至る過程が見えるが、そうでない場合、何のためにそれをやるのかが分からない。

ここでいう納得感というのは、自分の考えと一致するかどうか、という話ではない。別に自分の判断と異なっていたとしても構わない。きちんとした理由やロジックが存在するのであれば。
どういう理由でこのタスクをやろうとしているのか、どういう事情やロジックでこのタスクを優先的にやることにしたのか、それが分かっていれば、納得感を持って動ける。例え自分とは考えが違ったとしても。
「社長が言っていたから」「顧客に言われたから」という説明で思考停止しながら働くのでは、モチベーションを維持するのが難しい。そしてモチベーションが低ければ生産性も下がるわけで、組織にとってもデメリットとなる。

タスクの目的が見えていない状況でも、そのタスク自体が面白かったり興味があったりするものであれば、モチベーションの問題は回避できるかもしれない。
とはいえそういうタスクばかりではないわけだし、情報や議論の過程をオープンにしてタスクの背景が見えるようにしておいたほうがよい。

判断の精度が下がる

当たり前の話だが、判断の精度を高めるためにはとにかく情報が多いほうがよい。必要な情報がろくに与えられないようではまともな判断などできるわけがないし、一見すると関係性がないような情報でも、どこでどうつながってくるか分からない。
関係者の間に情報が行き渡っていなければ、行き違いや遠回り、手戻りなどが増える。

プログラマであれば、何かを作ってくれと言われたときにその背景や目的が分かっていれば、もっと適切な手段を提案できるかもしれない。作るべきものとして提示されたものが、課題解決の手段として適切ではないことは珍しくない。
作るものの細部についても、何を作るかやどう作るかについて、実際に作る人の立場から意見を出せる。特に「どう作るか」はプログラマが判断することが多いだろうから、とにかく多くの情報が欲しい。
重要な情報を隠され、後出しされると、「最初からそれを教えてくれていれば、こういう風には作らなかったよ」となってしまう。こうなるとモチベーションに重大な悪影響を与えるし、単純に「間違ったもの」「適切ではないもの」を作ってしまったのだから、組織としても大きな損失となる。

これはプログラミングに限ったことではない。
人によって得意なことや詳しいことは異なるし見ているものも立場も違うのだから、できるだけ多くの人に情報や状況を共有して考えてもらったほうがいい。そうすることで、組織としての判断の精度が高まっていく。
ただでさえ事業やプロダクト開発は不確実性が高く分からないことだらけなのに、分かっていることすら共有せず暗闇のなかで業務にあたらせるのはどうかしていると思う。

主体性を奪われる

判断の精度が下がるという話をしたが、透明性が低いとそもそも判断ができなくなる。判断するために必要な情報を持っておらず、得ようとしても得られないため、判断ができない。その結果、ただ言われたことをやるしかなくなってしまう。主体性や自律性は、情報がオープンになっており、少なくとも得ようとすれば得られる環境でのみ、成り立つ。
組織として何をしようとしているのか、どこに進もうとしているのかが分からなければ、個人としても何をすればいいのか分からない。

個別のタスクについても、必要な情報を得られる環境であれば、ある程度は自分で判断しながら開発を進めていける。しかし情報を得られない環境だとそれも難しい。背景が分かっていれば自明のことでも、情報が欠けていると自分で判断できず、いちいち他者に確認しながら進めることになってしまう。
これでは業務の効率は大きく落ちてしまう。

誰もが課題に気付きそれに対してアクションを起こしていける組織が望ましいと思っているが、そのためには情報が公開されている必要がある。

当事者意識が失われ組織の分断が進む

自分の知らないところで知らないうちに物事をが決まっていき、それが適切にシェアされない。自分が意思決定に一切関与できず情報も満足に共有されない。
そのような環境で当事者意識を保つのは難しい。無力感や疎外感が育っていく。自分にできることは何もない、という気持ちが強くなっていく。
言われたことだけ、与えられたことだけやっていればいい、というより、そうせざるを得ない、という状態になってしまう。そうなってしまうと、事業や組織の問題を自分事と捉えるのは難しくなる。

必然的に、他人の業務に関心を持たなくなる。
そして、誰もやらないタスク、誰も責任を持たないプロジェクトが増えていく。そうすると組織として問題が頻発するようになるが、それに対しても関心がない。それを解決しようとする過去の試みは、政治や情報の隠蔽によって何度も打ち砕かれてきたのだから。

お互いに「自分の領域」のことだけを考え、その外側のことに対しては何もしない。
だからフィードバックを送り合う文化も育たないし、誰かが送ったところで誰にも響かない。なぜなら興味がないから。事業や組織がどうなろうと、もはや関心がない。

透明性を実現するために大切なこと

透明性は大切である、という価値観の共有

これが何より大事だし、これが全てのような気がする。
具体的な施策や取り組みが上手く機能していなかったとしても、透明性は大切であるという価値観を強く持っていれば、改善していける。
逆に価値観や問題意識を強く持っていなければ、いくら施策を打ったところで、上手くいかない。表面的に仕組みや制度を取り入れただけで終わってしまう。

この記事を読んでいる人は透明性に対してそれなりに関心が高いのだと思うが、世の中はそういう人ばかりではない。むしろ少数派かもしれない。
ポジションによってアクセスできる情報に違いがあるのは当然、と思っている人は多い。組織を混乱させようという意図があるわけではなく、ごく自然に情報を隠蔽し、それを使って政治を行う。隠蔽される側も、そういうものだと思っており、疑問や不満を持っていない。彼らには彼らの文化がある。
情報や意思決定の透明性に対して特段の考えを持っていない人も多い。

こういった人が多数派の組織だと、議論の前提を共有できず、具体的な取り組みよりも前の段階で躓くことになる。
そして人の価値観というものは、基本的には変化しない。少なくとも周りの人間が変えさせることはできない。
そのため、透明性の大切さについて合意が取れていないような環境では、個人ができることはほとんど何もないと思う。

言語化しようとする姿勢、具体的にはテキストベースのコミュニケーションやドキュメント文化

テキストベースのコミュニケーションの素晴らしさは、非同期コミュニケーションが可能になることにある。その場にいなかった人間も参加できるし、議論を追うことができる。
そして、コミュニケーションそのものが、そのままドキュメントとして残るというメリットもある。これによって、コミュニケーション発生時には入社すらしていなかった人間でも、どのような議論が発生し、その結果どのような結論が出たのかを把握することができる。口頭でのコミュニケーションではこうはいかない。全てが消えてしまい、その場にいなかった人間には共有されない。
もちろん口頭でやり取りをしたほうが望ましいケースもあるが、その場合でも論点や結論についてはテキストにして共有すべき。

テキストベースでのコミュニケーションというと Slack のようなチャットツールがまず思い浮かぶだろうが、それだけでなくタスクやチケットを管理するためのツールでのやり取りも重要だと思う。
各人が、何のためにどんなタスクをやっているのか、進捗状況はどうなのか、適宜書き込んでいく。そしてその情報は当然、誰でも閲覧できる。そうすることで、誰が何をやっているのかという情報が可視化される。
また、タスクやチケットに対する連絡事項や質問、議論もこのツールで行うことで、情報がドキュメントとして残る。

ドキュメント文化というか、ちょっとしたことでもすぐにメモとして残しておいたり、走り書きでもいいから書き残したりするような文化も大切だと思っている。
取り敢えず雑に書いて、誰もが閲覧できる場所に放り投げておく態度。こういう動きを習慣にしている人が組織のなかに複数いると、助かる。タスクに関するちょっとしたティップスをタスク管理ツールに書き残しておいたり、ドキュメント共有サービスにメモしておいたり。
例えばどの会社にも、その会社独自の用語がある。専門用語であったり、組織の歴史のなかで自然発生してそのまま定着した謎の用語であったり。そういう用語が増えていくとコミュニケーションが効率的になって楽なのだが、その用語を共有していない他部署の人間や新しく入社した人間にとっては、かえってコミュニケーションの障害となる。情報の格差が生まれている状況といえる。そういうときに、その用語で検索するとすぐにドキュメントが見つかる状態になっていると、いちいち質問せずに済む。
ドキュメントが蓄積されていくという効果もあるが、誰が何に詳しいのかや何に関心があるのかが可視化される、という効果も期待できる。

上記のような行動のベースにあるのは、言語化しようとする姿勢だと思う。自分の頭のなかにあるものを言語化してオープンな場所に出力する姿勢。
そもそも言語化しなければ情報として形を持たないのだから、共有しようもない。可視化すべき情報がそもそも存在しない。言語化することで初めて形を持つ。
だから、いかにして言語化するかが大切になる。不完全でもいいから、自分が考えていることや知っていることを言葉で表現してもらう。
テキストベースのコミュニケーションやドキュメントの作成を推奨すれば、必然的に言語化を促すことになるし、しかもそれがそのままオープンな場所に置かれることになる。

成功体験の積み重ね

情報をオープンにしていく姿勢は、一部の偉い人が持てばよい、というものではない。メンバー全員が持たないといけない。そうしないと、情報は滞留し、行き渡らない。
そして情報の公開や共有を積極的に行ってもらうためには、それに対する成功体験を得てもらう必要がある。情報を共有してよかった、走り書きレベルのものを書いても大丈夫、という感覚を持ってもらわないといけない。

まだ自分のなかで整理しきれていないアイディアを書き込んだら、その行為を咎められた。抱えている問題意識について積極的に発信していたら、「言い出しっぺだから」という理由で一方的に担当者にさせられた。
こういう経験が続けば自然と、情報の発信や共有は滞る。まだ完成度が低いアイディアや意見のような「余計なこと」は言わないようになるし、人目につかないクローズドな場所でやり取りするようになる。
担当しているタスクとは直接関係ないような話題について書き込んだり議論したりすることを「余計なこと」と見做して評価しないような態度も、よくない。

透明性に限らず、何らかの文化や習慣を根付かせるためにはまず、それに対して安心感を持ってもらい行動の敷居を下げる必要がある。そしてそのためには、成功体験を積み重ねてもらうしかないと思う。