C#のリファレンスが間違ってる

Cの三項演算子は if - else の構文糖衣みたいなものだったが、型推論のある C++-1x や C# では変数の初期値選択に使用できるので構文糖衣以上の意味がある。C++ では式の値にならないほうの部分式は評価されないが、C# でも同じだったろうかと念のため言語リファレンスを見たら、やはり無効となる部分式は評価されないということで安心した。

それはいいのだが、後ろの方を見ると、結合が右からと書いてあるのに、例に挙がっている式が左からの結合になっている。

条件演算子の結合規則は右から左になります。 a ? b : c ? d : e という式は、a ? b : (c ? d : e) ではなく、(a ? b : c) ? d : e と評価されます。

逆だよなぁ、これだと else if チェーンみたいなのが書けないよな、と思って英語版を見ると、正しく書かれている。

The conditional operator is right-associative. The expression a ? b : c ? d : e is evaluated as a ? b : (c ? d : e), not as (a ? b : c) ? d : e.

正しくはこうだろう。

「条件演算子の結合規則は右から左になります。 a ? b : c ? d : e という式は a ? b : (c ? d : e) と評価され、(a ? b : c) ? d : e とはなりません。 」

誤訳なのか英語版の修正に日本語版が追いついていないのかわからないが、こういう細かい間違いもリファレンスでは問題だろう。

GitHubに上がっているので勝手に直してプルリク出せばいいという話ではあるが、おじさんはまだGitHubのアカウントを持っていないのである。ITエンジニアとしてそれでいいのかという話でもあるが、歳をとるとそういうしょうもないことが面倒なのである。こういう面では歳はとりたくないものである。

KISSは難しい

風邪ひいた。熱出た。仕事休んだ。熱下がった。なんか書く。ごめん。


耳学問というのか、目学問というのか、アメリカの教授やコンサルタントが書いたプログラミングの教科書を読んでいると、エレガントなプログラミングに必要なベストプラクティスがいろいろと書かれている。そして優れた教科書には、なぜその方法がベストと言えるのかという理由や、ほかの方法を試していたのだけれど最終的にそこへ至った経緯などが書かれていて、説明が腑に落ちる。

そしてそれを実践の場に持ち込むわけだが、なかなかにうまくいかない。まあそうだろう。実践とはそういうものだ。ただ、日本のソフトウェア業界でのベストプラクティスを模索しているうちに、教科書が想定している世界とは何か違った本質があるような感じもしている。そういう、他所から導入したベストプラクティスを身近な現場の文化に落とし込む咀嚼作業というのは大体が愉快なものなのだけれど、一点、KISS原則というやつに関しては、ちょっと複雑な思いがある。

KISSというのは、自分が聞いたところでは "Keeep it SIMPLE, stupid!" みたいな表現が最初だったのだけれど、より穏やかに言うと "Keep it short and simple" となるらしい。まあ、言わんとすることは同じだろう。この言葉が登場したころクリントン政権の時代だったとか、そんなあたりの理由で表現が変わったりしたんだろう。 "It's the economy, stupid" 「経済だっつってんだろ」みたいなスローガンがホワイトハウスにあって、流行語だったらしい。

で、KISSのそばにはDRYなんてのもあって、こちらは "Don't repeat yourself" のアクロニムとされる。そうだよね、再利用して手間を最小化しなきゃITじゃないよね、と納得する。ところが、実際にKISSだのDRYだのをやってみると、意外に難しいということがわかる。オブジェクト指向プログラミングという概念自体が、歴史的経緯もあって考案された当初とはかなり離れたところに流れ着いてしまった、というような事情もあって、コードをシンプルにしたはいいが、却って面倒なことになって周囲がドン引き、みたいな経験も一度ならずあった。

共通処理のまとめあげだとか、複雑な処理を抽象化して短いコードの中に畳み込んだりすると、確かに一見して短くてシンプルな、要するにエレガントなコードにはなるんだが、そこに浮世の事情を反映した仕様変更なんかがやってくると、雑多なコードなら1行で済んだと思えるような変更が、処理の抽象化が裏目に出てしまい、逆に複数のファイルにわたる注意深い変更になってしまったりする。それを、初期のコードを書いた人間と別の人がやったりすると、事情はもっと悲惨なことになる。

それは、要求が場当たり的で本質的じゃないからなんですよ、既存コードのここを変えたら、そんな変更は必要ないし潜在バグも取れますよ、という意見を言う。すると、そんな根本部分を変えたらテスト項目数が爆発するからやめてくれ、という話になる。まあ、わかる。いまだにテストの自動化をしていなかったからそんなことになるんですよ、などというようなことを言っても始まらない。納期は迫っている。

で、そういう場当たり的な変更要求が、KISSだのDRYだのとは無縁の、ひどいコピペコードの変更に行き当たる場合、そのコードを書いた人が愚直に "repeat himself" してくれたおかげで、今回の変更対象を担当しているコードだけをちょいちょいと直せば変更が完了し、他に影響しない。クソ長いコンチクショウなファンクションなのだが、他のモジュールをほとんど呼んでいなくて、条件分岐の全てはそのコードに盛り込まれているので、少なくともそのクソが書かれたソースファイルだけを読めば変更の主要な影響確認が完了する。

そんなのは、関数や変数の定義を簡単に追える開発環境があれば何も苦にならないじゃないか、という話は確かにある。そうだよね、その通りだよ。けれども、ツール選択というのはチーム全体に影響するものであって、独りよがりにツールを導入しても、他が追随できない。そこを根回ししてチーム全体の環境を改善するのがプロの仕事、ではあるんだけど、そういう権限がいつもあるとは限らないし、権限があってもそれはそれで大仕事になってしまう。

こういう目に何度か遭うと、ああ、シンプルってなんだろう、とか考えてしまう。大学などで専門にソフトウェアを研究したことのある人間にとっては、あるいはそういう人の書いた書籍を読み込んだ人間にとっては、シンプルさとはつまりエレガントさであり、短くかつ何も妥協しない美しさを指す。けれども、そういうエレガントさというのは、そこに至る背景を知り尽くした人間にしか到達できない領域でもある。

悲しいかな、日本のIT業界というのは土木や建築の人事モデルを導入していて、なおかつもっとひどいシステムになっている。アメリカ人の書くプログラミングの教科書を自分でも書けるようなレベルの技術者は、自分で会社を興すなりなんなりして、そういう組織から抜け出ていくので、日本の標準的なIT業界は、柵から逃げ出せなかった羊たちで充満している。

そういう羊たちがコーディングするとき、既存コードのエレガントさというのは、実は非常に危険な障壁となる。コピペだらけで愚直なコードは、精密に修正しようとすれば頭が痛くなるが、顧客が求める安易な変更要求に対し安直に応じるためには、逆にエレガントな整然さを持っている。ブール代数とかグラフ理論とかを知らなくても、ある程度の水準の教養で簡単に理解できる。ある人が言っていた言葉を思い出す。「わかりやすいということは、質が悪いということとは違うのです」と。

コピペ編集の冗長なコードなら、Hello worldから3ヶ月目のヒヨっ子にも理解できる。そういうコードを汚いと蔑むのは結構だが、この「ヒヨっ子にも理解できる」という側面は、ある意味では美しく、エレガントでもある。冗長なデータをエントロピー圧縮してコンパクトにまとめるのはある種の快感なのだけれども、無圧縮データというのは部分編集でも差分抽出でも優れている。正しい冗長さというのは、それ自身がエレガントな構造なのだ。

まだ枯れていない仕様だとか、アジャイルに必要な下地がそろっていない既存コードだとかを、KISSとかDRYとか言ってうっかり "short and simple" にしようとする行為は、ある意味、アンチパターンのひとつである「早すぎる最適化」になるんじゃないか。そういうことを思うようになった。

Cは、職人向きの難しい言語とされるが、歴史的な視点があれば、あれでも当時のマクロアセンブラFORTRAN(あえて大文字で書く)に比べると、非常にエレガントな言語だったというのがわかる。そして、そのエレガントなCが引き起こしたソフトウェアクライシスに対して、Stroustrupさんが立ち上がってC++を設計した意義も意気も理解できる。が、C++9xのなれの果てであるC++11なんかを見て思う。エレガントにしようとしすぎて、逆にエレガントさを失ったね、と。Pythonに後半で追加された仕様なんかもそういう傾向はある。

Cは、とにかく人間が主でコンパイラが従であり、このあたりははっきりとしていた。一応文法エラーは出すが、意味論はすべて人間が定義し、コンパイラは警告は出しても最終的にはコーディングする人間に従っていた。一方で、C++の理念をより推し進めたJavaC#では、コーディングでは間違いを犯す人間がコンパイラの許す範囲に従うべきということになり、人間の仕事はプログラムの定義をいかに正確にコンパイラに伝えるか、という一点に収束していく。

まあ、現場の運用ではそこまできれいにはいかないものの、基本思想にはそういう主従関係がはっきりしている。で、C++は先駆者の悲哀というのか、時代的な制約もあって、あれもこれもできるという学者の尊厳を尊重する方向へと進んでいき、結果として、一方では最強の言語でありながら、けれども一方では化け物を生み出しやすい黒魔法へと成長していく。エレガントに書くプラクティスは整備されているとはいえ、それを新入りに説明するためには、カレッジに講座のひとつでも寄付し、そこに最低2年くらいぶち込んでやる必要がある。

シンプルとはなんだろう。

ひとつには、エレガントというのがあるのだろうけれども、人々が求めているシンプルさというのは、場合によっては愚直さなのかもしれない。日本語の文脈では「愚直」というものが美談の対象になりやすいが、要は「馬鹿正直」であり、突き詰めれば「馬鹿」である。それが、シンプルという概念が持つ、本質の一極なのだろう。

マクスウェル方程式というのがあって、特殊相対論の母体になったものなのだけれども、これも先人たちが19世紀末の高度な技術に裏付けられた最新物理の成果と格闘しているときには、非常に汚らしい数式になっていた。現代の大学では、ベクトル場と微分系の連立微分方程式として非常にエレガントに書かれており、とてもシンプルになっている。遠隔力すら前提としない局所変数で解ける方程式になっていて、概念的にも美しい。テンソルを使うと、偏微分方程式は2個に集約できるらしい。もっと高度な代数系を設計すれば、たった一つの小さな方程式として書けるかもしれない。

が、そこまで行ってしまうと、そのシンプルな方程式を、果たして私たちは操作できるんだろうか、という疑いもある。テンソルが子供のおもちゃになってしまうような理論武装が必要とされるのに、シンプルなひとつの方程式になった、などと喜んでしまっていいのだろうか。

逆に、ベクトルを使わずに(x, y, z)の座標を使った成分表示でマクスウェル方程式を書き下すと、方程式はさらに増えてしまう。けれども、微分積分や極限みたいな概念は必要だとしても、その愚直で汚い成分形の方程式は、忍耐強い高校生でも扱える。そこにはベルトル場だとかテンソル場だとかの概念がまだ持ち込まれていないので、入り口に立つまでの準備期間は短くて済む。実際の数式操作は面倒になるけれども、そこに書かれていることの理解だけでいえば簡単になる。

面倒な作業が嫌いで抽象概念が好きな人間にとっては、シンプルでエレガントなコードが美しく扱いやすいとしても、逆の性質を持った人、面倒な抽象概念は嫌いだけど単純作業が好きな人間にとっては、冗長なコピペコードこそが美しく扱いやすいのだろう。これは、どうしたもんか。

「鳥を描いてください」という要求は、実は難しい。「鳥」というのは抽象クラスだ。"class" という英単語について日本人は学校のクラスだとか、社会的階級しか思い浮かべないが、ここでいうclassというのは「分類」とか「グループ」のことで、"object" というのは実際の「モノ」のことだ。目の前を飛んでいる一羽一羽のあれが "object instance" だとすると、そこに共通する何らかの性質でまとめ上げた分類項目が "class" になる。

モノが具体的な対象で、分類というのは抽象的な概念だ。「カラス」とか「スズメ」というのはまだ抽象度の低い "concrete class" つまり「具体的な分類」で、「鳥」というのはカラスとかスズメとかいう具体的な分類項目をさらに分類した上位分類 "meta class" で、別の言い方をすれば "abstruct class" つまり「抽象的な分類」だ。

こういう、クラスとかメタクラスとかを抽出して最小限の記述でうまくやろうぜというのがオブジェクト指向の原点(のひとつ)なのだけれど、こういう抽象的な分類を正しく抽出するためには、具体的な項目について深く広く知っている必要がある。

実際には、鳥は飛ぶものと定義した後に、あとから「ペンギン」や「キーウィ」や「ヤンバルクイナ」が発見されたり、ひどいと「カモノハシ」みたいが見つかって、こういうのがあるんだけど鳥クラスにねじ込めないか、先に哺乳類チームに問い合わせてみたけど断られたんだ、などということになってくる。「ハクチョウは白い」と定義してテストを通った後に、フィールドで「ブラックスワン」が発見されたりする。早すぎるクラス定義は、早すぎる最適化なんだ。うまくいくなら悪いことじゃないが、決して必須ではなかったんだ。私たちは、もっと具体的な事例を味わい尽くしてからクラスを抽出すべきだったんだ。

早すぎる抽象化の有名な悪い例は、天動説というやつだ。地上に立って夜空を見る。すると、時間経過に伴って、星が東から西へ流れる。「天球」というやつがあって、地球の周りをまわっているとする。自然だ。惑星という、星の中を動く変な星がある。あれは、地上にもっと近いところを早く回ってるだけだとする。自然だ。シンプルだ。

けれども、いろいろと調べるうちにシンプルな天動説では説明のつかない惑星の動きがたくさん出てきて、それに対応する補正を繰り返すうちに、天動説はどんどん複雑になって手に負えなくなった。そこまでいってようやく、コペルニクスの地動説モデルが出てくる。動いているのは、地球の方なんだ。細かいことまで考えると、天動説より地動説のほうが計算がシンプルになる。地動説の提唱ってのは、天界モデルに対する抜本的なリファクタリングだったのだ。

ただ、細かい天体運航の具体的データをあまり知らない人にとっては、天動説のほうがわかりやすい。天動説でも、ニュートンの逆二乗則なんかを使わずに、もっと泥臭い補正を山のように当てはめると、結構ちゃんと天体運航を記述できる。ただ、それはとても場当たり的で、全然エレガントじゃない。精度の高い計算をしようとすると、人間の手には負えなくなるだろう。

KISSやDRYを徹底したプログラミングをするには、そこで必要となる技法について、誰もがそれを理解できている必要がある。天動説しか知らない人ではダメで、ちゃんと地動説を理解しているような人が必要になる。そして、鳥を定義するにはブラックスワンやカモノハシの存在とその特徴を知っている人が必要になる。けれども、そういうのをちゃんと知っている人材はコストが高すぎて、そこらにありふれた現場では雇うことができない。コンチクショウ。

だから、地動説を知りながら、しかし天動説的なシンプルさでプログラミングする忍耐力が求められる。「なんかこれ、地面が動いてません? おかしいですよ」という声を愚直さによって封殺しないといけない。コンチクショウ。

でもまあ、自分も分野によってはこういう「地動説を理解できない人」なのであって、Wikipediaの数学用語のページなんかを読むと「こんな書き方じゃ何もわかんねーよクソが」という気分になるから、「記述のシンプルさよりも概念的シンプルさ」という考え方の意義も、まあわからないではない。

プロなんだから、概念の複雑さには着いてこいよ、てなことは言えるわけですけどね、なかなかね。シンプルってのは、それ自体が全然シンプルじゃねぇんだよな、コンチクショウ。


また熱出てきたから寝る。

RAIIとconstオブジェクト

C++の世界では、オブジェクトというのは生まれた時から完成していて、未初期化の赤ちゃん状態をなるべくなくそうという思想がある。RAIIというやつだけれども、一方でconst教というのもある。途中で値を変える必要のないオブジェクトは、値を変えようとする不貞の輩を見つけるために、constで宣言しようという思想もある。が、RAIIとconst教が同居すると、ちょっと難しい局面もある。

    Feeling feeling;
    if (today->isFineDay())
    {
        feeling = GOOD;
    }
    else
    {
        feeling = SO_SO;
    }

こういうことをやりたいとする。ここにconst教とRAIIの教条を持ち込むと、こうなる。

    const Feeling feeling = (today->isFineDay() ? GOOD : SO_SO);

天気が変わらないうちは俺の気分も変わりゃしねーぜ、と。こういう書き方ができると、グッと3項演算子のファンになったりするのだけれど、コーディング規約で3項演算子が禁止だったり、Allmanスタイルで統一されていたりすると、単なる値のスイッチングが上のような9行の大作になる。1行だと凝集度高杉だけど9行もちょっと極端だなぁ、となる。

まあ、boolからFeeling値を取得する手続きを無名のままにしないで名前の付いた小さな関数を書け、ってのがリーダブルコードの思想なんですが。でも英語ネイティブじゃないと、無数にある小さな関数の機能を簡潔な名前で書き分けるだけの文芸的センスがなかったりして、なかなか苦労するわけです。読むにしても定義を検索して骨が折れるし、調べてみてなんだよこれだけかよ、みたいな気分になったり、英語名が動詞も名詞も形容詞も無視してて変なことになってたりして、そのまま書いてくれよ、ということが少なくない。

/// getting one's feeling by weather of the day.
Feeling MyAwesomeClass::Impl::getFeeling(const DayInfo& theDay) const
{
    Feeling feeling;

    if (theDay->isFineDay())
    {
        feeling = Feeling::GOOD;
    }
    else
    {
        feeling = Feeling::SO_SO;
    }

    return feeling;
}

とかやってから

    const Feeling feeling = getFeeling(today);

となる、と。C++はメンバじゃない無名名前空間の関数を認めるので、ここではMyAwesomeClass::Implのメンバにしたりとかconst関数宣言とかは不要だけれども、なんとなく最悪ケースを想像させるような書き方にしてみた。詳細が隠蔽されて美しくはあるんだが、主要処理を書いている段階でここまでディテールに凝るのはなかなか難しいんだよな。リファクタリングマターだな、この水準は。