Hatena::ブログ(Diary)

プログラミング言語を作る日記

2016-07-17 まだたとえ話で消耗してるの?

[]まだたとえ話で消耗してるの?

常に話題には乗り遅れる私ですが、ちょっと前、「変数を箱にたとえる」ことについて議論がありました。

プログラミングの変数を教えるときの「箱の説明」の是非について。 - Togetterまとめ

実のところこの話題は昔から何度も出てきた話で、「何周目だ」という話ではあります。私自身、「センス・オブ・プログラミング!」に書いたことがあります。

変数は「箱」か? - the code to rock

そして、上記の「the code to rock」の文章にもありますが、「箱モデル」の代わりとしてよく提唱されるのが「名札モデル」です。

こういうことは文章で書いてもわかりにくいので、それぞれ、絵にしてみましょうか。

箱モデル

■「a = 5;」という代入

f:id:kmaebashi:20160717224325p:image

「aという名前が付いた箱に、5という値を格納する」プログラミング言語の入門書によく出てくる説明ですね。

■それに続いて、「b = a;」という代入

ここで、以下のような、「aの箱から値を取り出してbの箱に入れる」というイメージをしてしまうと、

f:id:kmaebashi:20160717225406p:image

「この代入のあと、aの箱は空っぽになってしまうのでは?」という疑問が出るかもしれません。これが箱モデルの(欠点といえば)欠点です。

しかし、それ以前の話として、「5が代入されたaという変数は、5という値が書けるところなら基本どこにでも書け*1、5と同じ意味を持つ」、という原則からすれば、「b = a;」という代入は、結局「b = 5;」と変わりません。

f:id:kmaebashi:20160717225405p:image

こう考えれば、そう難しい話でもないのではないでしょうか。

名札モデル

さて、名札モデルです。名札というと、小学生が胸につけてるアレのイメージでしょうか。だとすると絵にするとこうでしょうか。

f:id:kmaebashi:20160717230444p:image

しかし、名札モデルの人のイメージでは、どうも名札モデルが重要なのは参照型の場合であるようなので、

ここではこんな、「紐付きラベル」で描いてみます。

f:id:kmaebashi:20160717230443p:image

■「a = 5;」という代入

f:id:kmaebashi:20160717231136p:image

「5というオブジェクトに、aという名札を付ける」と言えばよいでしょうか。

■それに続いて、「b = a;」という代入

f:id:kmaebashi:20160717230443p:image

「aという名札の先にあるオブジェクトに、bの名札を付ける」でしょうか。

しかし、先ほどの「5が代入されたaという変数は、5という値が書けるところなら基本どこにでも書け、5と同じ意味を持つ」という原則からすれば、「b = a;」は「b = 5;」と等しいので、以下のようにならないでしょうか。

f:id:kmaebashi:20160717231819p:image

いやそんなの前者に決まってるだろう何因縁つけてんだ、と思う人は、それに続いて、「c = 5;」という代入を行った際には、こちらをイメージするのでしょうか。

f:id:kmaebashi:20160717232122p:image

それともこちらでしょうか。

f:id:kmaebashi:20160717232121p:image

箱モデルなら悩む余地はありません。こうです。

f:id:kmaebashi:20160717224406p:image

初心者が最初に扱うのは基本型だ

実のところこんなことは、クラスみたいな参照型を使うときには疑問の余地なく決まることです。Pointクラスがあったとして、「p1 = p2;」なら、p1とp2は同じオブジェクトを指しますし、「p1 = new Point(10, 20); p2 = new Point(10, 20);」なら、(たとえ座標が同じでも)違うオブジェクトを指します。

しかし、プログラミング言語の入門で、最初に例に出すのは、intみたいな基本型でしょ? 違いますか?

まず、少なくともJavaなら、intはプリミティブであり、参照型ではないので、箱モデルの方が直接的でしょう。

Rubyなら……「Rubyはあらゆるものがオブジェクトだ!」という声が聞こえてきそうですが、Fixnumの実装云々を持ち出すまでもなく、FixnumもBignumもimmutableなので、実質、値型と同様に使えます。

  1. 初心者に、参照をイメージするような「名札モデル」を教え、
  2. その上で、整数型はimmutableだからこうなる、

と教えるよりは、最初は「箱モデル」で教えるほうがなんぼか無難ではないでしょうか(別に断言はしませんが)。

箱モデルで教えていて、次に参照を教えるときはどうするかって? 「参照値が入る箱」でよいのでは。

f:id:kmaebashi:20160717235208p:image

何が言いたいのかというと

この手の「変数は箱か名札か」的な話は、変数の挙動について「既にわかっている人達」が、「どっちが自然か」ということでケンケンゴウゴウするもので、実際にその議論が初心者のためになっているかというと、全然なっていない気がするんですよ。

経験的に、ですが、たとえば私と一緒に会社の新人研修を受けた人達(のうち本当の初心者)の中で、「変数」がわからなくて数日レベルで悩んでいる人はいませんでした。たぶん最初の説明は「箱モデル」で受けたと思うのですが、それでそこが突破できるなら別にそれでいい。それに、実のところ初心者にとって「あらゆるものがオブジェクトだ!」みたいな「統一性」は、言語を学ぶ際にはそれほど大きな助けにならない。あまりにもばらばらなのは考えものですが、整数型なんてのは、最初は値として考えておいて、「あらゆるものがオブジェクト」言語なら、後から「ああこれもオブジェクトなんだ」と理解(感動?)しても特に問題にならないと思います。ていうか、たいていの人は、この順序で理解したんじゃないかなあ。

そして、学習を進めるうちには、「変数は、スタックとかstaticとか、あるいはオブジェクトのメンバとして保持され、オブジェクトそのものはヒープに確保される」「オブジェクトを指し示す値が参照である」「参照値は、(実装により間接参照になってたりするかもしれないが)まあ要するにアドレスのことだ」ぐらいには、物理的な実装に近いところまで理解できるでしょうし、その知識は無駄にはならないでしょう(メンタルモデルとして有用ですし、パフォーマンス等を考えるならなおのこと)。

「センス・オブ・プログラミング」に以下のように書いたのは、そういう意味です。

しかし――変数が「箱」であるのか「メモ用紙」であるのか、そんなくだらない話を真剣に議論してもしょうがないでしょう。たとえ話は所詮たとえ話です。「変数」が実際にどのようなものかを知りたければ、結局、「メモリ」の話を抜きにすることはできないと思います。

というわけで、プログラミングする上では、ある程度低レベルの概念を知る必要が常にある、という意味で、宣伝につなげますよ。

「変数」を知りたければ結局「メモリ」を知らなければいけないように、WebアプリケーションプログラマならHTTPを知らなければいけません。そこでこの本ですよ。

ApacheのようなWebサーバを、自分で作ることにより、Webアプリケーション開発を真に納得して行うことができるようになります。

もちろん、最初に「箱モデル」で変数を学ぶことが悪いことではないように、最初に、HTTPをわかりもせずフレームワークを使ってWebアプリを作ってみることは悪いことではありません。しかし、いずれにしても、どうせすぐ必要になる知識です。「Webサーバを作るなんてマニアックな!」と思わず、読んでみてくださいませ。

余談

なぜ日本人はオブジェクト指向をなかなか理解できないのか?:新刊ピックアップ|技術評論社

くうだらない、じいつにくうだらない。

てかさ、

オブジェクト指向では,「データ」を「オブジェクト」として扱いますので,「変数」もまた「オブジェクト」と同じものだと言えます(※2)。

これからオブジェクト指向言語を学ぶ方は,ここで紹介した「変数とはオブジェクトのこと」というイメージを胸に,学習に臨んでみてください。

「変数とはオブジェクトのこと」って、はっきり間違いだろうよ。

*1:「3a」と書いたからといって「35」(さんじゅうご)にはならないので、この原則にも説明は必要ですが……

2016-07-05 インタープリタは「翻訳」しない

[][]インタープリタは「翻訳」しない

404: Not Found - Qiita

中間言語方式 Java・.Net

(バイトコードなど)中間言語に変換してから、JavaVMなどがネイティブコードで実行する。

インタープリタ言語 PHP/Ruby/JavaScriptなど

1行ずつ、コンパイルしながら実行する。

コンパイラ(コンパイル作業)すら必要ないようにした。

■ 最終的にマシン語(機械語)に翻訳されます

……うへえ。今時こんな文章を読むことになろうとは。

当たり前ですが、JVMを含め、インタプリタネイティブコードへの変換は行いません*1

今から文章書くにはもう眠いので、昔、拙著「センス・オブ・プログラミング」に書いた文章を貼っておきます。

センス・オブ・プログラミング p.83の補足『インタープリタは「翻訳」しない』より

インタープリタは「翻訳」しない』

どうも、入門書によっては、コンパイラインタープリタについて、以下のような説明をしているものがあるようです。

コンパイラは、ソースプログラム全体を一気に機械語に翻訳して、オブジェクトプログラムを作成する。

それに対し、インタープリタは、事前に一括翻訳するのではなく、実行と同時に、1行づつ部分的に機械語に翻訳する。

はっきりさせましょう。この説明は、完璧に間違っています

インタープリタでは、ソース(または中間形式)を、インタープリタというプログラム自身が解釈しながら実行します。インタープリタが、ソースから機械語への変換を行なうことはありません。

最近のJava仮想マシンは、バイトコード機械語に変換する機能を持っています。しかしこれは「JIT(Just In Time)コンパイラ・・・・・」と呼ばれる技術であり、この部分を指してインタープリタとは呼びません。

この間違いは、私が8ビットパソコンのBASICで遊んでいた頃(もう20年近くも前になりますか---遠い目)にはあちこちで見掛けたものですが、さすがに最近は絶滅したものだと思っていました。

しかし、本書の執筆のため、本屋で各種入門書を見てみると、最近の入門書にも、上のような嘘が書かれているものがちょくちょくあるようです。これが驚異のべすとせらあ?

――困ったものです。

ここで、「これが驚異のべすとせらあ?」と書いている本は、「プログラムはなぜ動くのか 知っておきたいプログラミングの基礎知識」(矢沢久雄著)です*2。当時、「これが驚異のベストセラー!」というアオリが帯に書かれていたのです……

上記の補足を書いた「センス・オブ・プログラミング」が出版されたのが、2004年ですよもう12年も前ですよ。

だいたいプログラマーなら、インタプリタのひとつやふたつみんな作るものだろうに一度でも作ったらこんな勘違いすぐに解消されるだろうに。

はい宣伝。プログラミング言語の作り方に興味が出てきた方はこちら。

関係ないけど今一番売れてほしいのはこの本なので、これも宣伝しておきます。

Webアプリケーション開発を学ぶのに、「Webサーバを作る」という低レベルなところから攻めていこうという本です。低レベルなところから攻めるという点で、プログラミング言語の動く仕組みとかを考えるのと、関係なくはないですね。

ところで、Qittaにはトラックバックとかは飛ばせないようですが、元記事書いた方には、いったいどの本を読んで、インタプリタが「1行ずつ、コンパイルしながら実行する」という(誤った)知識を得たのか、ぜひ教えていただきたいところです。

*1:もちろんJITのような技術はありますが、これはJust In Time コンパイラであって、この部分を指してインタプリタとは呼びません。

*2:私が確認したのは1版7刷 (1刷ともに2001年

2010-12-06 「憂鬱なプログラマのためのオブジェクト指向開発講座」はどうトンデ

[]「憂鬱なプログラマのためのオブジェクト指向開発講座」はどうトンデモなのか

前回の続きです。

なお、私が持っているのは初版第10刷、正誤表がこちらにあるようです。

この人、Cで開発したことあるのかな

オブジェクト指向の教科書ではよくあることですが、この本も、Cによる開発とオブジェクト指向言語(この本の場合はC++)による開発を対比し、「C++の方がこんなにいいでしょ」という説明が随所にあります。そのこと自体は悪いことだとは思いません。

しかし、そういうことを書くなら、Cによるまともな開発についての知識が必要なんじゃないでしょうか。

p.30

クラスの宣言は一般的にヘッダファイルに記述します.C言語でのプログラミングでは,ヘッダファイルをプログラマが書く機会というのはあまり多くありませんでしたが,C++ではクラスを作るたびにヘッダファイルを書くことになります.

いや、本当に本当の入門者でない限り、Cプログラマだってヘッダファイルぐらい書きますってば。ていうか、経験を積んだCプログラマほど、.cファイルよりもヘッダファイルを大事にするものです*1

p.89

C++でもっとも一般的なクラスインスタンスの生成方法は,動的にヒーブ領域に取る,という方法です.つまり,Cでいえばmalloc()です.これを意外に思った人は多いのではないでしょうか.なぜならば,C言語でのプログラミングでは,malloc()でのデータ管理というのはとてもマイナーな方法だからです.しかし,C言語で今ひとつ脇役であった構造体が,C++でクラスに姿を変えて主役になったように,malloc()も姿を変えて主役になったのです.

何に姿を変えたのでしょうか.それはnewです.

この本の連載時の時代状況を考えても、Cにおいてもmalloc()はマイナーではなかったと思います。もちろん今でも状況によっては気楽には使えないかもしれませんが、そういう状況なら、C++のnewだって同じでしょう。構造体が「今ひとつ脇役」というのもどこの世界の話なんだか。

不完全型のような、Cにおける必須のテクニックも「なかったこと」にされています。

p.28

このようにクラスについてわかってくると,C言語のデータに対する考え方は実に楽観的で,構造体などはなんとも危うい仕組みだと思えてきます.FILE構造体なども,絶対に内部に触れてはいけないものなのですが,それを防ぐ仕組みがなにもなく,むき出しになっています.

FILEが無防備に公開されているのは、stdio.hが古いことと、マクロとかの都合によるもの(たぶん)であって、Cの言語仕様としては非公開にできるので『それを防ぐ仕組みがなにもなく』は間違いです*2

インベーダーの話

インベーダーについては前回も書いたのですが、4章のまとめ的なところでクラス図が登場しています。

p.83

f:id:kmaebashi:20101206014311p:image

関連や継承がないのは「まだ習ってないところ」だからです。それはさておき、

  • この「戦車」の右に移動、左に移動というメソッドは、誰が呼んでくれるんでしょうか。「インベーダー」も同様。
  • 細かいことのようですが、インベーダーミサイルや戦車ミサイルに「縦位置」だけあって横位置がないのはなぜなんでしょうか(単なる誤植?)。

クラス図といえばこの章には以下のようなクラス図もあって、

p.72

f:id:kmaebashi:20101206014312p:image

「生徒」に「平均点算出」とかのメソッドを付けてしまうのは、私は設計としておかしいと思うのですが、それはまあいいとして、「テスト」の属性に挙げられている「点数」は何でしょうか。そもそもこの「テスト」のインスタンスは、1回のテストの実施につき1個なのでしょうか。1回のテストの実施の中の、個別の生徒で1個なのでしょうか。

末尾スペース除去可能文字列クラス

p.134から、「プログラムレベルでの継承の例」として、文字列クラスを継承した「末尾スペース除去可能文字列クラス」を作っています(ある程度心得がある人なら、こう聞いただけで頭がクラクラしてくるでしょうが)。

ちょっと考えればわかることですが、末尾スペース除去の対象としたい文字列は、たとえばファイルから読むなりGUIから入力されたりしたもので、そのクラスは「文字列」です。「末尾スペース除去可能文字列クラス」を作ったって、一度生成されたインスタンスのクラスが変わることはないのですから役に立ちません*3

実は拙著「Java謎+落とし穴徹底解明」の「3.5.2 ケーススタディ――Point2Dに機能追加」という項は、執筆終盤にこの本を見て、「こんなアホウなことを考える人がいるんだ」とあわててねじ込んだ項です。

この本では、さらにその後、オーバーライドの例として、既存の文字列クラスのsetterをオーバーライドして「末尾のスペースが必ず削除される文字列」クラスも作っています*4

これはとても素晴らしいことです.なぜなら,「文字列」クラスをカスタマイズしたにもかかわらず,その文字列クラスにはまったく手を触れる必要がないのです.従来の方法では,モジュールをカスタマイズすると,今までそのモジュールを使用していた部分のテストは一からやり直しでした.そしてそのモジュールを使用している他のプログラムのソースには,旧バージョンのモジュールが生き続けることになりがちです.これは,同時に複数のバージョンのモジュールをメンテナンスするということになり,保守の面で深刻な問題になっていました.

しかしオブジェクト指向では,そのようなことを気に止めることすら時間の無駄です.

継承を使用すれば,過去のことを何も考えずにばりばりカスタマイズしてもまったく問題がありません.なぜなら,どんなに派手なカスタマイズをしても,その基底クラスのモジュールのソースは1バイトも変わっていないからです.

基底クラスのモジュールのソースは1バイトも変わっていないかもしれませんが、こんなわけのわからない文字列のインスタンスを、たとえば引数とかで渡された側はたまったものではないでしょう。もっともC++の場合、インスタンスの(ポインタでない)実体を扱えるという変わった言語なので、引数で渡される際には文字列になっており問題ないのかもしれませんが、そういうことなら、そもそもsetterをオーバーライドすること自体が役に立ちません(C++ではポリモルフィズムはポインタ経由でなければ動かないので)。

ところで、setterをオーバーライドして文字列末尾の空白を削除するというのなら、派生側のsetterは、どのようにして基底クラスが保持している文字列のメンバ変数にアクセスするのでしょうか。基底クラスがメンバ変数をpublicもしくはprotectedにしているのでない限り直接はアクセスできませんから*5、アクセスするには基底クラス側のsetterを呼ぶ必要があります。そのためには「基底クラス名::メソッド名()」とか書く必要があるのですが、この本の中には、これに関する説明はありません。これじゃわからないだろう、と思ったら、やっぱりわからない方がいらっしゃったようです。

http://www.seshop.com/book/qa/14479/thread

この場合は、派生クラス側でスペースを除去し、そこでできた文字列を、基底クラス側の「文字列設定」メソッドを呼んで基底クラスの「文字列」というデータに設定します。ただ、わかりづらいことも否めないと思いますので、今後の検討材料とさせていただきたいと思います。

だってさ。

時代の問題?

Web上のこの本の批判や、前回の記事のブックマークコメントで「この本は古いからしょうがないよね」的な意見が散見されました。確かにこの本には、「当時はみんなこんな(おかしな)こと言ってたねえ」という点もあります。「メンバ関数は必ずvirtual、publicでなくてもprotectedにしとけ」とか。

でも、上で挙げたようなことは単純に間違いであり、「古いからしょうがない」と擁護できるような性質のものではないのではないでしょうか。

ところで、「昔のオブジェクト指向では常識だった」といったことを考えるに、この業界内でのプログラミングの方法論がA→B→Cと遷移したとき、BがAよりはずいぶんマシであり、でもCはBよりずっとよい、というのなら、方法論は進化を続けているということです。Bの時点でCを思いついていた人がいなかったのはしょうがない。

でも、Bが、Aと比べても特にいいことないよね、ということなら、B時点でBを称揚した人は単に間違っていたということです。そして、そういう人が業界内にたくさんいたようなら、今あるCも、疑いの目で見る必要があるように思います。オブジェクト指向では、このあたり、どうなのでしょうか。

その他細かいこと

p.362

さて,それらを学習する前に一つ知っておかなければならないことがあります.これは「実体」です.

C言語では,関数に変数を渡すときは,値渡しとポインタ渡しの2種類の方法がありました.C++では,さらにもう一つの方法として,実体渡しという方法があります.

実体渡しは,値渡しとポインタ渡しの中間のような方法です.

まず、従来からある「値渡し」と,ここで学ぶ「実体渡し」の違いについて考えてみましょう.値渡しでは,関数に渡されるのは,あくまでもその変数の「コピー」です.ですから,渡されたデータをいくら書き換えても,元のデータには全く影響がありません.しかし、実体渡しの場合はその「変数そのもの」,つまり実体が渡ります.これを書き換えたら,元データにはもちろん影響します.それは,元データそのものなのですから.

私の知る限りにおいて、「実体渡し」という言葉をこういう意味(つまり「参照渡し」の意味)で使っている例は、この本と、この本の読書ノート的なWeb記事でしか見たことがありません。たいてい、「実体渡し」と言えば「値渡し」の方を意味するんじゃないでしょうか。

言っていることはわかります。私などは「ポインタじゃなくて実体を渡す」という意味で「実体渡し」という言葉を使ってきたわけですが*6、「コピーじゃなくて実体を渡す」という意味で「実体渡し」という言葉を使って悪いことはないでしょう(どうせきちんと定義された用語ではないので)。でも、あまり一般的な使い方とは思えません。私の周りだけかもしれませんが、Cプログラマにとって「ポインタじゃなくて実体」という言い回しはかなりされると思っていて、わざわざ混乱を招くような用語を使う意味はあるのでしょうか。この本の中でも、p.173には、集約の実装方法として

通常の関連をC++で実装する際には,メンバにそのクラスのポインタを持つことによって実装する,ということは以前学習した通りです.それに対して集約の実装は,メンバにクラスの実体を持ちます.ポインタではなく,実体なのです(LIST7-4).

という記述もあって、どう解釈したものかと思っているのですが。

また、グローバル変数について以下のような記述があって、

p.207

C言語のプログラミングにおいて,一般的に「グローバル変数は基本的に使ってはいけないもの」とされています.ほとんどの人はそれに反対はしないでしょう.

(中略)

この問題をオブジェクト指向的視点から見ると,これは「解決できない問題」というよりは,「解決しようとせずにほったらかしになっている問題」というようにしか見えないといっても過言ではありません.なぜならば,オブジェクト指向の概念に基づいてプログラミングを行うと,グローバル変数に似た構造の変数が無数に登場しますし,そしてそれを管理することが当然になっているからです.

それはなんでしょうか.(もうおわかりだと思いますが)それはクラスのデータメンバです.

なぜグローバル変数が他の変数と違って特別扱いされるかというと,それは関数が終わっても消えてなくならず,その結果,複数の関数から読み書きが可能だからだといえるでしょう.そしてこれはまさに,クラスの中でのデータメンバと同じ性質です.なぜならデータメンバは,複数のメンバ関数から読み書きされるというのが基本的な性質であり,それがメンバ関数内で宣言される通常の変数ともっとも違う点だからです.もし,グローバル変数という存在が持っているそのような性質そのものに問題があるのだったら,それはクラスの構造,さらにはオブジェクト指向という概念自体が否定されることになってしまいます.

クラスのデータメンバは、確かに複数のメンバ関数から読み書きされ、クラスが大きくなりすぎればグローバル変数的な様相を呈します。しかし、通常のデータメンバはインスタンス単位です。グローバル変数と対比すべきはstaticなデータメンバで、この本ではp.156からその説明が出てきます――が、ここではグローバル変数の話はカケラも出てきません。

どうもこの人、システムに対し1個だけ存在するのか複数存在するのかとか、多重度とか、そういったところの扱いがずいぶん無頓着に感じます。上のp.72のクラス図で、「テスト」が何に対してひとつなのかが変なことになっているのもその現れじゃないかなあ。

*1:p.255には『これはC言語の頃からの原則ですが,「関数のプロトタイプ宣言やクラスなどの宣言」は.hファイル,「その実際の実装」は.c,.cppファイルになります』という記述もあるのですが。

*2:少なくともFILE構造体は一般ユーザはポインタ経由でしか使わないので。

*3:いったんインスタンスまで作り直してしまうなら別ですが。

*4:追記:「末尾スペース除去可能文字列クラス」はどう考えても役に立ちませんが、「末尾のスペースが必ず削除される文字列」クラスのほうは、使い道がなくもない気はします。よい設計かどうかはさておき。

*5:この本でも、メンバ変数はprivateにすることを勧めています。

*6:本とかでは「値渡し」と書いてきたと思いますが

2010-12-02 「憂鬱なプログラマのためのオブジェクト指向開発講座」はどこがどう

[]「憂鬱なプログラマのためのオブジェクト指向開発講座」はどこがどうダメなのか

『見習いプログラマ(中略)10冊』を書いた理由と、更に読んで欲しい5冊 : ソースコードは飲み物です。

(3)更に読んで欲しい5冊

C言語ポインタ完全制覇 (標準プログラマーズライブラリ) C言語ポインタ完全制覇 (標準プログラマーズライブラリ)

前回書くべき書籍なのに、すっかり忘れていました。。。 超有名本ですよね。

C言語使わない人も、是非読むべきです。

あわせて http://sakurai.sumomo.ne.jp/page/c_pointer も見るべき。

ちなみに、ポインタについての凄くわかりやすい説明を前どこかで見ましたので、うろ覚えながら書きます。

『ポインタって何?』『2chのレスと、そのレスへの安価』

ご紹介いただきありがとうございます。(_o_)

……それはさておき、「憂鬱なプログラマのためのオブジェクト指向開発講座」という本についてですが。

C++をメインに使われている方には、非常に不評のようですね。

確かにポリモーフィズムの使い所やら怪しい箇所は何点かあります。

ですが、僕がオブジェクト指向について学び始めた頃に読み、

当時凄く悩んだことに答えてくれた本でした。

シューティングゲームで、自機がミサイルを打ち、敵機に当たった際の判定について考えて下さい。

ミサイルの当たり判定をするメソッドがあるべきなのは、どのクラスでしょう?

ミサイルが敵機に当たった際には、ミサイルと敵機が消滅しますが、ミサイル・敵機を消滅させるメソッドはがあるべきなのは、どのクラスでしょう?

もちろん、プログラムを書き慣れている人なら、『何当たり前のこと言ってんだよ』って思われるかもしれません。

でも学び始めの人は(いや、ある程度慣れてきていても)クラスの責務について迷うことは多いはずです。

その考え方をゲームを題材としてわかりやすく学べた書として、お勧めしました。

結城浩さんが「例は嘘をつかない」と言っておられるとおり、何らかの実例を用いるのは、考え方の正当性を確認するには大変重要です。よって、わけのわからないたとえ話や抽象的な話ばかりではなく、実際の例を用いて説明する、というのは、本の書き方として、よい方法だと思います。

――それが正しいのであれば。

上記結城浩さんの文章からの引用:

実際に動くプログラムを作るというのは、ソースコードという具体例を使って、コンピュータという相手に自分の理解した内容を説明しているようなものだ。きちんと動作したら自分は理解しているし、きちんと動作しなかったら自分は理解していないことになる。

憂鬱本の場合、例を使って説明するのはよいのですが、それにより、そもそも考え方が間違っているということを晒してしまっている、というのが実態です。

どこがどうおかしいのか、id:JavaBlack 氏は具体的なことはいつまで経っても書いてくれないし*1いつかまとめようと思いながら月日が過ぎてしまいました。今もまた時間の余裕がないので、かつて書いたものを貼ってお茶を濁しておきます。

以前、私が自分のWebページで書いたものがこちら。

http://kmaebashi.com/programmer/object/naze.html

手元に「憂鬱なプログラマのためのオブジェクト指向開発講座」という本があるんですが、この本、例題としてインベーダーゲームを取り上げています。

この本によれば、仕様から「名詞」を抽出し、それに対する「操作」を抽出して、あるクラスAがあるクラスBに対して「操作」を行うのであれば、 AからBに「関連」を付けるのだそうです。そして、「関連」はポインタで表現します。関連先が複数の場合は、ポインタの配列を使えばよいそうです。

まあ、大筋で間違ってはいないでしょうが、例題として、インベーダーゲームで、ビーム砲※1 の発射したビームからインベーダーに関連を張ってしまっているのは明らかに変でしょう。

ビームが発射された瞬間、55匹のインベーダーに対してポインタを張るのでしょうか? もし、UFOが出現したり、別のビームがインベーダーを破壊したら ※2、そのポインタ配列をいちいちメンテナンスするのでしょうか?

まあ、この本には「一時的な関連」という説明もありますが(p.109)、 p.143には「"プログラムコード上ではミサイルクラスはそのメンバとしてインベーダークラスへのポインタを「持っている」必要はあります」"と明記してあるし… ※3

それより前に、ほぼ同内容のことをこちらにも書いています。

http://www.ogis-ri.co.jp/otc/hiroba/oosquare-ml/Archive/200204.month/2595.html

私も、評判がいいので、この本をamazonで取り寄せて読んだんですが。

読みやすいのは確かです。

でも、この本は、やっぱり「わかったような気にさせる」本でしかないと

思います。実作業への導入を考慮して「まじめに読む」には、正直、

内容がひどすぎます。

「文字列」を継承して「末尾スペース除去可能文字列」を作るなんてのは、

オブジェクト指向をかじりかけの初心者さんがやってしまいそうな「間違い」

ですし。

「ミサイルがインベーダーを持っている」なんて主張する人は、おそらく

世の中にはひとりもいないだろうし。

だいたい、ビームを発射した瞬間に、ビームが全インベーダへのポインタを

持つようなプログラムなんて普通書かないでしょう。あの本の通りに書いて

いったら、インベーダーはできないですよ。

まだまだ言いたいことは山ほどあるんですが、眠いのでこの辺で。

某プライベートなMLに書いたものがこちら。

Date: Sat, 08 Sep 2001 21:18:32 +0900

Subject: 憂鬱なプログラマのためのオブジェクト指向開発講座

「憂鬱なプログラマのためのオブジェクト指向開発講座」という本が

2chでえらく評判がよいので、amazon.co.jpで購入して今読んでいるの

ですが。

うーん...

この手の本は、「従来の技法を不当に貶めることで自分の支持する

方法論を推奨する」という傾向がよく見られるものですが、この本も、

もろにそういう本ですし。

# まっとうな設計者なら、Cだって、システム全体をモジュールに

# 分割して限られたインタフェースのみで操作するようにする、

# ぐらいのことはやります。

「文字列」を継承して、「末尾スペース除去」メソッドを持った

「末尾スペース除去可能文字列」を作るのがよい、なんて話を読んだ

あたりで、いい加減放り出したくなっています。

「クラスの関連にはis a関連とhas a関連がある」というのを「伝説」

として否定しているけれど、

| 例えば、インベーダーゲームで、ミサイルはインベーダーとの間で

| 当たり判定をします。よってこの2つのクラス間には関連があり、

| プログラムコード上ではミサイルクラスはそのメンバとしてインベーダー

| クラスへのポインタを「持っている」必要はあります。しかし、

| 分析的視点から考えると、「ミサイルがインベーダーを持っている」

| ということは現実から離れすぎています。

ミサイルがインベーダーへのポインタを持っている、ということを

has aの関係だと主張している人が世の中に存在すると思うことの方が、

現実から離れすぎていると思うんだけどなあ。

# だいたい、この本のインベーダーゲームの分析では、実際のゲームは

# 作れないと思う。普通、ミサイルがインベーダーへのポインタを持つか?

はっきり言って、私には「トンデモ」にしか見えませんが...

他に読んだ方がいれば、感想をお聞きしたいです。

うーん、ここも同じようなことしか書いてないですねえ。他にもいっぱいおかしなこと書いてるんですけど。Java読書会BOFのMLにはちょっと違うことを書いたはずですが、今アーカイブが見えません。

だいたい、魔方陣のプログラムをCで書くとこうなるぞ、と言ってる時点で、この本がトンデモでしかないというのはどう考えても明らかだと思うんですが。

http://kmaebashi.com/programmer/object/list1.txt

今でこそ、憂鬱本の批判はかなり目にするようになりましたが、私がこれを購入した頃(「Java謎+落とし穴〜」の執筆途中だったはず)は、ネットには、私の見た範囲では絶賛の声しかありませんでした*2

世間の評判なんてものがいかに当てにならないかというひとつの例ではないかと。

(追記)

これだと「疑りぶかい〜」に書いてることから追加情報ほとんどないなあ。今週末にはもっと書く、と自分にプレッシャーをかけておきます。

*1:ていうか具体的な理由を挙げず批判するのは単なる中傷です。

*2:「俺は昔から指摘してたんだぜ」という自慢に見えます? まあそうかも。

2010-01-14

[][][]例外処理について、私はこう思う

他人の意見の翻訳ばかりでもアレなので。

例外処理機構についての私の考え方は、Diksamがそうなっているように、

  1. 例外処理機構自体は必要
  2. 検査例外も必要

というものです。

例外処理自体の有用性について

Joel Spolsky氏の記事の翻訳の感想でも書いたのですが、例外処理機構は必要だと思います。「戻り値でちまちまエラーケースを上位に戻していってうまくいくと思えるほど、私は(自分を含む)プログラマを信用していない」ためです。

Joel氏は

例外を上げるかもしれない関数を呼び、それをその場でcatchしないときはいつも、あなたは、データを整合性のない状態にしたまま突然中断された関数や、あなたが考慮しなかった別のコードの実行経路による驚くようなバグが発生する機会を作り出しているのだ。

と書いていますが、こういうケースは実際に存在します。たとえば(これは「プログラミング言語を作る」に書いた例ですが)何らかのツリー構造を作っているとして、

  node.childlen = new Node[5];
  for (i = 0; i < 5; i++) {
      node.children[i] = new Node();
  }

子のノードを5個追加しようとしていますが、これが3個目で失敗した場合、3回目の「new Node()」で例外が発生することになります。この例では、親ノードに対し「node.childlen = new Node[5];」を先に実行してしまっていますから、node.children[3]以降がnullになることになります。データ構造上、これを許さないケースは多いでしょう。子がいないなら、node.childrenはnullにするとか、長さ0の配列を割り当てておくのが普通です。たとえば以下のように書く必要があるはずです。

  Node[] temp = new Node[5];
  for (i = 0; i < 5; i++) {
      temp[i] = new Node();
  }
  // node.chlidrenを最後まで触らないので、
  // node.childrenが子がいないことを示す状態で初期化されていれば、
  // 例外が発生しても、データの不整合を起こすことはない。
  node.childlen = temp;

でも、例外処理機構を使っていると、こういうところの検証がえらく難しい、というのがJoel氏の指摘なのだと思います。

しかし、じゃあリターンコードでステータスをちまちま上位に返していけばこういうところが万全になるかというと、私にはちょっとそうは思えない。化数やメソッドの呼び出し階層を、「上司が部下に仕事を依頼する」ことにたとえるとするならば、リターンステータスで異常を返すことは、部下が上司に異常を報告していることを意味します。それは結構なのですが、上司は、いとも簡単にそれをもみ消すことができる。しかも、リターンステータスをいちいちチェックしなければ「自動的にもみ消す」わけですから、これはデフォルトがもみ消すほうに振ってある状態です。こういうやり方では、下っ端が、たとえば原子炉の放射能漏れに気付いたりとかしても、上司から上司へ報告しているうちにいつの間にかもみ消されてしまうのが目に見えています。とても推奨できない。

例外処理機構があれば、(検査例外の有無に関わらず)例外を「もみ消す」時には、どこかの階層で、明示的に「もみ消す」コードを書かなければなりません。例外をもみ消すのなら、陽に、その階層の責任においてやれよ、ということかと思います。もっとも、Javaでも、Exceptionを捕まえる空のcatch節を書いて、(Error以外の)あらゆる例外をもみ消すコードは山ほど見ましたから、いっそ例外クラスはnewされた時点で処理系レベルで強制的にログぐらいは吐いたほうがよいのかもしれませんね。

検査例外の話

検査例外と言うか、Javaで言うところのExceptionとRuntimeExceptionの使い分けの話になりますが。

これについては、私はかつて「Java謎+落とし穴徹底解明」で以下のようなことを書きました。

  1. Errorは、「回復が難しいか不可能」なところに使えとJLSに書いてある。
  2. RuntimeExceptionは、「どこでも発生し得るからいちいちthrowsに書いてはいられないが、アプリケーションがcatchする可能性がある例外」、ぶっちゃけバグだ*1
  3. それ以外のExceptionは、「ユーザの誤操作など、プログラムにバグがなくても発生する可能性があり、かつそれに対して必ずプログラムがきちんと対応しなければならない場合に使うべき」

この考えは今も変わっていません。

おそらく同じようなことを、赤間さん*2は以下のように書いておられます。

.NETとJavaの例外処理の違い – とあるコンサルタントのつぶやき

Java には検査例外と実行時例外と呼ばれる 2 種類の例外が存在しており、言語仕様として業務エラーを例外(検査例外)として取り扱える仕組みを持っている

  • メソッドシグネチャとして throws 句を書かないとダメ。
  • 呼び出し側で try-catch を書かないとダメ。

この 2 つの特徴は、要するにこの検査例外が、「必ず処理ルートとして考慮しなくちゃいけないケースである」ということを意味しており、この特徴はそのまま業務エラーに当てはまります。つまり、業務エラーとはそもそもどのようなものだったのかというと、

メソッド側(上の例でいうと BC 側)では、インタフェース仕様(メソッド仕様)の一部として定義しなければならないもの。

呼び出し側(上の例でいうと UI 側)では、必ず後処理してメッセージなどを表示しなければならないもの。

でした。CLR 系言語(C# や VB)では、言語仕様としてこのような業務エラーを体系的に取り扱える仕組みがないため、やむなく enum 値や構造体クラスなどを使って業務エラーを表現していたのですが、Java の場合には、検査例外を使えば言語仕様として業務エラーを体系的に取り扱える、ということになります。

これについてはまったく同意で、まさにわが意を得たり! と思ったのですが――

JavaのInteger.parseInt()について以下のような記述があって、

さらにつぶやいておくと、このことからわかるように、基本的な考え方として、汎用クラスライブラリの戻り値を設計する際には、

「業務エラーかどうかが状況次第で変わるものについては、かたっぱしからアプリケーションエラーに倒して設計しておくべき。」

なのだと思います。この点に関しては Java のクラスライブラリには問題があって、特に I/O 系の業務エラー(RemoteException や SQLException)が片っ端から検査例外として実装されてしまっているのはかなり困りものなのですよね....。(実装コードが非常に書きにくくなるため。これは Java をいじっているときの不満事項の一つでした。) 検査例外が .NET にないのは悔しいものの、不適切な検査例外の利用は逆にデメリットにもなるので、この辺のトレードオフが悩ましいところです。

これには首をひねったのでした(私は、5冊しかない著書の実に2冊において、「NumberFormatExceptionが検査例外じゃないのはどう考えても設計ミスだろ、ということを書いていますので)*3

「安全側に倒す」なら、例外をもみ消してはいけない。前述の通り、例外をもみ消すなら、陽に、その階層の責任においてやれよ、と私は思いますので。

それよりも

いやさ、JavaとかC#とか、ついでにDiksamとかの例外処理機構の使いにくさは、検査例外云々よりもむしろ、catch節の書きにくさにあると思うのですが…… 例外A, B, Cが飛んでくる可能性があって、どのケースでもとりあえずログは吐きたい、かつCのケースだけ特別な処理を書きたい、という場合、現状のtry catchの構文だと、ログを吐くコードを3つのcatch節にコピペする必要があります*4。面倒だからExceptionをcatchして、catch節の中でinstanceofで処理を分けたくなります。このへんをうまく書ける構文があれば、Diksamにも採用したいと思うのですけれど。

書ききれなかった

検査例外を不要とする主張に対する反論として書きたいことがあったのですが*5、眠いのでギブアップです。数日中に書きます。

あと、まあ、あれだ。

宣伝ですが、ぜひこちらも。

*1:というわけでDiksamには、RuntimeExceptionはありませんが、BugExceptionというクラスを処理系が提供しています。これは本来catchを禁止すべきだと思うのですが、アプレットのバグでWebブラウザが死んだり、サーブレットのバグでサーブレットコンテナが死んだりしては困るのでcatch可能としています。

*2:.NETに関係すれば絶対にこの方の本を読むことになります。

*3:それはそれとして、「アプリケーションエラー」といえば一般には「業務エラー」を指すのではないでしょうか。システムエラーとアプリケーションエラーの違いがよくわかりません。.NET Frameworkにおいてはいつの間にかApplicationExceptionの継承が非推奨になっていますが、Diksamでは、ApplicationExceptionクラスは存在し、かつそれは検査例外です。

*4:なお、例外を投げる側の想定と、それを使う側の想定はたいてい食い違うので、例外のクラス階層はcatchする側ではあんまり役に立ちません。

*5:や、たいしたことじゃないです。今まで書いているのと同じことです。