nodeでRESTのおもちゃを検討中

ちょっと思い立って、遊び用のREST APIのおもちゃを作成中。

前回遊んでいた https://akibakokoubou.jp/2020/05/05/qnap-local-media-player/ はとりあえず不便がない程度には動いていて実用中。

手軽なフレームワークを探していて Ts.ED(tsed) https://tsed.io/ を見つけたのだが、あんまり情報がない。少し調べてみるとnest https://nestjs.com/ とよく似ているっぽい。

偶然見つけたこともあるし、遊び用だしマイナー好きな人なので Ts.ED で少しいじってみる。取り急ぎの部分は一晩で出来たし。

関数型のその前に

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

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

手続き型プログラムは制御構造(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等で。

JVMバイトコード

Javaの実行時中間言語。MSの中間言語(MS IL)と並んで有名な中間言語。

自分らの世代で考えると本当にマシン語/アセンブラと同じと考えてよい。基本命令さえ把握すれば全然普通に読める。

普段このバイトコードでプログラムを読む必要はまったくないが、気分的に「いざとなればこのレベルで読めば全部分かる」と思い込んでおけば後々の今風技術にもひるまずに済む。

一部高級言語/オブジェクト指向言語固有命令と思われるものは出てくるが、それはそういうものでJavaマシン側でほどよく処理してくれていると考えれば問題ない(monitor,throw,invokevirtualなど)。

  • 解析環境
  1. intelliJ IDEAにjclasslib Bytecode viewer (https://plugins.jetbrains.com/plugin/9248-jclasslib-bytecode-viewer) プラグインをインストールする。
  2. Java,Scala等でサンプルのjavaコードを書く。よくあるサンプルプロジェクトみたいなものでもよい。
  3. 実行する(つまりjarがビルドされている)
  4. 該当ソースを選んで開く。その状態でメニュー View > Show bytecode with jclasslib を選ぶ。
  5. jar内サマリーとそのクラスのバイトコード解析ウィンドウが出る。

開いているファイルのクラスのバイトコード一式の表示が出る

  • Constant Pool 定数一式
  • Field メンバー変数
  • Method メソッド定義 Code部にバイトコードがアセンブラ風に表示される

あたりが分かればあとは何かのバイトコードの仕様とアセンブラ知識があればなんとなくは分かる。

元Javaソースとバイトコード出力結果を並べてみる。

public class SimpleJava { 
  public void addTest(int num1,int num2)  {
    int sum = num1 + num2;
    System.out.println(Integer.toString(sum));
  }
}
(引数num1はローカル変数1に、num2はローカル変数2に入っているとする) 
0 iload_1    (ローカル変数1から整数を取り出し、スタックにプッシュする)
 1 iload_2   (ローカル変数2から整数を取り出し、スタックにプッシュする)
 2 iadd      (スタックに詰まれた2つのデータを2つプルして取り出し、それを整数として加算し、スタックに再プッシュする=つまりスタックは2つの数字の代わりに加算した数字になる)
 3 istore_3  (スタックから整数をプルし、ローカル変数3に入れる)
 4 getstatic #8 <java/lang/System.out> (外部変数System.out出力端末番号を取り出しスタックにプッシュする)
 7 iload_3   (ローカル変数3から数字を取り出し、スタックにプッシュする=この時点でスタックには、System.outと加算した数字が詰まれる)
 8 invokestatic #9 <java/lang/Integer.toString> (外部の手続きInteger.toStringを呼び出す。恐らく中でスタックをプルして加算した数字を取り出し、文字列配列変換して、そのポインタをプッシュする)
11 invokevirtual #10 <java/io/PrintStream.println> (外部の手続きprintlnを呼び出す。スタックに詰まれていたSystem.outと変換した文字列ポインタを取り出し、プリント処理をする)
14 return     (戻る)

完全にスタックベースで計算するアセンブラである。

これでわかったつもりになって問題ない。

目的と書きたいこと

コンピュータをマシン語的、フローチャート/手続き型的に理解していて、なんとかオブジェクト指向の端っこくらいは理解したが、その先の今風技術がとっつきにくい人に 、なんとなく分かったと思わせて安心させるための念仏を短く作る。

  • マシン語的 = レジスタ、メモリ、プログラムカウンタ、インデックスレジスタなど
  • フローチャート/手続き型的=let ,if ,for/while, gotoなど
  • オブジェクト指向の端っこ=なんとなくclassはinterfaceは書くけど概念や哲学と言われると少し怪しい
  • 今風技術がとっつきにくい人=自分など

調べたい/書きたい項目

  • JVM
  • アノテーション
  • Autowired
  • lombok
  • 名付けのルール
  • MVC
  • 関数型
  • 非同期
  • カリー化
  • Option
  • map/for/flatMap
  • Either
  • Future
  • EitherT
  • React