Reactive

父: コンピュータはすごいぞ。論理を処理する機械だ。2+3と入れれば、5と出る。

子: すごいね! じゃ5と2を入れれば、3が出るね。

父: いや出ないよ。足せるだけだ。

子: え? 何で? 論理的機械なんでしょ?

Reactiveについての私の理解はそんなものである。逆から操作しても整合した結果が出るフレームワーク。

既視感があると思い浮かべてみると、物理世界をエミュレーションするゲーム物理エンジンとかに似ている気がする。

元々Vector演算をがっつりコンピュータに持ってきたのもシミュレーション分野→CGやゲームへみたいなところだし。

コンピュータ技術がこのあたりの整合性が現実的速度で出来るようになった結果、コンピューターは手続きやフローを幾何的に理解出来る空間感覚がある人が有利だったとこから、集合を変換演算していく数学的感覚が高い人が有利になってきたような気がする。

コメント

「プログラムソースにコメントを極力書かない」という思想も最近多くなり、レガシープログラマはそれなりに悩むことになります。

自分らの世代では「プログラムはコメント文がプログラムを動かすつもりでがっちりコメントを書いて、その間にプログラムソースを少しずつ埋め込むくらいに書け」って言われたものですが。

ただコメントを書かないという考え方はある程度は納得する。つまり

「仕様書とコメントの両方の説明文があったら、どっちが本当かわからなくなるじゃないか」

どちらか一方があれば十分であるという考え方。仕様書の手直ししていくうちにずれてウソになっていくコメントだったらないほうがよい という話。

実際はコメントは書かなくてよいのではなく「仕様書とコメントはどちらか一方は必要」である。例えばフレームワークのライブラリのソースを深掘りすると、API仕様としてソースに山ほどのコメントが付いている。こちらの場合は「仕様書は細かく書かずにコメントを書く(それをdocツールで仕様書にしてしまう)」という話で、同じことを2回書いたり、両方に書いてどっちが本当か分からなくなる という問題は発生しないようにしているわけである。

コメントは英語(英字)で書くか漢字で書くかも難しい問題がある。

メソッドやフィールド名は基本英字になるし、英字になれば語は英語かローマ字になる。英語が苦手な昭和プログラマーにはスラスラ読めるとはいかないのだ。ソースを書きながらかな漢字変換を切り替えるのも誤字の原因として煩わしい。

大前提としては「英語に慣れるのが理想。できれば留学やホームステイをして英会話もバリバリになるべき」ビジネスに幅と機会も大幅に大きくなるのだし。

そこまではもう行けない老年プログラマーはせめて中学英語を理解しつつGoogleやbing翻訳を駆使しよう。プログラム界隈で使われる英単語は結構限定的である。

個人で書く分には誤字を気にせず日本語でいいんじゃない。

関数型

普段の仕事で忙しくて、新技術をかまけていた頃に、関数型という話を一番最初に聞いたのはC#系のLinq( https://ja.wikipedia.org/wiki/%E7%B5%B1%E5%90%88%E8%A8%80%E8%AA%9E%E3%82%AF%E3%82%A8%E3%83%AA )でした。データ加工のための分岐やループの処理をSQL風の書き方で1行で書いてしまうという形。関数型とは言えないだろうが制御を条件にしてしまうという意味で、一番最初に感じを理解するにはLinqはとっつきやすい気がする。ただ今の関数型からは全然足りなかったとは思う。

値段のリストをすべて10円値上げする

int[] amountList = {100,110,95,120};
// えーっと、値段のすべてを取り出して、それらに10円を足して、それをリストにする
ArrayBuilder buf = new ArrayBuilder<Int>(); // まずリストの結果を集める場所を作る
for(int amount : amount) { // 値段のリストから1つずつ値段を取り出す
 int addedAmount = amount + 10; // 値段を+10 する
 buf.add(addedAmount); // 新しい値段を値段リストに入れる
} 
int[] ans = buf.toArray(); // 計算した結果をリストとしてまとめて取り出す
// ああ、仕事した。。。

手元で数字を入れた箱をあっちこっちに動かして、制御の分岐をあっちこっちに動かして、結果が出来ました。という感じの処理になる。メモリ管理が自動化されていない頃なら、さらに場所を作るときに元のリストの長さはいくつか調べて、その長さのメモリを確保して。。。とかも必要になるところです。これそのものは正しいですし、CPUはこんな感じで実際処理しています。

最初の仕様「値段のリストをすべて10円値上げする」を「値段のすべてを取り出して、それらに10円を足して、それをリストにする」という制御の仕様に変換しなおして、その処理を書く。

こういうのがどんどん重なっていくとそれなりにしんどくなってくる。それが従来の手続き型です。

val amountList = Seq(100,110,95,120)
// 値段のリストをすべて10円値上げする
val ans = amountList.map(_+10) // 1行の仕様=1行のソース

関数型の場合、すべての仕事を変換と直列流れにするということだから、「値段のリストをすべて10円値上げする」→関数型『「値段のリスト」を「10円値上げしたリスト」に変換する』 となる。

制御っていらないじゃない という話にしなければならない。

では条件を少し変更して「120円以上のものだけ10円値上げする」であれば制御があるのではないか。でもここを少し複雑にしたとしても結局「120円以上」は変換する条件となり、条件が複雑になっただけ(マシン語にしたときそれはgo toが含まれる制御になるがそれはコンピュータの都合である)

// 値段のリストの120円以上を10円値上げする
val ans2 = amountList.map(a=>if (a >= 120) a+10 else a)

ifが出てきたんだからこれ制御でしょ。とは思う。でもこれも条件を付けた変換である(と解釈する)

// amountが120以上ならamountをamount+10に変換する さもなくば amountに変換する
val addedAmount = if(amount > 120) amount+10 else amount

制御ではなくすべてを変換でまとめていく。

業務の仕様(ビジネスロジック)は概ね箇条書きで書く。箇条書きで書けるものは「データの変換」くらいには落とし込める(落とし込めない場合、論理的に仕様が完成していないことが多い) 。手続き型の頃はこの仕様をさらに制御の仕様に書き直す必要があった。しかし関数型の場合は制御の仕様に書き換えなくてもよい。

極端な話「仕様書の1行が、プログラムソースの1行に合致する」くらいまで行くことを狙っているのである。

関数型が話題になった頃のプログラム言語は、初期化やコンピュータ制御の都合をソースから極力排除する仕組みになっている。これも仕様書にない(省略された)記述をプログラムソースからなくすためである。

ここから先のプログラムは「データを別データに変換する」の作業を延々書き連ねていくことになる。集合の数学的変換演算が得意な人の独壇場ですね。

行が減る代わりに1行の中に表現されるものが多くなる(1行が長くなる)。けど仕様書とは乖離が少なくなる。1つの処理がやっていることをサブルーチン/関数みたいに見えないボックスにしてしまうのではなく、確かに1行でやっていることが書かれていて確認できる。なによりディスプレイの1画面で見通せることが多くなる。

レガシープログラマとしては便利だとはわかるものの、進んでいるうちに不安になる。

そこは納得いくまでライブラリのソース,デコンパイルソース,JVMとにらめっこすればよいと思う。便利さとケンカしても勝ち目はない。

関数型のその前に

レガシープログラマは関数型を考える前に一つだけ覚悟しておくべきことがある。

ここから先はカルチャーが変わる。怖いことではないが一旦今までの考え方が通じなくなる部分がある。そのためゼロから学び直す部分が発生すると覚悟して勉強が必要になる。ただそれでも中身はプロセッサーであることは同じなので、一旦そこを頭に入れてから後で戻ってくればよい。

手続き型プログラムは制御構造(if,goto,whileなど)が作りの主体なので、イメージとしてはフローチャートである。レガシープログラマはフローチャートなどの幾何構造のイメージトレーニングがうまい人が多いと思う。その上で古いコンピュータの都合(少ないメモリ、遅いCPU、IOの制限など)、ツールの都合(遅いコンパイラ、少ないメモリからくる言語制約、理不尽なフレームワークの約束事など)を理解して納得して最後までつきあってくれる人がプログラムを使いこなす。

もう今のPCは十分リッチなのでそのあたりを最初に考える必要はない(逆にメモリの都合とか考えてわかりにくいディープコーディングをしたらそのほうが怒られる) 論理的で筋道を単純化していることが重要になる。

この単純化の際に、制御というあっちこっちに飛びまくる仕組みは無駄に複雑である、と考える訳だ。制御構造ではなく、すべてを「単純な直列結合」で表現する。結合する要素は「データを変換」するという要素だけで記述する。

今まで制御構造として書いていた部分は、データ変換の条件という形で中に入れてしまう。

じゃ今まで制御として書いていたifはどうするのか。例えば「旗がAだったらロボットは右に行く、旗がBだったらロボットは左に行く」みたいなものは?

旗→ロボット制御信号(右行け/左行け)

みたいな変換になる。人は直列に並んだ手順のほうが理解しやすい。その前提で今までの制御を全部書き直したほうが人が理解しやすいし、多くの場合業務の仕様書も箇条書きで手順が並んでいるので仕様書とも乖離しない(手順書まで落とした段階では制御が入ることもあるが仕様の段階では直列な箇条書きの場合が大半である)。

実際にはこの複雑な条件というものをエラー処理まで含めてうまく表現するのにいろいろ考え方を導入した訳で、その仕組みが今時の関数型プログラミングである。

すべてをデータ集合の変換として表現するということは、つまり今風のうまいプログラマは「論理的で数学直感(集合、ベクトル、統計)が優れている人」になる。レガシープログラマは「チャートなどの幾何直感が優れていてコンピュータの都合に合わせることができる人」になる。

だから今は純粋に頭が良いといわれるタイプが優秀なプログラマになる。今プログラムが優秀な人は昔の環境でプログラマになっていた場合、理不尽すぎてついて行けない(ついていく価値を見いだせない)と思うかもしれない。

でもレガシープログラマはそのあたりが違うのだと割り切って得意な若い人に任せればよい。あとはそれまでの長い体験を生かしてどうとでもなる。中身はそれでもコンピュータなのだから。

null

かなり前に「コンピュータ言語にnullがあることがすべての元凶」との論説がよくありました。

最初に読んだときは「うーん、そうなのかなー、便利だけど」と思いつつも、OptionやEitherなどを使い慣れてくると「うーん、なるほどなぁ」と思うことになった。

全否定する気は今でもない。0(全ビット0)や-1(全ビット1)はハードの論理ゲートにとって特別な状態として取り扱いやすいのは確か。1ビットが粗末にできない時代は仕方ないだろう。その延長上で高級言語が進化しているのだからその過程でnullがあったのも仕方が無いこと。

論理的にはないほうが素直になるというのなら、これからそうする ということでよいだろう。どんな文化/歴史にも無かったほうがよい黒歴史は存在するものだし。

MVC

最近のアプリやシステムサービスはおおよそMVCモデルで作られることが多い。モデル-ビュー-コントローラ

データ(DB含む)はモデルとして定義して、概ねクラスで作る。

コントローラは呼び出し元(httpサーバー経由で来るユーザ操作とか、デバイスからの呼び出しとか、外部サーバーからのAPI呼び出しとか)からの要求を受け付けて、モデルに突っ込んで、サービスを呼んで加工し、ビューで返すべき結果にまとめ直して、呼び出し元に返す。

こういう形式にまとめられた理由について、論理的な解説書はいろいろ理由を書いているがその個々は私は別に頭に入ってはいない。読めばもっともだと思う。

でもプログラムはそれにはまらない書き方は出来るし、クラス分けも別の見方の分け方もできる。初期のオブジェクト指向系の人(教育系のSmalltalkの人とか)にすれば現実モデルと全然合致しないと言うかもしれない。

なんでDBのレコードって現実世界に実物が存在する物ではないのにクラスになるのかとか、レコードの分割単位は実際のオブジェクトの単位ではなかろうとか。オブジェクトのほうにモデルとスキーマをがっちり合わせたらRDBの形には入らなくなる(もちろんそっちの考え方のシステム/言語も今はいろいろあるだろう)。

でも、なぜこの分け方が多いかは単純に「たくさんこの手のシステムを作った人から見て、こう分けたほうが後々を含めて一番手間がかからなかった」という経験知見ということだろう。

建物を建てるときに、1階より2階を大きくした建物を作ることは出来る。芸術家がデザイン的に建てる場合もあるし、立地上そうする必要があったということもあるだろう。でも特に条件が無ければ2階は1階と同じか小さくしたほうが建てやすい。

プログラムの規模が小さいうちはどうとでもなるのだ。段ボールの家なら1階と2階がどういう関係でも問題ない。実用的な規模、または大規模なシステム、多人数、多数の企業が関わるシステムだと、大枠が合理的にわかりやすくないと破綻する。経験・知見でこれいいよね というアプリ/サービスの作りの一つがMVCである。

経験知見により取捨されていく方法論は大事にすべきなのだろう。ただ将来ともその知見通りとは限らない(とにかくソフト分野はカルチャーが常に変わり続ける)。自分の若い頃の知見(コンピュータの都合に合わせたシステムが後々手間がかからない)は、技術と方法論の進歩でいくらでも変わっていくのだ。

名付け

Lombokを使うとき、変数numOfPieを外から読むにはgetNumOfPie()を呼び出すことになる。getXXXという名前は固定の形式でツール側で自動生成される。多量のメンバー変数があるとき、書くのも面倒だしソースもむやみに長くなって見通しが悪くなるので助かるのだが、一方で「今まで自分で決めていた変数や関数の名前を、仕組みの側に一方的に決められる」というのもレガシープログラマにとってはつまずいてしまうところだ。

近代のフレームワークは名付けをいろいろ強制する。時期としてJavaのLombok, JPA(findByXXXAndXXX…()),Spring, PHPフレームワークのCakePHPあたり以降のフレームワークには多かれ少なかれあるんじゃないだろうか。知ってる範囲/覚えている範囲で

  • Lombok: getter setterを書かずに済む data→getData(),setData()
  • JPA: 関数を書かなくてもDBのモデルだけから解析して findByData() と書けばDataの検索関数が呼べる(頻繁に利用されているかどうかは知らない。少なくとも案件で使うことになったことはある)
  • CakePHP モデルの名付けで英単語の複数形で書くとリストとして処理され、単数形で書くと単一と見なされるとか。詳細は忘れた。

不便そうに見えてこれは非常に便利な機能である。設計フェーズならまだしもコーディングしているときにいちいち名前を考えるのは実は面倒である。一時的な変数は a とか i とか 使って、後になってなんだっけと思ったりする。取り出すならgetXXX(),設定するならsetXXX()と決め事をしてあればそこを悩まずに済む。

余談だが名前は英文的に長く付けることが多くなった(キャメルケースかスネークケースかは処理系の慣例次第)。もちろんそういう長い変数名/関数名を使えるようになったのはハードがリッチになったことも大きい。若い頃に使ったアセンブラ,FORTRAN,BASIC,初期のプリプロセッサ型C(Introl Cとかも)なども長さは8文字までとか今ではまったく考えられない制約があった(そこでハンガリアン記法なども使っていたな)。

昭和プログラマからすると、このフレームワーク側の名付けの強制については、そういう強制があるということを頭の隅に置いておかないと「この関数/変数はどこにあるのだ」「自分で付けた名前のメソッドがなぜか呼び出されない」「なぜか原因不明のコンパイルエラーが出る。変数名を変えただけで起きたり直ったりする」など理不尽な理由で悩み続けることになる(CakePHPとかも結構悩んだ)。

名付けが強制される(ツールやフレームワーク側で合成される)ということは、一つ書いた名前 がいろんなところで合成されて使い回されるということだ。例えば あるクラスのメンバーに cupOfTea という名前のメンバー変数を作れば、それは getCupOfTeaとかsetCupOfTea とか findByCupOfTea とかソース全体に波及していく。つまり最初の名前に直感的にわかりにくい/区別しにくい名前を付けると、それが後々まで響いてしまう。変数名にうかつにfindNameとか付けてしまうとgetFindNameとなったり、a1,a2 とつけたらfindByA1()となったり、後で悩みまくることになる。今はリファクタリング機能があるIDEが多いので後で直すことも出来ないではないが、仕様書で仕切る多メンバープロジェクトでは一度付けた名前を軽々と直すことも出来ない。公開APIなどもだ(最近のオープンソース系のプロジェクトでは、メジャーバージョンアップのたびにまずいところを根っこから直すところも多いので、そういうところはdeprecatedとか付けて名付けを直すことあるのだろう。直された側は悩むのだが)。

今時の設計者にはソフト開発向けの名付けのセンスがいるのかもしれない(慣れだとは思うが)。ただシステムが巨大になれば名付けも含めて膨れ上がって矛盾し出すのは仕方がないことだ。破綻を遅らせるようなうまい名付けセンスが望まれるとともに、名付けが破綻するくらいに使われ続けたシステムならばそれはそれで大往生とも思える。

最初の名付けの後々の影響が大きいため、近年のプログラムでは誤字(typo)は非常に気を遣うようになっている。上記の名付けロジックでつまらないバグになったり、長く続くプロジェクトで初期の誤字でニヤニヤされたり。IDEの補完入力でもtypoがあるとうまく働かなかったりする。結果、IDEはスペルチェッカーが入っているものがほとんどになってしまった。

スペルチェッカーというとワープロの専売機能(スペルチェッカーライブラリのアプリ組み込み案件とかもやったな)だったのに、ソフト開発に英語のスペルチェッカーが必須になってしまったのは昭和のプログラマには笑い話にも思えてしまう。

lombok

lombok https://projectlombok.org/ の一番分かりやすい機能は @Getter @Setter だろう。

Setter,Getterの話は私はC#の勉強時に知って、ああなるほどと思った。

クラス内のメンバー変数はクラス外で読み書き不要なら他人の処理に壊されないようにprivateにすれば安心である。しかし外から書かれると困るが外から読みたいことは多々ある。そういう変数はprivateに設定した上で読み出し専用のメソッドがあればよいという話。C#はそれを言語の基本レベルで持っている。Javaも意図的にgetData()的に書けば書いていけるが、フィールド数が山ほどになればそれを書くだけで疲れてしまう。

lombokはEclipseに組み込めばアノテーションで@Getterと書くだけで、ソースには見えないgetterのコードが埋め込まれて、別の処理から例えばgetData()と書けば中身を読める。変数そのものはprivateなので外からは壊されない。

@Service
public class AdderService {
  @Getter
  private String data;
}
↓出力classファイルをデコンパイル
@Service
public class AdderService {
  private String data;

  public AdderService() {
  }

// 見えないままに自動生成
  public String getData() {
    return this.data;
  }
}

エディタからは直に見えないgetData()というメソッドを他の場所の処理で書ける。ソースを全文検索してもgetDataは見つからないのに処理が通って、プライベート変数が外から読めて、コンパイラも怒らない。昭和プログラマからすればこれは言われなければ分からない。lombokの設定準備が少し特殊(たしかlombok jarファイルを直接起動してEclipse exeを加工するような操作?)な点も含めて、何回も設定しなおしで悩むことになった。

実現手法としては、コンパイル時に処理に割り込んでアノテーションを検索して処理を追加するみたいな感じなのだろう。細かいところは知らない。Javaの仕様に書かれているのだろう。かなりハックめいた手法である。でも考えてみれば自分のLexyも似た観点でOSをハックして英語メッセージを日本語化している。最初に考えた人もわくわくしながら書いたのだろう。

これが便利なのは確かだった。特にorマッパーなどで多量のフィールドを持つデータを格納するクラスの手間や間違い、ソース量を減らし、保守性をかなり上げる。

ただこの時期の新技術は手間を減らす部分が主ではないと思う(後で思った)。React系の直前までの新技術は「プログラムソースはビジネスロジック(仕様書)の事だけを書く」という進化だったのだと思う。

Autowired

Autowiredによる依存性の注入(昭和のプログラマにはこの言葉がピンとこない)は、ぶっちゃけ

「アプリフレームワークの初期化のときやクラス(=Bean)の初期化のときに、jarの中の@Autowiredのラベルのついている変数を探して、存在したら、nullかどうかチェックして、nullだったら対象のシングルトンインスタンスが作られているかどうかを探して、作られていなかったらそれを先に初期化して、対象インスタンスのポインタをjarの実行中のバイトコードの変数に直に上書きしてしまう」

という話。JavaBeansは初期化時の規則が定まっているので、そういう初期化プロセスまでフレームワーク側ががっつりハックできるという話なのだが、実行時のバイトコード内の変数の位置を実行時に探索して、後付けで値を入れてしまうというのは自分らの世代では「OS以外やっちゃいかんこと」という先入観があるのでどうしても「え? なんで」となってしまう。

実行時にスタックポインタに直書きしてプログラムの飛び先を変える古いウイルスのような挙動とか、デバッガで実行中のアプリを一時停止してメモリを書き換えてプログラムの飛び先を変えるとか、アプリではそれやると反則だよね(そんなコード書いたら袋だたきだよね)というイメージが古いプログラマにはある。でもそれをJavaアノテーション仕様とBeanの仕組みでルール範囲でハックして実現して、現実として「便利だよね」という状況を作ったら認めざるを得ない。すべては「ソースコードをビジネスロジックの仕様記述のみにする」という今風の哲学によるものだからだ。

「nullにいつのまにか値が入っていても、便利だからいいじゃないか。業務ロジックを書いているときに初期化なんて考えたくないし、何より目に邪魔だから見たくない(見えないところでやってくれ)」というコンセプト。

あとは、細部まで追った訳ではないが簡単に調べた部分だけ。

Spring Boot サンプルコード

package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class DemoApplication {
  public static void main(String[] args) {
    SpringApplication.run(DemoApplication.class, args);
  }

  @Autowired
  private AdderService autowiredAdderService;

  @GetMapping("/hello")
  public String hello(@RequestParam(value = "name", defaultValue = "World") String name) {
    return String.format("Hello %s!", name);
  }
}

デバッグポインタをAdderServiceのコンストラクタに設定してスタックを追う。

DemoApplication 初期化時にAutowiredを探索
autowiredする要素(inject)を登録する
inject要素を作る
存在していなかったらシングルトンインスタンスを作っちゃう

※ @Autowiredによる紐付けは最近は推奨されないそうだ。 http://pppurple.hatenablog.com/entry/2016/12/29/233141 言ってることはわかるので今度Springを使うときには参考にしよう。

アノテーション

かなり前に、はじめてSpring Bootの案件に入った際に、マイコン脳の老プログラマが最後まで腑に落ちなかったのはアノテーションだった。

簡単に言うと「初期化されていないメンバーになんで値が入っているのか(Autowired)」「存在しないメソッドが何で呼び出せるのか(Lombok,JPA)」

便利なのは確かであったが当時はそういう約束事だと考えて進めるしかなかった。今だって中の詳細ロジックが読めている訳ではない。ただ細部はウソでもよいので自分が納得できる理由が説明できておかないと解決困難な問題を追跡する際に当て外れな部分の調査に負荷が発生するので、問題解決の枝刈りをするためにも「結局何なのか」を自分なりに理解しておく必要があった。

ネットでごそごそ調べた調べた結果、アノテーションはその名の通り「単なるラベル」だった。

プログラムのソースのメソッドやクラス、メンバ変数の位置にラベルでマークを付けるもの。

ラベルがどう使われるかはいくつか種類があり、

  1. コンパイル時に探すことができて、jar出力結果を変える
  2. jarの中にそのままラベルを付けて、実行時に変数やメソッドに位置を探せるもの
実行時ラベルの例

で、ツール/フレームワーク側でラベルを探索することで、よい意味でのインチキめいたハックをしている訳だ。

とりあえず自身としてはそう理解できていればよいと考えた。細部はAutowiredやLombok等で。