「Pythonのlen関数がいかにオブジェクト指向的であるか説明する」で、せっかく(?)我らが Smalltalk の御大、アラン・ケイに言及されているのにスルーではもったいない(??)ので、当たってもぜんぜん痛くない発泡スチロール製マサカリを。w
#まず「オブジェクト指向(プログラミング。以下同様)」とは何か、から
老婆心ながら、これを抑えておかずに「いかにオブジェクト指向的か」を論じたところで空しいだけです。^^; 以下は念のため、TL;TR だが「オブジェクト指向は何か」を知らない人には DR とも言い切れない毎度おなじみテンプレ。(テンプレなので blockquote でお届けします)
「オブジェクト指向」は、言及者の故意(主に自分の推すオブジェクト指向分析や設計と紐付けしたい人が多いみたい…)か単に理解不足かは別にして、一般に明確な区別なく語られてしまっていまいがちですが、実のところ次の**2つのまったく異なる考え方(アイデア)**を指します。したがって、文脈によってどちらを意図しての発言かを、聞き手の側が都度切り分けて整理する高度な“エスパー力”が求められる史上まれに見る学習者泣かせの実に難しい用語です。
1.“メッセージ”のオブジェクト指向
「オブジェクトへのメッセージ送信」に象徴される、メッセージングを方便やお題目にして、「決定の遅延」を推奨し、可能な限りそれをサポートするアイデアです。アラン・ケイが考え、Smalltalkで限定的に実践され、その効果が検証されました。(参考→「ソフトウェア工学」は矛盾語法か?)
メッセージングを方便やお題目にせず本当にメッセージとして送信する実装でも構わないのですが、実際にトークン列としてメッセージを送っていた(ただし非同期ではない) Smalltalk-72 の段階ですでに、主に効率面でスケールしないことが分かっていたので、現実解としてはそのキモたる「決定の遅延」に必要な「結合の遅延」のしくみ(動的性)だけをできるだけ残しつつ、通常のしかし動的なメソッドの呼び出しをもって「メッセージ」と称することが多いです。
もっとも、メソッド呼び出しをメッセージと称するにあたっては、そのキモたる「決定の遅延」をサポートするためにコールすべきメソッドがそこに無かった場合でも、送られたメッセージはそれとして(デフォルトで無視を決め込むか、あるいは例外を挙げるかはさておき)何かしらの応答できる枠組みはあってしかるべき(具体的には Smalltalk なら doesNotUnderstand: 、Ruby なら method_missing のような機構)でしょう。
本当に非同期でやりとりするメッセージと言えばみんな大好き**「アクター」ですが、これはこのケイのメッセージングのオブジェクト指向のアイデア(具体的には Smalltalk-72)をヒントに、並列・並行処理の問題解決に特化した理論**です。まずアクターがあってのケイのメッセージングのアイデアだと説明されることが多いですがこれは間違いです(参考→Actor induction and meta-evaluation)。加えて、アクターの考案者のヒューイットは(彼の PLANNER に強い影響を受けて考案された一部仕様のみで)実装は試されなかった“ Smalltalk-71”と、メッセージングの実験で作られた Smalltalk-72 を混同している(参考→ケイの指摘)らしいので、一次情報であっても両者の関係を論じる際は注意を要します。
2.“型(あるいはインターフェース)”のオブジェクト指向
「カプセル化、継承、ポリモーフィズム」に象徴される、ユーザー(プログラマ)による型定義を「クラス」という('70~80年代にはまだ新しかった)言語機能を用いてやるアイデアです。(参考→What is “ObjectOriented Programming”? (1991年の改訂版))
ユーザー定義の型には、抽象データ型(技法的にはデータ抽象とも)と呼ばれる技術を使用します。もとより抽象データ型は、別にクラスを使わずとも実践できます(事実、抽象データ型の発案者は「クラスター」というクラスとは異なる言語機能で抽象データ型ををサポートする言語を実装しています)。このオブジェクト指向のキモは抽象データ型に他なりませんが、だからといって「クラスが無くてもオブジェクト指向はできる」とか、まして「オブジェクト指向にクラスは必須ではない」などという主張は、ことこの「型のオブジェクト指向」の文脈に限っては本末を転倒しておりナンセンスです。ちなみに前述三点セットのうち、「カプセル化」は抽象データ型の特徴、「継承」は言語機能としてのクラスの特徴、「ポリモーフィズム」は派生クラスで部分型を表現することをそれぞれ表わしています。ただ、この部分型についてはクラスの継承を使うことでこれを表現すると、実装によっては問題が生じることが分かっていて(参考→Inheritance Is Not Subtyping)、それを回避するために提案された言語機能としての「インターフェース」(参考→Interfaces for Strongly-Typed Object-Oriented Programming)を型を定義する際にクラスの代わりに(あるいは補助的に)使うことが多いです。
型を作ったり使ったりするわけですから、当然、静的に型を解決するなどそのメリットを大いに活用すべきで、実際このアイデアをサポートする言語の多くは静的型チェック機構を備えていますが、動的型言語でも(きわめて限定的にはなりますが)実践や一部メリットの享受は可能です.卑近なところでは、前述のメッセージングのオブジェクト指向の実践の場であるはずの Smalltalk も、途中から SIMULA スタイルのクラスを採用することで、継承を通じて不完全ながらこの「型のオブジェクト指向」を意識した機構(具体的には、インターフェースを満たさない時の subclassResponsibility が挙げる例外)を備えていたりします。
なおこの「型のオブジェクト指向」は C++ の考案過程で整理し到達したアイデアではありますが、キーとなる「クラス」の考案者である SIMULA の設計者たちや、「抽象データ型」の考案者自身はもちろん、ちょっと時代は下りますが Smalltalk と並び「オブジェクト指向」の代表格であった Eiffel の設計者を含む、複数の人たちが時期を前後して(本人たちの主張を尊重すれば)独自に似たようなアイデアに到達しています。
本題の「オブジェクト指向的か否か」は、この2つのオブジェクト指向に照らして評価する必要があります。
#シンタックス的にどうか?
Python の len(list)
のシンタックスはオブジェクト指向的ではないのは、元記事の冒頭でも述べられているとおりです。しつこいですが、メッセージングのオブジェクト指向、型のオブジェクト指向の両面から解釈してみましょう。
もとより、メッセージングのオブジェクト指向では、オブジェクトに対するメッセージの送信の意味を持ちますから(そうでなければならないということはありませんが)、やはり レシーバー メッセージ
の順になっていた方が使い勝手は良いです。
ただ、なんでもメッセージで表現することを好むメッセージングのオブジェクト指向の解釈の柔軟性を考えると、たとえば Smalltalk-72 において、do
や for
といったクラス外関数にメッセージを送ることでループを実現していた経緯
これhttps://t.co/YcDDMsLlMA を書いていてふと、Smalltalk-76からメンバー関数を「メソッド」と称する前の-72で「メソッド」は何だったのか?に興味がわいて調べてみた。結局よく分からなかったけど、今はメソッドと区別されるクラス外の関数もメソッドと呼んでいた事実が判明してなかなか興味深い。 pic.twitter.com/5rIfvUUntW
— sumim (@sumim) 2018年11月30日
を鑑みると、Python の len(list)
も len
に対する list
というメッセージの送信と捉えることも可能ですから、(もとより Python がメッセージングのオブジェクト指向をさほどまじめにはサポートする気はなさそうだという事実も踏まえ)第三者の勝手な解釈の幅の持たせ方しだいではオブジェクト指向的と主張することもまぁ可能かな…とは思います。
試しに、Smalltalk-72 で len
へのメッセージ送信で list
の長さを求めるコードを書いてみましょう。to len (⇑(:) length)
が len
というクラス外関数(やはりオブジェクトでメッセージを受け取ることができる。念為)の定義です。
通常、Smalltalk-72 では vec length
で vector
(配列)の長さを求めますが(念のため、Smalltalk-80 以降、現在の Pharo なども含む処理系では length
の代わりに size
を使うよう変更されています。Python の逆ですね ^^;)、クラス外変数 len
を新たに定義することで、len
に vec
をメッセージ送信することでサイズを求められるようになります。
閑話休題。
一方、型のオブジェクト指向の視点からはどうでしょう。このパラダイムでは「メッセージ」やまして「レシーバー」といったコンセプトはいっさい関係ないのですが、それでもクラスに属する関数(C++風に言えば「メンバー関数」。Java が Objective-C や Smalltalk の影響で「メソッド」と呼ぶようになってから多少混乱はあるものの、クラス従属の関数を特にメソッドと呼んで区別する分には問題ないでしょう…)は、第一引数.関数名(第二引数以降)
の形式でコールするのが主流となっています。したがって、元記事の冒頭でも触れられているとおり、このオブジェクト指向においても Python の len(list)
は“オブジェクト指向的”とは言えなさそうです。
いずれのオブジェクト指向の考え方においても、かたや(len
がメッセージの受け手であるという無理矢理な解釈をしない限りにおいて)メッセージの受け手が誰かを知る上で、かたやコールする関数がどの(引数が属する)クラスに定義されているのかを知る上で、list.len()
の方が“読みやすく”はあると思います。きけば Python であえて len(list)
を選んだのも“読みやすさ”ゆえの選択であるようなので、ここはオブジェクト指向的であることをあえて捨てて、好みの読みやすさをとったと考えたり説明するのが順当だと思います。
#では、実装面ではどうか?
メッセージングのオブジェクト指向においては、実装をどうするかはあまり気にしない(決定の遅延、ばんざい!)ので、ここでは型のオブジェクト指向だけ。
繰り返しになりますが、型のオブジェクト指向は「型をクラス(あるいはそれに準ずるインターフェース等の言語機能)で定義できるようにする」というのがキーとなるコンセプトなので、定義した型に属するデータの操作(メンバー関数、あるいはメソッドとも呼ぶ)は型に属することになります。もちろん、ユーザー定義型(抽象データ型)は、クラスのような仕組みを使わずとも実現できるので(関数型がよい例ですね)、その型に属するデータの操作を型に内包する必要はかならずしもありません。このクラス以外の方法で実現された抽象データ型を提供する言語機能としての抽象データ型を「狭義の抽象データ型」とここでは呼ぶことにします。
Python の len(list)
の見た目や使い方はオブジェクト指向というよりは「狭義の抽象データ型」寄りだと分かります。
ところで、(型の)オブジェクト指向と、狭義の抽象データ型は運用面でのトレードオフがあります。同じインターフェースを持つ型を新たに追加したいとき、便利なのがオブジェクト指向です。追加する型と操作のセットを定義すれば、既存の型の定義にはいっさい触れずに済ませられます。狭義の抽象データ型ではそうはいきません。通常、狭義の抽象データ型では、操作(関数)は引数の型をみて分岐し、その型ごとに処理を記述します。型が増えると、関連する操作はすべて書き換える必要が生じます。
逆の場合はどうでしょう。型は追加せず、操作(ひいてはインターフェース)を新たに増やしたいときに圧倒的に有利になるのが狭義の抽象データ型の方式です。既存の型の定義には触れずに、追加したい操作(関数)を追加して記述するだけで済ませられるのに対し、一方の(型の)オブジェクト指向では既存の型を記述するクラスにすべてに手を入れなければならないからです。
私は当初、Python の len
関数の実装は、引数の型を見て分岐する「狭義の抽象データ型」のスタイルをとっているものと思い込んでいました(前後しますが、すくなくとも __len__
の導入以前は─)。ところが改めて調べてみると、入出力値の型や範囲のチェックこそしていますが、len
の中でやっているのは list.__len__()
だというではありませんか(参考→Pythonはどうやってlen関数で長さを手にいれているの?)。なんとも肩すかしを食った気分です。
くしくも、前述の脱線で Smalltalk-72 において関数 len
を定義したときの実装と似た感じになっていることもいろいろと興味深いですね。
#まとめ
結局、Python の len(list)
は、見た目や使い方はオブジェクト指向的ではないけれど、実装はオブジェクト指向的であったという、元記事と同じ結論でした。すこし理屈っぽく紐解いてみてはいますが…。^^;
Python は、静的型チェックをサポートしていないことからも分かるように、(型の)オブジェクト指向のおいしいところは積極的に取り入れつつも、見た目や使い方などはオブジェクト指向からはあえて外れて、(少なくとも設計者が)読みやすいと思うものを採用している言語であるようです。
なので、仮に「オブジェクト指向的じゃない!」と揶揄されることがあっても、無理に「オブジェクト指向的」だと言い張るのではなく、Python にはあえてそれを捨ててももっと大事なものを選択したんだ!と受け流しておけばよいのではないのかなぁ…と老婆心ながら思った次第です。