なぜ自動テストの導入は失敗するのか?

 開発室の雑談。営業側のマネージャが言うには
「今のプロジェクトで自動テストの導入を試みている話をしたら、XXXさんのところでも過去にいくつか導入を試みたけどもみんな上手くいかなかったって話になって」

 なるほど?

 まあ確かに自動テストはシステム開発にとって魅惑の技法ではあるものの、では導入がうまくいっているか? というと普及率は低いと言わざるを得ない。私がお手伝いしたプロジェクトでは、元請け側から自動テストをやるお達しが来たわけだが、紆余曲折あって掛け声倒れのような状態になってしまった。

 ビジネス書の煽りタイトルのような本件だが、古式ゆかしき受注生産の業務システム開発プロジェクトに自動テストを導入しようとして失敗する事例を聞いたので、僕なりに分析して見出した要素を挙げておこうと思う。

V字モデル

 ソフトウェア開発の手法としてV字モデルというものがある。

 オーダーメイドでシステムを作るにあたって、どのような要求があるか、どう設計すればよいか、それを段階を踏んで細分化・詳細化していき、プログラムのコーディングに至り、そして単体テスト結合テスト、とパーツ単位の検品からその組み合わせとしての試験、といったように進めて成果を上げていくわけである。

f:id:Nagise:20210429133857p:plain
V字モデル概念図

CC 表示-継承 3.0, https://ja.wikipedia.org/w/index.php?curid=894431


 このV字モデルと、いわゆるウォーターフォール開発(定義や本来の意味は?といったことに踏み込むと難しいのでここでは触れない)において、自動テストの導入のネックとなるのはなんであろうか?


自動テストとは?

 自動テストとはこの文脈ではコンピュータ・プログラムが想定通りに動くかテストする工程を自動化するものだが、自動化といってもピンとこないかもしれないので簡単に触れておこう。全自動のコンピュータが何とかしてくれるという話ではない

 例えば、ふたつの数字を渡すとその和を返す機能を考えてみよう。

public int add(int x, int y) { … }

 これを試験する場合、ふたつの数字x, y にさまざまな値を渡して、得られる答えが期待通りかを確認することになるだろう。

  • 1, 2 を与えれば 3
  • -1, 1 なら 0
  • -2, -3 なら -5

 といった具合に。とはいえ、ひたすら並べていってもバグ検出の効果が薄いので、特徴のあるテストケースを用意するなどの工夫は必要だ。まあ、その辺の話も割愛。

 ともかく、こうした入力と出力があって、それが予見可能であれば、プログラムを呼び出してそういう値になるよな? ということを確認するプログラムが書ける。

assertEquals(3, add(1,2));
assertEquals(0, add(-1,1));
assertEquals(-5, add(-2,-3));

 こうした検証のためのプログラムが2021年時点のIT界隈で「自動テスト」と呼ばれるものの本体であって、「自動テストの導入」というのはこうした検証用のプログラムを書くことに他ならない。

 つまり、本来のプログラムのほかに、その検証用のプログラムを別途用意するというわけである。

 この検証用プログラムがうまく作れれば

  • 以後のテストは検証用プログラムを実行するだけで済む
  • 人力で膨大なテストを行うのに比べればコストダウンできるだろう
  • テストフェーズのリードタイムが短くなるだろう
  • 人力に比べヒューマンエラーが抑えれるだろう
  • 上記の総合的な作用により品質が向上するだろう

ということを夢見ていくつものプロジェクトが玉砕してきたというわけだ。


テスタビリティ

 先の自動テストのための検証用プログラムは非常に簡単な例だった。

 しかし、世の中にはテストしにくいプログラムというものがあって、検証用プログラムを書くことが非常に難儀することがある。ここでテストしやすさ、テスタビリティ(testability)という概念が出てくる。


 テストしにくいとはどういうことか? いろんなケースがあるが、テストしにくいモノのひとつには「状態を持つ」プログラムが挙げられる

 工学系の学部などで科学の学生実験とかやらされた人は身に染みていると思うが、「同じようにやって同じようになる」ためには、前提が揃っていることが大事である。

 状態を持たないプログラムを対象にして自動テストのための検証用プログラムを書くことは簡単である。同じ値をつっこめば、いつも同じ値を出力する。 1 + 2 = 3 であり、冪等(べきとう、何度繰り返しても同じ性質)である。

 しかし、内部で状態を持つプログラムは同じように操作したつもりでも突然違う挙動を示す。電卓でボタンを順に 1, +, 2, = と押した場合、3と表示されるだろうか? そのテストを始める前に誰かが 5, + と入力した状態から始めたらどうなるだろう? 期待される答えである 3 ではなく、8 が表示されてしまう。

 状態をもつプログラムのテストしにくさはこういうところで出てくる。まあ電卓の場合はCボタンを押してクリアしてからスタートすればいいわけだけども、じゃぁデータベースはどうしたら良い? データベースにリセットボタンが必要になってくる。データベースにテスト用のリセットボタン、用意してありますか?



 これは一例でしかないが、テスタビリティというのはなんとなくで上がる話ではなくて、テスタビリティを上げるための技法を駆使しなくては上がらないということの一端が分かっていただけただろうか。V字モデルでいえば基本設計、機能設計、詳細設計といった設計の工程で、あらかじめテスタビリティを上げるための配慮した設計をしておかねばならない。


自動テストはいつ作られるのか?

 冒頭に挙げたV字モデルの概念図では、要求分析に始まり、基本設計、機能設計、詳細設計と細分化してからコーディングを行い、単体テスト結合テストシステムテスト、受け入れテストと進んでいくように描かれている。

 オーダーメイドでシステムを作ろうとした際、こうした工程を経てシステムを作りましょうという提案がされることが多いことだろう。

 では、「自動テスト」を導入するとした場合、自動テストはこのV字のどこで作られるのだろう?



 単純に手動での単体テストを置き換えるものと位置付けるのであれば、V字モデルの「単体テスト」のフェーズで自動テストを作成するということだろうか? プログラムを一通り作り終えて、単体テストフェーズに移りましょう!というタイミング?

 😩😩😩

 この案、聞いただけでうんざりした顔をした人も多いのではないだろうか。



「導入を試みたけどもみんな上手くいかなかったって」

 そりゃそうだろう。うまく行くわけがない。



 いわゆるウォーターフォール開発の手法によって、書類で工程を分離して記録を残すやり方だと、設計フェーズと、コーディングと、テストフェーズは明確に分かれている。「分離することで上手くやってきた」と語る人もいることだろう。

 そうか、これが大きな阻害要因だったか。


自動テストが破壊するもの


 自動テストにはプログラムを「部分的に実行する」という意味合いもある。プログラムを書いて、動かしながら、その動きが狙い通りか確認しながら仕上げていく。この「動かしながら」のためには巨大システムの一部のパーツを、その一部分だけで動かす技法が必要で、自動テストのためのテスティングフレームワークがその答えの一つだ。

 プログラムを書きながら、自動テストのための検証用プログラムもまた並行して書いて、自動テストを用いることでプログラムを実行しながら進めていくというスタイル。その究極はTDD(テスト駆動開発)だけども、そうでなくともコーディング中から自動テストを書くのが良いやり方だ。

 早めに動かして、早めにバグを検出できるならそれに越したことはない。

 しかし、これはV字モデルにおけるコーディングフェーズと、単体テストフェーズを曖昧にする。従来のV字モデルのやり方で、明確に分離された単体テストフェーズに自動テスト作成を試みるようでは、自動テストの効能が十全には発揮されない。自動車を買って馬につないで牽かせるような話だ。



 また自動テストは、いかようなプログラムにも後付けで用意できるわけではない。これは先にテスタビリティの部分で解説した。自動テストをやるには自動テストしやすい設計である必要がある。極力ステートレスにするとか、モックに差し替えが可能なようにDIコンテナを駆使するとか。そこにはいろんな技法があって、そうした技法を凝らすことでシステムを自動テスト可能となるよう、先人たちが工夫を重ねてきた。

 それでも、より具体的なプログラミングを行っていると、後になってから「これはあっちに持って行ったほうがいいな」ということは多発するし、自動テストのために設計変更したくなることも生じてくる。これはV字モデル的にはコーディングフェーズから詳細設計フェーズへの手戻りだ

 また、自動テストの効能のひとつに「リファクタリングを可能にする」という大きなポイントがある。詳細設計フェーズとコーディングフェーズの分離はこの開発時のリファクタリングを大きく阻害する


 自動テストを活かすには、詳細設計フェーズとコーディングフェーズが分離されていてはいけない。コーディングフェーズと単体テストフェーズが分離されていてもいけない。



自動テストの導入のためには


 本稿の主張は、V字モデルこそが自動テストの阻害要因であるというものである。であるから、V字モデルに拘泥する限り、自動テストの導入は成功率が低いし、よしんば導入できたとしても効果が低く成果が上がらないものとなるだろう。


 おそらく、V字モデルでいうところの詳細設計~コーディング~単体テストのフェーズを再考する必要がある。この部分を大きく改革しないと自動テストの恩恵を得ることは難しいだろう。


 ある程度のスキルレベルにあるメンバーで行われるプロジェクトであれば、この詳細設計~コーディング~単体テストの一連の工程を、プロジェクトメンバーがうまいことやってくれていた。しかしこれは、そういう練度のプログラマで構成されたプロジェクトであればこそだろう。


 となれば。より低スキルの寄せ集めメンバーでベルトコンベアの流れ作業のようにシステムを作ろう! という思想でやるのであれば。少なくとも旧来のV字モデルの詳細設計~コーディング~単体テストに変わる新しい分業モデルを検討しなくてはならないはずだ。

修正履歴

  • 2021/04/29 自動テストのコードの値の誤りを修正

Javaのリテラル

Q. リテラル(literal)とは何か?
A. 言語による

なんでこんな記事を書いているかというと、リテラル禁止FizzBuzz大会がいまいち盛り上がらなかったので、ルール解説をして参加者を増やそうという目論見である。

Java言語におけるリテラル

プログラミング言語一般で「リテラル」というと

コンピュータプログラミング言語においてリテラルは、ソースコード内に値を直接表記したものをいう。言語によってリテラルとして表記できる型の種類や表記方法は異なる。

リテラル - Wikipedia

という感じで、 42 とか "hoge" とかそういう値を直書きしたものを言う。このあたりは雰囲気で把握している人も多いだろう。ではその明確な範囲としてどこまでがリテラルか?となると、言語ごとのルールブック、つまるところ言語仕様に当たらなければならない。

Java言語仕様は怖くない。インターネットでオープンにされており、非常にアクセスしやすい。 Java SE Specifications のページに各バージョンの言語仕様へのリンクがまとめられている。

2021年9月には次期LTS(長期サポート)版のJava17がリリースされるが、本稿執筆時点でのLTSはJava11なのでJava11の言語仕様を元に調べてみよう。
物事を調べるには原典にあたることが大事である。原典をみて誰かが本を書いたりblogを書いたりする。それを見て書かれたblogは原典を見たものより信頼度が落ちる。孫引きして書かれた引用元も示されてないような記事を信頼してはしてはいけない。

"The Java Language Specification, Java SE 11 Edition" の "HTML"のリンクを辿ると The Java® Language Specification Java SE 11 Edition の目次に辿り着ける。

リテラルについて書かれているのは 3.10. Literals の部分だ。Java言語仕様は英語で書かれているが、現代は機械翻訳が優秀なので、機械翻訳でもある程度概要をつかむことができるのではないだろうか。また過去には公式に日本語訳がされたものが出版されたこともあるが、Java5の内容の
Java言語仕様-第3版 (2006年)が出たのが最後となっている。

Java言語におけるリテラル

Literal:
 IntegerLiteral
 FloatingPointLiteral
 BooleanLiteral
 CharacterLiteral
 StringLiteral
 NullLiteral

と列挙されていて、各項目の説明が以下に続く。

リテラルの詳細な定義まではここでは立ち入らないが、ざっくりと表にすると

種類 概要
IntegerLiteral 整数の数値 42 0xCAFEBABE 0b0010_1111
FloatingPointLiteral 浮動小数の数値 1.234
BooleanLiteral boolean型の値 true false
CharacterLiteral charの値 'A'
StringLiteral 文字列 "hoge" ""
NullLiteral null null

といった感じ。

また、 3.10. Literals に記載されていないが 15.8.2. Class Literals という項目があって、classリテラルについて記載がある。これも Java言語における「リテラル」に含めそうだ。

これは Hoge.class などといったように、クラス名に .class とつけることで java.lang.Class オブジェクトをリテラルとして表す記法。

リテラルを使わないでFizzBuzzをしてみよう!

さて、以上でJavaにおける「リテラルの禁止」の指す範囲が明確になったと思う。

プログラミングパズルが好きな人は是非、リテラルを用いないFizzBuzzに挑戦してみて欲しい。たぶん、セミコロンを使わないFizzBuzzよりは簡単じゃないかな。(参考: セミコロンレスJava はチューリング完全か? - プログラマーの脳みそ

セミコロンレスJavaもそうだが、この手のプログラミングパズルは標準APIの範囲で行うことが暗黙の了解となっている。

僕も書いてみたけども芸術点がイマイチなのでまだ満足できていない。

空気を読んだ話

一般に「リテラルを使うな」というのはプログラムコード中にマジックナンバーなどを直接記載しないということを意図したものである。なんらかの意味のあるフラグの値などは定数定義(Javaの場合はstatic finalフィールドにするかenumにすることが多いかな)して間接的に用いることを指す。

言語仕様上のリテラルを一切用いないというのは揚げ足取り的な話で、しかしここではネタとしてその制限でプログラミングを書けるか?という設問がおもしろいので取り上げている。

訂正

2021.04.19 BuzzをBullと誤っていた箇所があったので修正

IT勉強会界隈の冬の風物詩といえば、飯テロ勉強会の異名を持つブリ会議 ―― burikaigi ―― だろう。
toyama-eng.connpass.com

そもそも富山で旬の寒ブリを食べようというIT業界人の同好の士が、ブリを食べるために勉強会を催したというのが始まりである。飯テロ勉強会なのは仕方ない。
しかし、勘違いしないで頂きたいのだが、あくまでもIT勉強会なので趣旨を誤解なきよう。講師陣は今回も豪華です。セッションの内容も充実。
例年、北陸にあって先端情報に触れる貴重な機会でもあります。地方勉強会のひとつの形ですね。


プログラマーの修学旅行とも評されるburikaigiなのだが、今年ばかりは新型コロナウイルス感染症 COVID-19 の情勢を見るに、大勢で集まって宴会というわけにはいかない。
しかし、松本魚問屋様の協力によってリモート・ブリしゃぶの目途が立ち、burikaigi 2021 オンライン開催とあいなりました。

matsumoto-uodonya.co.jp

なんと、参加者限定 burikaigi スペシャル商品を用意していただきましたよ。
懇親会参加登録者には特設サイトのURLをお伝えいたします。

もちろん、自前でブリを用意してもらっても大丈夫です。
全国からリモート懇親会でブリを楽しめるイベントとなっています。



さて、ここからはリモート・ブリの紹介です。(まるで通販番組みたいだな🤔)

ブリしゃぶは水揚げ当日に血抜き処理をし、丁寧に仕上げたものを瞬間凍結!
生のブリはどうしても獲れるかどうか不確かで、かつ、保存可能な期間も限られるためブリ会議当日にちょうどおいしく配送というのは非現実的なわけです。
冷凍配送ゆえの安定供給。最近の冷凍技術は凄いですね。冷凍品とあなどるなかれ。試食してみたけども、とても美味しかったですよ!
ただ、あくまでブリしゃぶ用とのことでご承知おきください。刺身では力を十全に出し切れない……。

今回はなんと、ゆずぽん酢、タイの昆布締、ぶりジャーキー、ほたるいか塩麴漬けをセットにした飲兵衛仕様でご提供!

とはいえ、懇親会だけ参加とかはダメだよ。ちゃんと勉強しような!

f:id:Nagise:20201217201311j:plain
burikaigi用の特別セット
f:id:Nagise:20210105204911j:plain
冷蔵庫で解凍
f:id:Nagise:20210105212648j:plain
しゃぶしゃぶ

西暦1年は閏年か?

閏年(うるうどし)の話題。

 Twitterで見かけた話題で「西暦1年は閏年かどうかぱっとわからん人おる?」という些か煽り気味のツイートを見かけたのだけども、反射的に「閏年じゃないに決まってるじゃん」とぱっと答えてしまわないだろうか。本当にそうだろうか? そう単純な話なのだろうか?

 プログラミングを学んでカレンダーを扱うことを学ぶ際に置閏法についても簡単に触れられることがある。置閏法というのは閏年や閏月(太陰暦では1年が13ヵ月になるケースがあり追加の月を閏月と呼ぶ)をどのようなルールで挿入するかという話で、まさにアルゴリズムであるからプログラミングの話題と相性がいい。

置閏法

 現代の西暦の置閏法(ちじゅんほう)は

  1. 西暦を 400 で割り切れる年は閏年
  2. 上記以外で西暦を 100 で割り切れる年は平年
  3. 上記以外で西暦を 4 で割り切れる年は閏年
  4. 上記以外は平年

といった手続きで閏年(つまり2月29日がある)か平年かを判定することができる。

 2000年問題といえば西暦を2桁で扱っていたシステムの桁溢れというのが表の問題だったが、裏の問題として閏年というのもあった。2000年は400で割り切れるから閏年なわけだが、どういうわけか100で割り切れるので平年だと判定してしまうバグが見られた。単に4で割るルールだけにしておけば結果的に2000年には問題が起きなかったのに、中途半端に100年ルールだけ追加しちゃったもんだから2000年2月29日にトラブルを出すことになったシステムがいくらかあったようだ。

 やるなら半端は良くない。

その暦はいつからのものか?

 そう、半端は良くない。

 先に述べた置閏法は現代の西暦の置閏法であるが、この暦をグレゴリオ暦という。2014年にJava8が出るまでの古いカレンダークラスの場合(Java8以降のDate Time API の話は後述)、抽象クラスである java.util.Calendar に対して具象クラス java.util.GregorianCalendar が定義されていた。グレゴリアンの名前の通り、グレゴリオ暦をモデル化したものである。さあこれで、ここまで漠然と「西暦」と呼んでいたものがグレゴリオ暦という名前をもって具体化してきた。このグレゴリオ暦ローマ教皇グレゴリウス13世の名前からきている。グレゴリウス13世によってそれ以前の暦であるユリウス暦の改良が命じられ、1582年10月15日からグレゴリオ暦が行用されるようになった。

 旧暦であるユリウス暦ガイウス・ユリウス・カエサルにより紀元前45年1月1日から実施されたものである。西暦1年を考える場合、当時の暦はユリウス暦であることに注意せねばならない。

 西暦1年当時は閏年だったかどうかぱっとわからん人おる? 僕はぱっとわからなかった。

ユリウス暦の置閏法

 ユリウス暦の置閏法は

  1. 西暦を 4 で割り切れる年は閏年
  2. 上記以外は平年

とシンプルである。なんだ、ユリウス暦だとしても100の倍数の年以外は一緒じゃないか、と安心した人も多いだろう。
ただし、上記は紀元8年以降の話である。

紀元前45年にカエサルがこの暦法を導入した際に閏年は4年に1回と決められたが、直後の紀元前44年にカエサルが暗殺された後、誤って3年に1回ずつ閏日が挿入された。この誤りを修正するため、ローマ皇帝アウグストゥスは、紀元前6年から紀元後7年までの13年間にわたって、3回分(紀元前5年、紀元前1年、紀元4年)の閏年を停止した

ユリウス暦 - Wikipedia

あぶない! 西暦1年というのはユリウス暦の導入直後の混乱期で閏年が正しく運用されていなかったようだ

 なお紀元前45年から紀元8年までの間に、どの年に閏年が置かれていたのかについては諸説ある。1999年にローマ暦とエジプト暦の両方の日付が記載された紀元前24年当時の暦が発見され研究に進展があったとのこと。日本語版Wikipediaには該当論文が示されていないが、英語版Wikipediaの記述からすると ALEXANDER JONES 氏の1999年の論文 CALENDRICA II: DATE EQUATIONS FROM THE REIGN OF AUGUSTUS (PDF)が該当のようである。このカレンダーに基づくC. J. Bennett氏の2003年の研究によれば紀元前44, 41, 38, 35, 32, 29, 26, 23, 20, 17, 14, 11, 8年が3年おきの閏年でAD 4から4年おきの置閏法へと戻ったとのことである。

暦法紀年法

 西暦1年あたりの閏年の混乱を見てきたが、そもそもカエサルの時代に「西暦を4で割る」なんて考え方はされていなかったはずである。何しろ紀元前なので。

 ここで暦法紀年法について確認しておきたい。おおざっぱに言えば紀年法は年を数える方法論で、1年をどのように捉えるかの暦と分離可能なのである。例えば日本では和暦が用いられるが、これは紀年法が和暦で、暦法としてはグレゴリオ暦を用いている。なお、日本が現代の暦になったのは明治5年(西暦1872年)11月の改暦以降のことなので、これより前の時代を扱う場合は暦が連続しないので注意が必要である。このあたりの変遷は国立国会図書館江戸から明治の改暦が読みやすいだろう。


 そもそも西暦自体が525年にローマの神学者ディオニュシウス・エクシグウスによって算出されたものである。

525年、ディオニュシウスはローマ教皇ヨハネス1世の委託を受けてキリスト教の移動祝日である復活祭の暦表(復活祭暦表)を改訂する際に、当時ローマで用いられていたディオクレティアヌス紀元(ローマ皇帝ディオクレティアヌスの即位を紀元とする)に替えて、イエス・キリスト受肉(生誕年)の翌年を元年とする新たな紀元を提案した。これはディオクレティアヌスキリスト教の迫害者であり、その迫害者の名を残す事が疎まれたからである[3]。

西暦 - Wikipedia

 つまり、西暦525年から遡って元年を決めたわけである。なお、ディオニュシウスの推定したキリストの誕生年と、ナザレのイエスの実際の誕生年にはズレがあるとされており、紀元前4年頃に生まれたと考えられている。これはヘロデ大王の治世(紀元前37年 - 紀元前4年)の末期に生まれたという記述による。

 では、西暦525年よりも前の時代の紀年法ディオクレティアヌス紀元で遡ればいいかというと、ディオクレティアヌス紀元の元年も西暦284年までしか遡れない。西暦1年は遠いのである。ディオクレティアヌス紀元以前はローマ建国紀元(Ab urbe condita, AVC/AUC)が用いられていたようである。ローマ建国紀元の元年は、紀元前1世紀のローマ人ウァッロによって推定された、都市国家ローマの建国年(紀元前753年)ということである。これもまた、推定で過去に遡って元年が定められたパターンである。さらに前は執政官ふたりの名前を挙げて表したようだ。(紀年法暦法についてはここで取り上げたもの以外もたくさんあるということは付記しておく)

 さて、ここで何が言いたいかというと「西暦を4で割りきれるなら閏年」というのはたまたまであるということ。神学者ディオニュシウスの推定で定められた西暦の紀元からの数字だとたまたま4で割りきれる年が閏年の年となった。後付けである。その西暦をもとに1582年に定められたグレゴリオ暦が、閏年の調整のためにきりがよい100で割りきれる年や、400で割り切れる年を用いた、という後付け設定によるものだ、という点である。

 グレゴリオ暦の前、ユリウス暦は「西暦を4で割りきれる年を閏年をする」と定められたわけではなかった。ユリウス暦が定めらたのは紀元前のことで、初期には閏年が誤って3年おきに入れられたりその調整で閏年がない期間があったりした。それから500年余りたった頃に後付けで西暦が定められた。そうすると、たまたまユリウス暦閏年が西暦を4で割り切れる年だった。しかし単純に4年1度の閏年では春分の日がだんだんずれてきて西暦1582年にグレゴリオ暦に改められ、閏年の100年ルールと400年ルールが生じた。西暦を100で割る、400で割る、というのは後付けの西暦の切りのいい数字を用いたに過ぎない


 なお、1582年から施行されたグレゴリオ暦暦法を、1582年以前にも適用したものは proleptic Gregorian calendar と呼ばれるようだが、日本語では定訳がなく、先発グレゴリオ暦、遡及グレゴリオ暦、予測的グレゴリオ暦、予期的グレゴリオ暦などと訳されるようだ。

曜日

 400年間のうち閏年は97日あるため、日数は 365 × 400 + 97 = 146,097 日 となる。これは7で割ることができ400年で20,871週間あることになる。グレゴリオ暦は曜日も含めて400年周期の暦と言える。

 曜日の算出にはツェラーの公式が用いられることがあるが、このツェラーの公式を用いた曜日計算も1582年10月15日のグレゴリオ暦への切り替え以前に適用できないことは注意が必要である。計算することはできても、それは実際にその時代に用いられた暦ではないナニカ(先発グレゴリオ暦)である点は理解しておきたい。

 ユリウス暦グレゴリオ暦との間には約10日のズレが生じていた。切り替えに際してこの10日のズレを調整したわけだが、曜日については連続した形で適用された。つまり、ユリウス暦での 1582年10月4日(木曜日)の翌日がグレゴリオ暦 1582年10月15日(金曜日)とされた。

 また、切替えはヨーロッパ各国で一斉に行えたわけではなく、イタリア、スペイン、ポルトガルなどごく少数の国であったという。ローマ教皇による発令だったが、ローマ教皇というのはカトリックのトップであって、プロテスタントやその他のキリスト教派閥にまでその威光が通じたわけではなかったのだろう。その後、徐々に広まっていったようである。

Javaの実装

 1995年の初期のJavaから存在するjava.util.Dateクラスは、当初は日付を表す文字列をパースしたりする機能も有していたのだが、1997年のJava1.1にはこれらの機能は早々に非推奨(Deprecated)になってしまっている。そうした機能はjava.util.Calendarに分離された。java.util.Calendarはカレンダーの抽象クラスで、グレゴリオ暦に相当するのはjava.util.GregorianCalendarである。

 Java8 (2014年)が出た際にはDate and Time APIというものが追加された。(JSR 310: Date and Time API
Java8以降でプログラミングするのであれば概ね新しいDate and Time APIを使いたいところだ。

 さて java.util.GregorianCalendar だが、

歴史的に、グレゴリオ暦を最初に採用した国々では、1582年10月4日(ユリウス歴)のあとに1582年10月15日(グレゴリオ歴)が続きました。 このカレンダはこれを正確にモデル化しています。 グレゴリオ暦への切換え日の前は、GregorianCalendarではユリウス暦を実装しています。 グレゴリオ暦ユリウス暦の唯一の違いはうるう年のルールです。 ユリウス暦は4年ごとにうるう年を指定しますが、グレゴリオ暦では、400で割り切れない世紀の初年をうるう年にしません。

GregorianCalendarは、先発グレゴリオ暦およびユリウス暦を実装します。 すなわち、日付の計算では、現在のルールを無限の過去あるいは未来に向けて適用します。 このため、GregorianCalendarはすべての年について一貫した結果を生成するために使用できます。 ただし、GregorianCalendarを使用して得られた日付は、歴史的に、現代と同様のユリウス暦が採用されたAD 4年3月1日以降の日付だけが正確です。 この日付より前には、うるう年のルールは不規則に適用されており、BC 45年以前にはユリウス暦は存在さえしていませんでした。

GregorianCalendar (Java SE 11 & JDK 11 )

マジか!

Calendar c = new GregorianCalendar(1582, Calendar.OCTOBER, 4);
System.out.print(c.getTime());
c.add(Calendar.DAY_OF_MONTH, 1);
System.out.print(c.getTime());

Thu Oct 04 00:00:00 JST 1582
Fri Oct 15 00:00:00 JST 1582

 1582年10月4日(木)の翌日が1582年10月15日(金)になっている……。
 100年ルールも確認してみよう。1600年は400年ルールが適用なので、1500年と1700年で比較してみよう。

Calendar c = new GregorianCalendar(1500, Calendar.MARCH, 1);
c.add(Calendar.DAY_OF_MONTH, -1);
System.out.println(c.getTime());

c = new GregorianCalendar(1700, Calendar.MARCH, 1);
c.add(Calendar.DAY_OF_MONTH, -1);
System.out.println(c.getTime());

Sat Feb 29 00:00:00 JST 1500
Sun Feb 28 00:00:00 JST 1700

 ユリウス暦である1500年3月1日の前日は2月29日となっている。一方、グレゴリオ暦の1700年は100年ルールが適用されて3月1日の前日は2月28日だ。
 先発グレゴリオ暦のつもりでjava.util.GregorianCalendarを使うと、予期しない動きをするかもしれない。

 なお、純粋に先発グレゴリオ暦として用いたい場合は setGregorianChange(java.util.Date) メソッドでグレゴリオ暦への切り替え日をより過去にすることで対処することができる。

ISO 8601

 ISO 8601 で日付と時刻の表記に関する国際規格が定められている。

 端的に言えばこのISO 8601は先発グレゴリオ暦で、1582年10月15日以前にも適用される。ただし、0000年から1582年の範囲は、事前に通信の送信側と受信側との間での合意がある場合にのみ使うことができるとされている。

 このISO 8601に相当するのが IsoChronology である。Java8 以降の Date and Time API での標準はこのIsoChronologyとなっている。

 java.time.chronoパッケージの解説によればIsoChronologyの他に

が標準で実装が提供されている。これらの暦にまで踏み込むと話が長くなりすぎるので打切り。太陽太陰暦での閏月などについては興味のある人は調べてみると面白いだろう。

結論

 結果的に言えば西暦1年は閏年ではなく平年である。

 しかし、その導出過程は「西暦を4で割り切れないから」ではなく、「ユリウス暦は初期には閏年が誤って3年おきに入れられ、その調整で閏年がない期間があり、西暦1年はその期間に該当するから」というのがより正確な回答であろう。

 なんらかのアルゴリズムを学んだ時に、なぜそのアルゴリズムなのか、その適用範囲はどこまでなのか、現実に人がそのように使っているのか? といった部分にも興味をもって目を向けてみると少し世界がひろがって少し面白い世界が知れるかもしれない。

OSSの通らない自動テストを追いかけた話

備忘録。

MyBatis Migrationsソースコード を落としてきて自動テストを叩いてみたけども通らないテストがあったんですよ。(masterブランチ commit 29945670f2937e3c9f107d9f59e6b03d7e58926e 2020-10-19版)

java.lang.AssertionError:
Expected: (an instance of java.io.FileNotFoundException and exception with message is "/tmp/NoSuchFile.sql (No such file or directory)")
but: exception with message is "/tmp/NoSuchFile.sql (No such file or directory)" message was "\tmp\NoSuchFile.sql (指定されたパスが見つかりません。)"
Stacktrace was: java.io.FileNotFoundException: \tmp\NoSuchFile.sql (指定されたパスが見つかりません。)
at java.io.FileInputStream.open0(Native Method)
at java.io.FileInputStream.open(FileInputStream.java:195)
at java.io.FileInputStream.(FileInputStream.java:138)
at java.io.FileReader.(FileReader.java:72)
at org.apache.ibatis.migration.commands.BaseCommand.copyTemplate(BaseCommand.java:173)
at org.apache.ibatis.migration.commands.BaseCommandTest.testNonexistentFile(BaseCommandTest.java:61)
(後略)

該当のテストコードはこちら

  @Test
  public void testNonexistentFile() throws Exception {
    String srcPath = "/tmp/NoSuchFile.sql";
    expectedException.expect(FileNotFoundException.class);
    expectedException.expectMessage(is(srcPath + " (No such file or directory)"));

    File dest = File.createTempFile("Out", ".sql");
    try {
      BaseCommand.copyTemplate(new File(srcPath), dest, null);
    } finally {
      dest.delete();
    }
  }

expectedException には @Rule がついてて、要するに実行してエラーが出たら、エラーメッセージが想定通りかを検証している自動テストなわけです。
スタックトレースから "/tmp/NoSuchFile.sql (No such file or directory)" が期待されるメッセージだったけど "\tmp\NoSuchFile.sql (指定されたパスが見つかりません。)" というメッセージだったから合致しなかったよ、ということが読み取れます(基本)

試したこと

JUnitの実行時のJavaVMのオプションに -Duser.language=en を設定する。
これはシステム・プロパティの値を設定するもの。
詳しくは java コマンドのリファレンス で -D のところを見てください。
システム・プロパティ "user.language" でデフォルトの言語ロケールを変更することができます。

しかし、これを設定してもテストは通らず。

自動テストに手を加えて、直前でプログラムでシステムプロパティを変更してみます。

System.setProperty("user.language", "en");

しかし、これでも効果なし。なぜ?

原因探索

言語の設定をしてもエラーメッセージが変わらないので FileNotFoundException が作られるところでどういうコードになっているのか探すことに。

at java.io.FileInputStream.open0(Native Method)
at java.io.FileInputStream.open(FileInputStream.java:195)
at java.io.FileInputStream.(FileInputStream.java:138)

トレースを辿ると FileInputStream の open0 メソッドで Native Method になってしまいました。OSに合わせたC言語のネイティブコードになってるわけですね。

でも大丈夫! JavaGPLオープンソースなのでネイティブメソッドもばっちり公開されています。
Javaのコードは元々Mercurialというバージョン管理ツールで管理・公開されていたんですが、世の中のシェアを鑑みて最近Gitに変わりました。(JEP357)


さて、OpenJDKのリポジトリのソースを見てみましょう。
今回のターゲットは共通部は src/java.base/share/native あたりに、OS固有のものは src/java.base/【OS名】/native あたりにあります。

まずは FileInputStream.c から

JNIEXPORT void JNICALL
Java_java_io_FileInputStream_open0(JNIEnv *env, jobject this, jstring path) {
    fileOpen(env, this, path, fis_fd, O_RDONLY);
}

さらに辿って io_util_md.c の fileOpen()

void
fileOpen(JNIEnv *env, jobject this, jstring path, jfieldID fid, int flags)
{
    FD h = winFileHandleOpen(env, path, flags);
    if (h >= 0) {
        jobject fdobj;
        jboolean append;
        fdobj = (*env)->GetObjectField(env, this, fid);
        if (fdobj != NULL) {
            // Set FD
            (*env)->SetLongField(env, fdobj, IO_handle_fdID, h);
            append = (flags & O_APPEND) == 0 ? JNI_FALSE : JNI_TRUE;
            (*env)->SetBooleanField(env, fdobj, IO_append_fdID, append);
        }
    }
}

同ファイル内のwinFileHandleOpen()

FD winFileHandleOpen(JNIEnv *env, jstring path, int flags)
{
/* 中略 */
    if (h == INVALID_HANDLE_VALUE) {
        throwFileNotFoundException(env, path);
        return -1;
    }
    return (jlong) h;
}

さらに io_util.c の throwFileNotFoundException()

void
throwFileNotFoundException(JNIEnv *env, jstring path)
{
    char buf[256];
    size_t n;
    jobject x;
    jstring why = NULL;

    n = getLastErrorString(buf, sizeof(buf));
    if (n > 0) {
        why = JNU_NewStringPlatform(env, buf);
        CHECK_NULL(why);
    }
    x = JNU_NewObjectByName(env,
                            "java/io/FileNotFoundException",
                            "(Ljava/lang/String;Ljava/lang/String;)V",
                            path, why);
    if (x != NULL) {
        (*env)->Throw(env, x);
    }
}

さらに jni_util_md.c の getLastErrorString()

JNIEXPORT size_t JNICALL
getLastErrorString(char *buf, size_t len) {

    DWORD errval;

    if ((errval = GetLastError()) != 0) {
        // DOS error
        size_t n = (size_t)FormatMessage(
                FORMAT_MESSAGE_FROM_SYSTEM|FORMAT_MESSAGE_IGNORE_INSERTS,
                NULL,
                errval,
                0,
                buf,
                (DWORD)len,
                NULL);
        if (n > 3) {
            // Drop final '.', CR, LF
            if (buf[n - 1] == '\n') n--;
            if (buf[n - 1] == '\r') n--;
            if (buf[n - 1] == '.') n--;
            buf[n] = '\0';
        }
        return n;
    }

    // C runtime error that has no corresponding DOS error code
    if (errno == 0 || len < 1) return 0;
    return strerror_s(buf, len, errno);
}

ここで FormatMessage というのが Win32 APIFormatMessage ということのようです。

DWORD FormatMessage(
DWORD dwFlags,
LPCVOID lpSource,
DWORD dwMessageId,
DWORD dwLanguageId,
LPTSTR lpBuffer,
DWORD nSize,
va_list *Arguments
);

というパラメータになっていて dwLanguageId に 0 が渡されていますね。

If you pass in zero, FormatMessage looks for a message for LANGIDs in the following order:

1. Language neutral
2. Thread LANGID, based on the thread's locale value
3. User default LANGID, based on the user's default locale value
4. System default LANGID, based on the system default locale value
5. US English

ここでメッセージが日本語にされていたようです。OSレベルのメッセージだったからJavaシステムプロパティで言語設定を変えても影響されず、日本語のメッセージが表示された、というわけでした。

結論

こんな自動テストは消してしまおう……!

ファイルパスの違いは JUnit の OS指定の @EnabledOnOs アノテーションで逃げることができるのですが、今回はメッセージの部分まで含んでいるのでこのアノテーションでの対処では不十分です。

OS に依存するメッセージを固定でテストするというのは過ぎた制約で、テストとしてはおよそやりすぎの類のものです。
テストコードに手を加えずに通したいなら言語設定が英語のmac使うといいんじゃないかな。

Spring boot で作るAPIと React のビルドを共存させたい

Spring boot では Webシステムをmain起動のスタンドアロンJavaプログラムのように起動して試せる点が便利である。ところでこうしたサーバーサイドのWeb APIを作るようなケースで、Reactのようなコンパイルを伴うWebクライアントと連携して動作するようなものを開発するとき、どのようなプロジェクトのディレクトリ構成でどのようなビルド、どのような実行をすると便利にやれるのだろうか?

IDEの有償プラグインがこうした開発を統合してくれるのかもしれないが(試用して回ったわけではないのでよくわからない)ここではギョームで普通に用いられているEclpise IDEVisual Studio Codeの組み合わせで考えてみたい。(本稿ではIDEは主題ではないので、任意のIDEで検討してもらいたい)

Spring の フォルダ

SpringのQuick Start に従い、デモプロジェクトを作成し、Eclipse側でMavenプロジェクトとしてインポートすると以下のような構成になっている。

f:id:Nagise:20200627145159p:plain
Spring boot の構成

ここで、DemoApplicationを実行するとSpring bootが内臓のTomcatを起動させ、 src/main/webapp 以下(図のオレンジの矢印の部分)が http://localhost:8080/ 以下のパスでアクセスできるようになる。
加えて、@GetMapping("/hello") のアノテーション定義に則って http://localhost:8080/hello にアクセスすると、そのアノテーションがついているhelloメソッドを呼び出し、その戻り値をhttpのレスポンスとして返すように動く。

React の フォルダ

create-react-app を使ってシンプルなReactのサンプルを作成すると、以下のような構成になっている。

f:id:Nagise:20200627145306p:plain
React の構成

npm run build でコンパイルすると、html や JavaScript が動く形で build ディレクトリに作成される(図のオレンジの矢印部分)。Webアプリケーションとしてサーバーに配置するのはこのbuild部分ということになる。

双方の都合

サーバーサイドのWeb APIと、コンパイルして生成されるWeb クライアントを複合して動かす場合に、上記のような2系統のビルドをどうにか両立させないといけない。

最終的にTomcatのようなWebサーバーに配備する場合であれば、ビルドスクリプトによってReactのビルドを動かし、生成物を Spring boot 側の webapp 以下の配備予定パスに複製、そののち、 Spring boot 側をwarファイルにパッケージングしてwarファイルをWebサーバーに配備、という手順を踏むことになるのだろう。しかし、開発段階からこれをやるのは面倒だし、開発時にはクライアント側もサーバー側も双方ともいじりながら開発することもあるから、両開発環境でビルドがしたい。(逆に、完全に分業がされているならば、この2系統ビルドで頭を悩まさなくていい)

この2系統ビルドとその実行を便利にしたい、というのが今回のテーマである。

方法はいろいろあるのだろうが、今回は Spring boot をハックして Tomcat の Resources Component機能を利用してデバッグ環境のTomcatがSpring boot の src/main/webapp ディレクトリと、Reactのbuildディレクトリの双方を参照するように設定してみたい。

Tomcatによるリソースの読み込み

あまり知られていないかもしれないが、Tomcatにはファイルリソースを読み込むにあたって、ファイルを読み替えることができる。

日本語で解説してくれている記事は少ないが、 webapps配下でないディレクトリにリソースを置く - DukeLab では例が挙げられている。
Tomcatがリソースを読み込む際に PreResources の設定があれば、先にそちらを読みに行き、そこにリソースがなければ通常のwebappフォルダ、そこにもなければ PostResources の設定の場所を読みに行く。これによって、設定ファイルなどをwarファイルの外に分離したりすることができる。

TomcatThe Resources Component というドキュメントに詳細が記載されている。
この機能を用いる際には注意点がある。Javaプログラム中で設定ファイル等にアクセスする際には java.io.Fileクラスなどを用いてはいけない。 javax.servlet.ServletContext の getResource() などのメソッドを用いてファイルリソースを取得することを徹底しなくてはならない。HTMLなどの静的ファイルにだけ用いる分にはこのあたりはあまり気にする必要はない。

この Resources Component機能の設定は古典的には Tomcat本体のserver.xmlを編集することになる。このserver.xmlは、Tomcatがwarファイルをどのように配備するかといった設定を記載するファイル。しかし、warファイル側でこのデフォルト設定みたいなのを書いておきたいみたいな要件があり、 warファイルの /META-INF/context.xml を用意しておくと、配備時にこの設定が生かされるようになった。

これを Spring boot の内蔵Tomcatに対してどう設定するかという話なのだが、/META-INF/context.xml を用意しても内蔵Tomcatには効かない。なにか方法はないかと探していたら
SpringBootで組み込みTomcatの設定 という記事が目についた。Spring boot では TomcatServletWebServerFactory内蔵Tomcatの設定を行えるらしい

しかし、APIドキュメントを探してみてもResources Componentの設定は行えなさそうだ。さて、どうする……?

WebResourceRoot

Spring boot の TomcatServletWebServerFactory のソースコードを読み込んでいると、org.apache.catalina.WebResourceRoot というTomcatのクラスの createWebResourceSet というメソッドを呼んでいる箇所が気になった。

	URL url = new URL(resource);
	String path = "/META-INF/resources";
	this.context.getResources().createWebResourceSet(ResourceSetType.RESOURCE_JAR, "/", url, path);

前後を読むとこれが直接の該当機能というわけではないのだが、この createWebResourceSet メソッドこそが求める Tomcat の Resources Component 機能であるようだ。WebResourceRoot は interface なので Tomcat のソースを辿り実装クラス StandardRoot を見つける。このソースを見るとこれが探していた機能であることを確信する。これを Spring boot で Tomcat を設定する TomcatServletWebServerFactory でうまく呼び出せれば……?

import org.apache.catalina.Container;
import org.apache.catalina.Host;
import org.apache.catalina.WebResourceRoot;
import org.apache.catalina.WebResourceRoot.ResourceSetType;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.startup.Tomcat;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.embedded.tomcat.TomcatWebServer;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class TomcatConfigration {
    @Bean
    public ServletWebServerFactory serverSettings() {
        TomcatServletWebServerFactory factory = new MyTomcatServletWebServerFactory();
        return factory;
    }

    static class MyTomcatServletWebServerFactory extends TomcatServletWebServerFactory {
    	@Override
    	public WebServer getWebServer(ServletContextInitializer... initializers) {
    		TomcatWebServer ret = (TomcatWebServer) super.getWebServer(initializers);

    		Tomcat tomcat = ret.getTomcat();
    		Host host = tomcat.getHost();
    		Container[] findChildren = host.findChildren();
    		StandardContext context = (StandardContext) findChildren[0];
    		WebResourceRoot webResourceRoot = context.getResources();
    		webResourceRoot.createWebResourceSet(ResourceSetType.PRE,
    				"/", "C:\\work\\react\\demo\\react-demo\\build", null,  "/");
    		return ret;
    	}
    }
}

単純にWebResourceRootを得て素直にcreateWebResourceSet()を呼び出せなかったため工夫する必要があった。

TomcatServletWebServerFactory を継承したクラスをつくり、getWebServer()をオーバーライドしてsuper.getWebServer(initializers)で作成した TomcatWebServer オブジェクトから WebResourceRoot を引っ張り出し、createWebResourceSet() に react側のbuildディレクトリを指定するようにしてみた。

f:id:Nagise:20200627151041p:plain
Spring boot と React の共存


このように、Spring boot の動的なAPIと、React が Spring boot の内臓Tomcatによって同時させることができた。
URL がともに localhost:8080 となっている点に注目いただきたい。

まとめ

  • Spring boot の内蔵 Tomcat に設定を加えることで、外部のファイルリソースを組み合わせることができた
  • Reactのような別系統のビルドが必要な環境と組み合わせて開発する際に便利

よりよい工夫などあれば、トラックバックして頂けると嬉しい。

追記

Reactに限って言えば、React側のサーバーにproxyを指定してやることで、Spring boot側のAPIを同一ドメインで呼び出せるようにすることができるようだ。

React開発時には、APIサーバーとReactアプリサーバーを別にして、プロキシを使うというベスト・プラクティス

本来静的コンテンツであるところの HTML + JavaScript の動作を確認するためにWebサーバーを立てているわけだが(npm startで起動させているアレである)そこのProxyを通して本来の動的WebサーバーであるところのTomcatを呼び出している。僕がサーバーサイドエンジニアだからか、この方式は少し不思議な構成という気がする。


Vue.js でも同様のアプローチをとっているようだ。

[Vue]devServerでフロント開発中、別ポートで起動しているバックサーバーにリクエストを送る方法 - Qiita


本記事のアプローチはこれらとは逆のような感じになるが、React や Vue.js に限らず、汎用に用いることができる方式であるし、なんとなれば、React と Vue.js の混在にでも対応できる。(そんなことをしたいわけでもないが)

また、proxyがどのような通信方式をサポートしているかという点にも不安があって、例えば WebSocket を持ちいたAPIサーバーなどに対応できているのか?などなど、サーバーアプリ屋としてはサーバーをメインに据えたい気持ちがいろいろとある。

しかし、視点を変えて、分業体制とかでクライアントサイドを主体としている開発者側からはproxy方式の方が便利なのかもしれない。

セミコロンレスJava はチューリング完全か?

セミコロンレスJava (Semicolonless Java)というのは Javaセミコロンを使わずにコードを書くというチャレンジである。ネタであり言語の構文を駆使して行うプログラミングパズルの類の遊びである。(ときどき本気にしてその有用性について聞いてくる人がいるので、この冗談がどういう冗談でどう面白いのかの解説のような無粋なことを書いている)

ところで、セミコロンを使わずともJavaチューリング完全なのであろうか?

証明方法

チューリング完全であるという証明は、すでにチューリング完全であることが知られている体系を該当環境で実装することで行うのが簡単な方法論である。

Brainf*ck といったチューリング完全な簡易プログラミング言語を実装するとか、チューリング完全であることが知られているライフゲームを実装するとかが考えられる。

今回は、ライフゲームの1次元版とも言える、1次元セルオートマトンを作成することでこの証明を行う。

Elementary cellular automaton (基本セルオートマトン)は 0 と 1 の連続したセルからなり、自身とその両隣、つまり3bitの組み合わせから、次世代のセルの値が定まる。

f:id:Nagise:20200527201721g:plain
基本セルオートマトン

この3bitの組み合わせ8種類が、それぞれ次世代に0になるか1になるかの組み合わせが2^8通り考えられ、この組み合わせを0~255の値で表し、ルール110 などと呼ぶ。

ルール110チューリング完全になることが知られている。

実装

Githubにも上げてあるが、コードは短いので全文を記載する。


public class ElementaryCellularAutomaton {

	public static void main(String[] args) throws Exception {
		synchronized (new ElementaryCellularAutomaton(new Rule(110), "0000000000000001", 20)) {}
	}

	/**
	 * Run Elementary cellular automaton.
	 *
	 * @param rule
	 * @param states initial states. made of "0" and "1"
	 * @param generation
	 */
	public ElementaryCellularAutomaton(Rule rule, String states, int generation) throws Exception {
		synchronized (new Output(states)){}

		while (--generation  > 0) {
			synchronized (new Output(states = new Next(states, rule).peek().toString())){}
		}
	}

	/**
	 * Calculate the next generation.
	 */
	static class Next extends java.util.Stack {
		/**
		 * Calculate the next generation.
		 * @param states
		 * @param rule
		 * @return String : next generation
		 */
		public Next(String states, Rule rule) {
			synchronized(push(new Next(states, (char[])rule.peek(), 0, new StringBuffer()).peek())) {}
		}

		private Next(String states, char[] map, int x, StringBuffer sb) {
			synchronized (sb.append(map[(states.charAt(states.length()-1)-'0')*4
					+(states.charAt(0)-'0')*2
					+states.charAt(1)-'0'])) {}
			while (x<states.length()-2) {
				synchronized (sb.append(map[(states.charAt(x) - '0')*4
						+(states.charAt(x+1) - '0')*2
						+states.charAt(x++ +2) - '0'])) {}
			}
			synchronized (push(sb.append(map[(states.charAt(states.length()-2)-'0')*4
					+(states.charAt(states.length()-1)-'0')*2
					+states.charAt(0)-'0']))) {}
		}
	}

	/**
	 * https://en.wikipedia.org/wiki/Elementary_cellular_automaton#The_numbering_system
	 */
	static class Rule extends java.util.Stack {
		/**
		 * create Rule Object.
		 * @param ruleNo the rule number of the automaton.
		 */
		public Rule(int ruleNo) {
			synchronized(push(new Rule(110, 0, new char[8]).peek())) {}
		}

		private Rule(int ruleNo, int i, char[] map) {
			while (i<8) {
				switch (map[i++] = (char)('0' + (ruleNo & 1))) {}
				switch (ruleNo >>= 1) {}
			}
			synchronized (push(map)) {}
		}
	}

	/**
	 * System.out.println() utility
	 */
	static class Output {
		/**
		 * Output message to System.out
		 * @param message message
		 */
		public Output(String message) throws Exception {
			if (null == java.io.PrintStream.class
					.getMethod("println", new Class[] {String.class})
					.invoke(System.out, new Object[] {message})){}
		}
	}
}

動かし方

コンパイルしてmainを実行するだけ。

パラメータはハードコーディングしてしまっているので、いじりたい場合は以下の部分の値を変更してください。

synchronized (new ElementaryCellularAutomaton(new Rule(110), "0000000000000001", 20)) {}

new Rule(110) は先に解説した基本セルオートマトンのルールの値である。

"0000000000000001" は基本セルオートマトンの初期値。0と1からなる文字列で指定する。とりあえず16文字にしているが、この文字数は多くすることが可能。

20 は出力する世代数。大きな値にすればそのぶん先の世代まで出力してくれる。

技法

本コードは Java 1.1 相当のコードとなっている。一般にセミコロンレスJavaは古いJavaほど構文の制約が多く、難易度が高いとされる。Java1.0での Hello world は未解決問題となっており、Java1.1が事実上の最難関とされる。

セミコロンレスJavaの構造化の手法は過去記事 セミコロンレスJavaの構造化&オブジェクト指向 - プログラマーの脳みそ が詳しい。
セミコロンレスJavaではreturn文が書けないため、戻り値のあるメソッドは定義することができない。そして、戻り値のないメソッドは呼び出しが式にできないためセミコロンなしで呼びだせない。(リフレクションを用いることで回避することができるが、記述が煩わしい)

そこで、java.util.Stackを継承したclassをnewすることでコンストラクタをメソッド呼び出しの代替とし、peek()で戻り値となる値を読みだすという手法で構造化を行う。

Java5 以降であれば、for-each構文でローカル変数宣言を行うことが可能なのだが、Java1.1ではその手法が用いれないためコンストラクタの引数を用いて変数宣言の代用としている。

結論

Java 1.1 はセミコロンを用いずともチューリング完全であることが示された。