なんか考えてることとか

変な人が主にプログラミング関連で考えていることをまとめる。

Rustがオブジェクト指向型言語ではないのとその理由

  • 2021/06/12
  • 2021/06/15 「追伸」追加

ja.wikipedia.org

Rustはマルチパラダイムプログラミング言語であり、手続き型プログラミング、オブジェクト指向プログラミング、関数型プログラミングなどの実装手法をサポートしている。

Rustは2021年6月時点でWikipediaでは「オブジェクト指向プログラミング(以下OOP)」をサポートしていることになっている。実際にはRustがオブジェクト指向型言語であるかどうかは、かなり意見が割れているのにも関わらずだ。

Rustの公式ドキュメントTRPLではRustがオブジェクト指向プログラミングであるかどうかは相当意見が分かれることをほのめかしている。そのため、確かにオブジェクト指向型言語であるかどうかは人によって意見が異なるのかもしれない。しかし、いや、だからこそ、このような意見が分かれる情報を断定してはいけないと思う。

さて、自分はRustがオブジェクト指向型言語であるか、についてどう考えているかであるが、タイトルからもわかる通り「オブジェクト指向型言語ではない」と考えている。
その考察となぜそのような設計になったのかについて解説する。

オブジェクト指向における2つの考え方

Smalltalkベースの考え方

Smalltalkとは、様々なプログラミング言語の要素にAlan Kay氏の「メッセージング」という考えを取り入れたオブジェクト指向型言語および統合化プログラミング環境である。
Smalltalkに取り入れられている「メッセージング」は、オブジェクト指向プログラミングを始めに提唱したAlan Kay氏によるオブジェクト指向の考え方である。

こちらの方が詳細に解説してくれているが、つまりメッセージングとはざっくり言ってしまえば「オブジェクトにメッセージを送る、あるいはオブジェクト同士がメッセージを送り合う」という考え方である。その影響か、後に解説するSimulaベースのオブジェクト指向とは違って、メソッドを実行することはそのまま「メソッドを実行する」とは言わず、「メッセージを送る」などと言う

ところで賢明な読者は気づいたのではないだろうか。この考え方には継承という概念が存在しないと言うことに。実はAlan Kay氏のオブジェクト指向の考え方では継承は必須ではない。実際にSmalltalk-72では継承がなかった。しかしやはりいろいろときついものがあったのか、後に継承機構が取り入れられている(その際にSimula 67をベースとしたため、今のSmalltalkは比較的簡単にSimulaベースのOOPも実現可能である)。

Simulaベースの考え方

実はAlan Kay氏のオブジェクト指向はSimula 67から発想を得た。が、Alan Kay氏とは別にこれを別方向に(より忠実的に)解釈し、Cに取り入れた人がいたのだ。そう、後のC++となるC with Classesの開発者、Stroustrup氏である。

Stroustrup氏のOOPの考え方は以下のようになっている。

Decide which classes you want;
provide a full set of operations for each class;
make commonality explicit by using inheritance.


(どのようなクラスを求めるか決める;
各クラスに対応した操作を用意する;
継承を使って共通性を明示する。)

What is ‘‘Object-Oriented Programming’’? (1991 revised version)より引用

このC++オブジェクト指向が大きく広がり、JavaC#など様々なプログラミング言語に採用された。これをC++ベースのオブジェクト指向と言う人もいるが、Smalltalkベースのオブジェクト指向はSimula 67から発想を得たに過ぎず、Simula 67の影響をもろに受けているのはC++であるため、自分はSimulaベースのオブジェクト指向と呼んでいる。

ちなみに今ではSimulaベースのオブジェクト指向オブジェクト指向三大要素から成っていると言われる。

このSimulaベースのオブジェクト指向ではSmalltalkベースのオブジェクト指向とは打って変わって継承が非常に重要であったとされている。

Rustには継承がない

さて、このRustだが、実は隠蔽化とポリモーフィズムはサポートされている。ただ一つ、「継承」だけがないのである。そのため、RustはSimulaベースのオブジェクト指向の観点で考えたときにオブジェクト指向三大要素のうちの一つが欠けているということでオブジェクト指向とは言えないのである

そもそもRustには本当に継承がないのか?と疑問に思われるかもしれない。しかし、以下を実行してみればわかる通り、Rustでは継承ができない。

/* -------------- */
/* 継承できない例1 */
/* -------------- */

// ベースとなる構造体
pub struct Base {
    member_base: i32,
}

// 継承する構造体
// エラーになります
pub struct Derived : Base {
    member_derived: &'static str,
}

fn main() {}

Rust Playground

/* -------------- */
/* 継承できない例2 */
/* -------------- */

// ベースとなる構造体
pub struct Base {
    member_base: i32,
}

// 継承する構造体
pub struct Derived {
    member_derived: &'static str,
}

// Base構造体のメソッド実装
impl Base {
    pub fn method(&self) {
        println!("This is method in Base: {}", self.member_base);
    }
}

// Baseを継承したDerivedのメソッドを実装
// エラーになります
impl Base for Derived {
    // オーバーライド
    pub fn method(&self) {
        // 基底構造体のメソッドも呼び出す
        <Self as Super>::method();
        
        println!("This is method in Derived: {}", self.member_derived);
    }
}

fn main() {}

Rust Playground

もしRustに継承があったらこうなるんだろうなーって妄想なんで、そうはならんだろ、というのは気にしないでいただけるとありがたい。

見ればわかる通り、そもそも型とメソッドの実装部分が別々であるため、その時点で「継承できなさそうだなー」というのは感じ取っていただけるだろう。

Rustにはメッセージングという概念がない

いくらAlan Kay氏の考えるオブジェクト指向に継承はなかったとしても、そもそもRustにはメッセージングという概念が存在しないため、この時点でSmalltalkベースのオブジェクト指向とは程遠い。

Smalltalk-72において実はクラスは関数(手続き)の定義とあまり変わらず、メソッドは条件分岐をするような感じ(具体的には、メッセージ式中でメソッド名を見つけたらその手続きを実行する)になっており(こちらを参照)、関数を定義することで実現するのとは別のアプローチでメソッドを実現している。ここから、メッセージングとはどういうことなのか、というのを感覚でつかみ取るのと同時に、「あぁ、RustはSmalltalkベースのオブジェクト指向とは違うな」ということを感じ取ってもらえると思う。

また先ほども書いたが、継承がないとは言っても、Smalltalk-76以降では継承機構が追加されているため、Smalltalkベースのオブジェクト指向では継承が必要ないとは言っても、Smalltalkベースのオブジェクト指向でも継承がないとやはり厳しいものがあったことを物語っている

つまり、Rustがメッセージング抜きでSmalltalkベースのオブジェクト指向だとして考えて、今のSmalltalkでは継承があるという事情によって、今のSmalltalkと比べて継承がないのはRustがオブジェクト指向型言語たらしめるのか?と言われれば、これもまた微妙なところなのである(もっとも、Smalltalkベースのオブジェクト指向では「メッセージング」が一番の要となっているので、やはりメッセージングがない時点でRustはSmalltalkベースのオブジェクト指向型言語ではないのだが)。

Rustはオブジェクト指向型言語ではない

以上から言えることは、どちらのオブジェクト指向でもないのだ。もちろん、これ以外にもオブジェクト指向の考え方があるものの、いずれもSmalltalkベースのオブジェクト指向もしくはSimulaベースのオブジェクト指向から着想を得ている。

そして、Rustのオブジェクト指向(と言われている機能)は、関数型言語の影響も強く受けている。後述するトレイトも純粋関数型言語であるHaskellから強く影響されていると考えられる。
別に関数型であることがオブジェクト指向ではないことの決め手になるわけではないが、トレイトがオブジェクト指向型言語ではないHaskellに強く影響されている以上、トレイトはオブジェクト指向型言語の決め手としては弱い

以上から、自分はRustがオブジェクト指向型言語ではないと考えている。

なぜRustはオブジェクト指向に欠いた設計になったのか

これはうっかりそうしてしまったのではなく、意図的である

Rustでは継承はないが、トレイトと呼ばれる機能がある。ちなみにこのトレイトはSchärli氏らの提唱したトレイトではないため注意が必要だ。詳しくはこちらを参照。

このトレイトという機能は、型に実装することでその型はトレイトで定義されたメソッドを使うことができるようにする。

/* ----------------- */
/* 型にトレイトを実装 */
/* ----------------- */

// なんか適当に構造体を定義
pub struct ImplStruct;

// 実装するトレイト
pub trait Implement {
    // 処理は書かなくて良い
    fn frame(&self) -> ();
    
    // デフォルト実装も可能
    fn default_impl(&self) -> () {
        println!("Default.");
    }
    
    fn overridden(&self) -> () {
        println!("not yet overridden.");
    }
}

// 構造体にトレイトを実装
impl Implement for ImplStruct {
    // 何も処理がないメソッドはここで処理を実装する
    fn frame(&self) -> () {
        println!("Implement!");
    }
    
    // 実装したトレイトのメソッドはオーバーライドが可能
    fn overridden(&self) -> () {
        println!("OVERRODE.");
    }
}

fn main() {
    // ImplStructのインスタンスを生成
    let instance = ImplStruct;
    
    // メソッドを実行
    instance.frame();
    instance.default_impl();
    instance.overridden();
}

Rust Playground

Implement!
Default.
OVERRODE.

ここで重要なポイントはこれは型にトレイトを継承したのではなく、型にトレイトを実装したと考えることだ。
「継承した」はis-a関係にあり、「実装した」はhas-a関係もしくはpart-of関係(今回はpart-ofのほうがわかりやすいと思うのでpart-ofで行くことにする)にある。

is-a関係

主に継承したとき、「継承先 is a 継承元」、つまり「継承先は継承元である」と言うことができる。

たとえば以下の疑似オブジェクト指向型言語の例で考えてみる。

// 霊長類クラス
class 霊長類 {
    /* 省略 */
}

// 霊長類クラスを継承したヒトクラス
class ヒト extends 霊長類 {
    /* 省略 */
}

このとき、「ヒト is a 霊長類」、つまり「ヒトは霊長類である」と言える。なぜこのように言えるのかと言うと、ヒトは霊長類の特徴(プログラムで言いかえるとフィールドやメソッド)を持っているからである。
つまり継承をするとき、継承先は必ず継承元と同じフィールドやメソッドを持っていると言える

part-of関係

Rustで型にトレイトを実装することは、is-a関係に当てはまらない。

先ほどの疑似オブジェクト指向型言語で考えてみる。

auto インスタンス := new ヒト()

is-a関係では、インスタンスを生成する際は継承先(今回の場合「ヒト」)だけでも成立する(もちろんグローバルスコープ内に継承元(今回の場合「霊長類」)の定義が必要となるが)。そしてそれは継承元もそうである。

すなわち、継承先も継承元も単体で成立することを示している。

それと比べてトレイトは実装する型なしでは成り立たない。つまりトレイトはその型の部品であると考えるべきである

これに適していると考えられるのがpart-of関係である。Rustだと「トレイト part of 型」、すなわち「トレイトは型の一部である」となる

「is-a関係」の問題点

is-a関係、つまり継承の問題点だが、継承をすると、継承先は継承元のフィールドやメソッドをごっそり持っていく形となってしまう。大胆にコードを再利用するのであればそれでも良いのかもしれないが、もし要らないコードがあったら?その場合は大問題である。

この継承における問題のわかりやすい例として有名なのが「バナナモンキージャングル問題」である。

オブジェクト指向言語の問題は、それらが持ち歩くこの暗黙の環境をすべて持っていることです。あなたはバナナが欲しかったのですが、あなたが手に入れたのはバナナとジャングル全体を持ったゴリラでした。

さようなら、オブジェクト指向プログラミング(日本語訳版)より引用

大草原不可避なのだが、それはとりあえず置いておこう。

このように要らない機能まで持っていってしまうことで、使うべき機能がわからなくなってしまう可能性があるために、今では多くのオブジェクト指向型言語に当然のようにある継承という機能について疑問視する声も増えてきている

Rust開発者も継承には問題があると考えたようで、TRPLでは継承に関してこのように言及している。

Inheritance has recently fallen out of favor as a programming design solution in many programming languages because it’s often at risk of sharing more code than necessary. Subclasses shouldn’t always share all characteristics of their parent class but will do so with inheritance. This can make a program’s design less flexible. It also introduces the possibility of calling methods on subclasses that don’t make sense or that cause errors because the methods don’t apply to the subclass. In addition, some languages will only allow a subclass to inherit from one class, further restricting the flexibility of a program’s design.


(継承は必要以上にコードを共有してしまうリスクがあることから、近年では多くのプログラミング言語においてプログラミングの設計手法として支持されなくなっている。サブクラスは必ずしもすべてのコードを共有すべきではないのに、継承ではそうなってしまう。これによりプログラム設計の柔軟性が損なわれる可能性がある。また、サブクラスでメソッドを呼び出す際に、そのメソッドが無意味であったり、サブクラスに適用されていないメソッドであるためにエラーになったりする可能性もある。それに加えて、言語によってはサブクラスは1つのクラスからしか継承できないもの*1もあり、これによりプログラム設計の柔軟性がさらに損なわれている。)

17.1. Characteristics of Object-Oriented Languagesより引用

「part-of関係」による解決策

このis-a関係を持つ継承の問題点は、part-of関係によって解決できる。
というのもpart-of関係の場合、「このオブジェクトにはどの部品を持たせよう?」という感覚でフィールドやメソッドを柔軟に付け加えることができるからである。

Rustのトレイトはまさにそれで、トレイトはあくまで一つの用途に特化した機能を提供するだけであり、型に様々な用途のトレイトを複数組み合わせることで一つのオブジェクトを作り上げるのである

RustはOOPができるし、オブジェクト指向型言語では?

ここで、「RustでもOOPが可能だから、オブジェクト指向型言語である」という人も現れると思うが、これははっきり間違いであると断言しておこう。なぜなら、OOPができるからと言って、その言語がオブジェクト指向型言語であるとは限らないからである。

たとえば、オブジェクト指向型言語ではないことが自明であるCでは、OOPはできないのだろうか?否、できるのである。しかし、それが容易にできるだろうか?否、できない

つまり何が言いたいのかと言うと、プログラミングパラダイムにおける「○○型言語である」というのは、そのプログラミング手法が容易にできるようにサポートされているか?ということである。
自分はRustが容易にOOPできるようにサポートはされていないと思う。オブジェクト指向型言語から多くの要素を取り入れていることは確かだが。

以上から、「RustはOOPが可能だからオブジェクト指向型言語」というのは、わかりやすい例で例えれば「CはOOPが可能だからオブジェクト指向型言語」であると断言しているようなものであり、それは指摘としては失当な物であるとこの記事では考える。

追伸

TwitterでたくさんのふぁぼRT、ブログでのコメントありがとうございます。

私は今でも「Rustはオブジェクト指向型言語ではない」という主張は変わりませんが、今思えば少し硬く捉えてWikipediaに対して痛烈な批判をしてしまったかな、とちょっと反省しております(汗。

ブクマコメへの返信もここで行います。

OOP言語自体の定義の問題なのだが、オブジェクト指向プログラミングを機能や構文で実現する支援をする言語っていうのがオブジェクト指向プログラミング言語っていう説(?)もあって、その言語で書いたらOOPになるっての

https://b.hatena.ne.jp/entry/4703997949117521282/comment/poad1010

言わんとしていることはわからなくもないですが、そもそも私は「RustがOOPのできない言語」とは書いておりませんし、それにオブジェクト指向型言語で書いたらOOPになるのであれば全世界のオブジェクト指向プログラマたちは苦労してしないと思います(おそらくこの辺は言葉足らずなのでしょうけど)。

*1:単一継承のこと。C++Pythonでは複数のクラスから継承ができる多重継承がある