リファクタリングの価値の考察シリーズ6 - 経済的価値

2022年のリファクタリングの価値の考察

2026年の新シリーズ 2 - 構造抽出性

シリーズ 3 - 瞭然化

シリーズ 4 - リファクタリング不可能点

シリーズ 5 - 試験性と検証可能性

さて、ここまで、リファクタリングの価値の考察について、Epiplexity 構造抽出性という概念を導入して再検討してきた。これをまとめよう。

構造抽出性はいろいろなことに派生することを主張してきた。

構造抽出性を上げる → 試験可能な境界を作る → 検証可能性を上げる → 将来の変更コストを下げる

といったことを主張しているが、その効果の程度は冷静に評価する必要がある。

 

リファクタリング不可能点の示唆すること

構造抽出性が著しく悪い状況について、デファクタリングによって組み合わせ爆発をさせることでリファクタリング不可能点が生じることを予言した。

これは現代のソフトウェアによる暗号が、解読するためには非現実的な計算量となるので実質的に解読不能であるとされるようなことに似ている。構造を組み合わせ爆発が起きるように複雑に複雑にしていくと、構造の抽出に非現実的な計算量が求められるようになり、現実的には構造抽出が不可能になるだろう。

 

もし未来にAIがより発達したとしても、組み合わせ爆発による複雑さから生じる構造抽出性の低下はどうしようもなく、どうにもできない。

 

これはソフトウェアの保守性の最悪の状況で何が起きるのか、最大ダメージについての検討である。最悪ケースのダメージが大したことないのであれば、無視をするという戦略もありうるが、リファクタリング不可能点が示すのはソフトウェアの全損である。

 

例えば巨大システム開発の事例として、みずほ銀行のシステム統合プロジェクト MINORI がよく例に挙げられるが、総投資額は4000億円以上と言われる。こうした巨大投資をして作られたコンピュータシステムが解析不能、改修不能となれば、ビジネスとの乖離が大きくなってきたときに4000億の投資をゴミ箱に捨てて、代替新システムへ更新しなくてはならない。その代替新システムを作るための投資もそのような規模の金額を要すると思われ、極端なこと言えば構造抽出性がそのぐらいの経済的価値を左右することになる。

 

構造抽出性の悪化によりリファクタリング不可能点を超えると、システムが何をしているのかさっぱりわからない巨大な塊となり、システム都合でビジネスが硬直することになる。その上、セキュリティリスクに対しての対処も困難であれば急なシステム停止のリスクも高まる。そうしたビジネス上のリスクが生じるということはまず理解しておきたい。

 

極端ではないケースでのリファクタリング価値

さて、最悪のケースについてまず検討した。しかし、世の中のIT業もそこに陥らないように格闘してきたわけで、大抵のITシステムというのはリファクタリング不可能点近傍という危機的状況にあるわけではない。

 

ソースコードの解析性(analyzability)もピンキリだろうが、もはや解析不可能です!と匙を投げるほどではないのが多数派だろう。多数派であってくれ。

こういったケースでの構造抽出性、ここでは解析性と相関するとして、構造抽出性の向上がどのような価値を産むと考えうるか?

 

構造抽出性はある程度、計測ができる。詳細は元論文 From Entropy to Epiplexity を参照して欲しいが、AIにコードを解析させて仕様をちゃんと読み取れているか?をスコアリングすることで構造抽出性を推定することができるだろう。ここで同じAIで計測したもの同士は比較可能だが、異なるAIで計測したものは比較できない点に注意されたい。あくまで相対指標である。

 

ここで、ある仕様のメソッドをAIによって解析させ仕様書を書かせた場合、欠落した仕様が30%ほどあったとしよう。これは、このAIでこのメソッドについて様々なタスクをやらせるにあたって、30%ほどの仕様欠損を含んだ仕事をすることになるだろう。これはAIの仕事に常にリスクを孕ませることになる。これは感覚的にかなり怖い。

ほぼ100%で仕様を汲み取ってくれる状況になければ、かなりAIの仕事が信用できなくなるだろう。これはAIが導入可能か、信用できるかどうかみたいなレベルで経済的なコストとなって影響するだろう。

 

ほぼ100%で仕様を汲み取ってくれる状況であれば、ようやくAIが読み取るコストの比較のような話になる。少ないトークンで読み取れるならAI利用料が安いといった経済的なコストの話になるだろう。これは上記のAIが使えるか使えないかみたいな話に比べれば些細な話である。

 

カテゴリ コードの状態 後続のAIタスク
解析不可能 コードの解析不可能 後続のAIタスクが不可能
不確実性な解析 解析可能だが欠損を含む 後続のAIタスクに不確実性
解析可能 欠損なく解析できる 後続のAIタスクが可能

 

この状態を三段階のカテゴリとして考えよう。手持ちのAIが欠損なく解析できる状態かどうかは世界が変わる境目と言える。

解析不可能な状態はもう手の打ちようがないわけで、別種のアプローチでシステムの再構築をする必要がある。

AIが欠損を含む段階では、AIにやらせたタスクに対し、常々人間による確認がついて回る必要がある。この段階では人間によるレビューがボトルネックになるだろう。

AIが概ね欠損なく構造抽出できる段階にくると、AIにタスクを任せることができるようになるだろう。

 

構造抽出性は観測者に依存した概念であり、AIが強くなることで構造抽出の精度が上がればそれにより世界が変わることはあるだろう。しかし、リファクタリング不可能点の議論でAIが強くなったとて構造抽出ができない組み合わせ爆発的な不可能があることを挙げた。

つまり、構造抽出性はAIの活用可否に関わり、AIによる生産性の恩恵を受けれるかどうかを左右させる。構造抽出性の計算量クラスクラスが大きいと強いAIでも解析不可能であろう。以下は概念図である(実際的にどのぐらいの計算量クラスだと解析不可能かは分からないし、コードのスケールが十分小さければ計算量クラスが例え階乗だろうと解析することができるだろう点には注意)

構造抽出性の計算量クラスとAIの強さ

コードの構造抽出性は、AIが活用できるかどうかの基底となる。思いつくだけでも以下のようなタスクに影響するだろう。

  • ソースコードを解析して要約して説明する
  • 仕様の抽出
  • 仕様の整合性チェック
  • ソースコードの修正作業
  • コードレビュー
  • テストケースの検討

こうしたタスクにAIが活用できるかどうかは、利用するAIがそのソースコードを解釈できるかどうか、構造を抽出できるかにかかってくる。

 

現代のAIが生成するソースコードは十分に構造抽出性が高いので、新規に作成したコードはリファクタリング不可能点にはそうそう陥らないと思うが、継ぎ足し継ぎ足しして長く守られてきた秘伝のソースのようなITシステムだとリファクタリング不可能点に近い状況にあるかもしれない。

こうしたシステムは塩漬け的に運用されていることが多いが、そのうちAIが発展すればリプレースできるのではないか?という楽観論も見られる。しかし、リファクタリング不可能点の議論は、そこに陥っているならばAIが発達しようが手に負えないことを示唆し、リプレース悲観論となる。

 

リファクタリングの経済的価値

本シリーズでは、リファクタリング、およびその拡張概念としての瞭然化は、構造抽出性を高めるという方針を持つと良いだろうということを述べてきた。

ITシステムの保守性は構造抽出性により「解析不可能」「不確実な解析」「解析可能」のカテゴリに分けることができる。そして、リファクタリング(本シリーズではコードのみならずドキュメント等を含めて構造抽出性を高める瞭然化を提唱するが)によって構造抽出性を高める行為は、プロジェクトが「解析不可能」「不確実な解析」「解析可能」のカテゴリを越境するときに大きな経済的価値を産む。

 

あるいは、構造抽出性が低くなっていくことも、これらのカテゴリを越境するときに大きな損失を産む。カテゴリ内では茹でガエル的にじわじわとした影響にとどまるだろうが、油断して放置し過ぎると気がつけば越境してしまう。

 

2022年の論考で、リファクタリング(≒保守性)の価値は将来の改修という不確実性に依存する、またシステムの価値はそのシステムがビジネス的にどれほどの価値があるかに依存する、としていた。

2026年の本シリーズでも、基本的な価値としては対象システムがビジネス的にどのぐらいのウエイトかにまず依存するが、不確実性のウエイトは減るのではないか。というのは、AIを前提とするならば、AIの時代には改修をやるぞ!とプロジェクトが動き出さずとも、AIがソースコードから構造を抽出する機会があるだろう。

 

そして、構造抽出性カテゴリとして「解析可能」を維持する経済的価値は大きく、この状態にあればAIによってシステムの変更性(changeability)は高いレベルで保たれるだろう。これはビジネス的にはシステムが即応できるという価値に繋がる。

しかし、構造抽出性カテゴリを跨がない微細なリファクタリングひとつひとつの価値となると、小さく評価されるだろう。カテゴリを跨がない限りはおおきく利益を得ることも大きくシステムが毀損することもない。

しかしシステムが荒廃していくとき、徐々に構造抽出性は低くなり、気がつけば構造抽出性カテゴリが「解析可能」から転落していることだろう。

リファクタリングの価値の考察シリーズ5 - 試験性と検証可能性

2022年のリファクタリングの価値の考察

2026年の新シリーズ 2 - 構造抽出性

シリーズ 3 - 瞭然化

シリーズ 4 - リファクタリング不可能点

 

今回は元祖リファクタリングの少し外側の話で試験性と検証可能性について。

 

リファクタリングの価値の考察新シリーズでは、From Entropy to Epiplexity という論文で提唱された Epiplexity という概念でリファクタリングを再評価すると面白いのではないか?という着想をあれこれと考察している。

 

リファクタリングのバイブルとされるのは2000年のマーチン・ファウラーの書籍「リファクタリング」だが、ここでのリファクタリングの定義は

 

リファクタリング(名詞):外部から見たときの振る舞いを保ちつつ、理解や修正が簡単になるように、ソフトウェアの内部構造を変化させること。

 

であった。しかし従来、この「理解や修正が簡単になるように」の部分が人間主観的であり、そのメトリクスは与えられてこなかった。

ここを Epiplexity、本シリーズではこれに構造抽出性という訳語をあてているが、この概念、つまりある有限の計算資源で、より多く情報の構造を抽出できることを良しとするという概念で捉えると良いのではないか?というのが本シリーズの中心テーマである。

 

リファクタリングとテスト

シリーズ2 - 構造抽出性 で元祖リファクタリングでのテストの扱いを以下のように解説した。

書籍「リファクタリング」では4章で「テストの構築」と題して自動テストの構築を解説しており、《リファクタリングを自動化するツールがたまたま使える状況にあったとしても、依然としてテストは必要です。》と述べている。

そして《よいバグ検出器を作り、それを頻繁に実行してください。これは、どのような開発にとっても強力なツールです。また、リファクタリングの事前条件でもあるのです。》と語って章を締めくくっている。

《外部から見たときの振る舞いを保ちつつ》を確認するため、保証するため、テストが必要である。

時代背景をおさらいしておくと 2000年当時はまだ JUnit による自動テストは普及期ではない。(1998年10月が初回のリリース)

 

書籍「リファクタリング」は「外部から見たときの振る舞いを保ちつつ」の検証のためにテストが必要であるということは述べているが、テストにフォーカスした書籍ではないため、テストの具体にはあまり踏み込んでいない。

 

試験性と検証可能性

ソフトウェアのテストは、

  • 同じ状態から
  • 同じ入力をすれば
  • 同じ出力がされる

という再現性を前提としている。

 

テストのためには、ソフトウェアをまずある一定の大きさのモジュールに区切る。そしてそのモジュールに対して(これはAPI単位や、メソッド単位でやることが多い)同じ状態、同じ入力をやり、同じ出力になるかを確認する。

 

この区切ることができるか、同じ状態を作ることができるか、その区切ったモジュールを実行することができるか、といった利便性が試験性(Testability)の概念であろう(ISOのソフトウェア品質特性のひとつ)。

 

この試験性とは別に検証可能性(Verifiability)があり、対象が要求・性質を満たすことを確認可能である性質とされる(これはISOの品質特性には挙げられていない)。

対象が要求・性質を満たしていることを、何らかの合理的な方法で確認できるか?入力値を全網羅することは出来ないにしても、意味のある区分けをし、その区域を網羅的に確認をした、などなど。

 

この検証が困難な事例としては Suica の運賃システムのテストの話が面白い。

 

運賃のパターンが10の40乗通りくらいあり、これをいかに絞り込んで実行可能なパターン数にするか?という話がされている。

こうしたシステムは検証可能性(Verifiability)が低い、検証困難である、と言えよう。

 

組み合わせ爆発を抑え構造を見出す

あるメソッドを対象に考えた時、その入力値を網羅するようにテストを行うことは、コンピュータをもってしても容易ではない。例えばただのintの引数がひとつあるだけで、約43億の入力値が考えられる。これがふたつになるとその2乗で約1845京という組み合わせになる。intふたつを引数とするだけで、もう現実的に困難である。

では実際にテストケースを考える際にはどうしているだろうか。入力値をコードや仕様から意味のある区間に分けて、組み合わせをずっと少なくしてテストをするだろう。
マイナスの値、ゼロ、プラスの値、といった区間でテストすれば良さそうだ、といった構造を見出す。

 

この構造を見出しやすいがどうか?がまさにEpiplexity 構造抽出性が関わってくるところだ。
これは仕様を元にしたブラックボックステストにせよ、コードを元にしたホワイトボックステストにせよ、元になる情報があり、それを人力にせよAIにせよ、解析して構造を見出し、テストケースを作り出すことになる。
明瞭な仕様であれば、コードであれば、テストを設計するのはたやすい。


本シリーズでは、リファクタリングの価値考察を構造抽出性を高めるという思想で再定義しようとしている。この構造抽出性の向上が何をもたらすか?そのひとつが、この検証可能性への作用であろう。

リファクタリングを拡張し、振舞いを変えずに構造抽出性を高める、とした概念をシリーズ3で瞭然化と呼んだ。瞭然化という拡張概念ではテストパターンの構造を抽出することもその範疇とすることができる。構造抽出性の向上という行為によってテストの改善も視野に入れることになる。

 

これらの要素は連鎖的に作用し、

構造抽出性を上げる → 試験可能な境界を作る → 検証可能性を上げる → 将来の変更コストを下げる

といった価値を期待することになるだろう。

次回、経済的価値のまとめ

 

リファクタリングの価値の考察シリーズ4 - リファクタリング不可能点

さて、リファクタリングについて、Epiplexity 構造抽出性という概念で考えると良いのではないか?という話を前回、前々回としてきた。

構造抽出性とはソースコードを見た際に、ここではどういう仕組みで何をしようとしているのか?といった情報を掴みやすいかどうかといった概念だった。従来よりEntropy情報量という概念があったが、実際のAIのような有限の計算資源の存在にとっては理想論すぎ、「実際に抽出可能な情報」というものについて考える必要があるというものだった。

 

この概念を考えるために、敢えて逆を考えてみよう。つまり、外部から見た振る舞いを変えずに、コードを理解しにくくするとどうなるか。何が起きるのか。

 

デファクタリング:外部から見たときの振る舞いを保ちつつ、理解や修正が困難になるように、ソフトウェアの内部構造を変化させること

 

リファクタリングの逆の操作をデファクタリングと呼ぼう。デファクタリングをやることで何が起きるのか

 

リファクタリング不可能点

 

構造抽出性が悪いとはどういうコードだろうか。一例としてグローバル変数のようなモノが挙げられる。局所変数は、その変数が局所的であることが保証されるが、広いスコープから参照・更新できるグローバル変数があると、変数がどのように変わるのか状態遷移を考えるにあたって、まさに組み合わせ爆発的なケースについて考慮を必要とする。

 

実際には局所変数Aと局所変数Bのように振舞うのだとしても、これらをまとめてグローバル変数Gとしたとしよう。このグローバル変数GをリファクタリングしてAとBに分離するためには、Gを参照・更新する箇所を網羅的に調べて、A・Bが混ざっていない、二つの独立した変数に置き換えうるということを確認しなくてはならない。これはとても困難な作業だ。

 

これがA・Bのふたつの変数ならまだ良いが、これを変数C・D・E……と掛け合わせていくと、まさに構造を把握するために指数関数的な組み合わせ爆発を考慮しなくては真の構造を見出すことができなくなる。これがデファクタリングの一例だ。

 

混ぜるのは簡単だが、分離するには全体を把握する必要がある。ここに計算量の非対称性があるわけだ。

 

構造を抽出するために必要となる計算量のクラスという概念を考えることが出来る。例えばオーダー記法でいう O(2^n) となる状態組み合わせを網羅的に確認する必要がある構造抽出性、といったように。デファクタリングによって計算量クラスがより発散しやすくなるようにすると何が起きるか?

 

組み合わせ爆発により現実的な計算資源では構造を把握できなくなる。

 

ある計算資源で現実的な時間で解析可能かどうかに線を引くことが出来、O(2^n) クラスの構造抽出性で、nがある程度大きいともう計算不可能だろう、構造を解析して掴むことが出来ないだろう、というリファクタリング不可能点が現れる。

 

この計算不可能性は組み合わせ爆発によるものだ。

 

www.youtube.com

 

このリファクタリング不可能点に達したコードは、文字通りリファクタリング不可能で、ここに陥ったコードはたかだかコンピュータの性能が数桁向上した程度では手に負えないシロモノとなる。人間でも手に負えないが、コンピュータを用いても手に負えない。鍵をなくした暗号化されたデータのようなどうしようもない塊になる。

 

このようなモノが存在しうるということは重要な示唆だ。ここに陥ったかのように見えるレガシーコードはあなたの周囲に存在しないだろうか?そのようなコードは、この先の未来にAIが強力になっていくとしても救えないことが示唆される。

 

現代の現実的なコードの構造抽出性

 

構造抽出性クラスを考えることで、どうやらリファクタリング不可能なコードという概念がありそうだということを示した。

では、我々が現代にAIを使ったりしつつ生産しているソースコードというのはリファクタリング不可能点に陥るのだろうか?

 

おそらく、これはかなり可能性は低いのではないか。AIが生成するコードは現代の知見に基づき相応に可読性の高い、保守性の良いコードであるということ。構造抽出性がもとよりそれなりに高いだろうとみている。
なのでなかなかリファクタリング不可能点にまで陥って解析不能・理解不能なコードの塊ができることがないのではないか。

 

しかし、AI生成によって規模が際限なく拡大していくと、システム全体では構造抽出性が下がって全貌は理解不能に陥るかもしれない。

これを防ぐには、結局のところ、パーツ単位で構造抽出性を高める、従来からの複雑性緩和の工夫を随時やっていくということになるだろう。

 

決定的に構造抽出性を低くする諸悪の根源、プログラミングにおけるタブー的なモノは、1960年代後半のソフトウェア危機の頃に既に人類はすでに直面し、闘い、討伐してきたのではないか。故に我々はソフトウェアを現代の規模にまで巨大化させることが出来ているのではないか。

 

それは無軌道なgotoの乱用で生じるスパゲティのような処理フローであったり、メモリ空間を分けず状態遷移の把握を困難にする可変グローバル変数のようなものであったり。そういうものを我々は討伐してきたはずだ。

 

難読化という可能性

 

デファクタリングという操作を考えることで、組み合わせ爆発によるリファクタリング不可能点の存在を示唆したわけだが、ここで構造抽出性を下げる難読化の変換は、難読化されたコードを読み解きリファクタリングして構造抽出性を上げるよりもたやすい。

これは暗号化はたやすいが、その暗号化されたデータの暗号を破って復号化することは困難であることに類する。

 

デファクタリング操作によってAIをもってしても構造が分析不可能で、しかし、実行動作させることが可能な関数というものを作りうるのではないか。

 

まとめ


Epiplexity 構造抽出性を意図的に悪くすることを目論むと、構造を理解するために組み合わせ爆発を乗り越える必要があるコードを生成することができ、これは組み合わせ爆発により現実のコンピュータでは計算資源的に構造を抽出できないソースコードとなりうるだろう。

 

計算資源的に構造の抽出が不可能となり、リファクタリングができなくなる点をリファクタリング不可能点と定めた。

レガシーコードにはリファクタリング不可能点を超えたモノが存在する可能性がある。こうなるとAIの発展とは関係なく、そのコードはもうリファクタリングが不可能である。

 

ただし、現代のAIを補助的に使うような開発では容易にそのリファクタリング不可能点を超えることはないのではないか。

1960年代後半に言われたソフトウェア危機はまさにこのリファクタリング不可能点の話だったのではないか。当時に整備された構造化プログラミングなどの技法は、リファクタリング不可能点を遠ざけるための重要な基礎となっているのではないか。

 

逆説的にそうした技法を逆用することでデファクタリングが行えるのではないか。デファクタリングにより構造の抽出が不可能なコードを意図的に創り出すことができるのではないか。

 

次回、試験性と検証可能性

 

おまけ

過去に リファクタリングの事象の地平線 という記事を書いている。この時は経験則として大きなステップでリファクタリングをしなくてはいけない状態に陥るとリファクタリング不可能になるといったことを語っていたが、この「リファクタリング不可能」を本稿ではもう少し具体的に示せたのではないか。

 

追記

 

8/23追記。簡単にだが、実験を行った。

 

関数hoge(int)に対するデファクタリングをAIによって行いhoge2(int)を作成し、同じAIによって解析させる、hoge2(int)を更にデファクタリングをさせhoge2(int)を解析される、hoge3、hoge4……として6段階まで試みた。

感触としては、かなり長大で複雑な人間には解読困難なコードが生成されたものの、AIによっては解析が行えた。これが示唆するのは、並の人間の営みによるスパゲティに対しては現代のAIは十分な解析能力をもっており、コードそのものの解釈という点ではリファクタリング不可能点は遠そうに見える。

つまり、過去のレガシーコードが偶発的にリファクタリング不可能点を超えているのではないか?という可能性を危惧していたわけだが、ロジックの複雑さ起因のリファクタリング不可能点はそう簡単には超えなさそう。

 

現実のシステム開発においてリファクタリング不可能点が登場するのは、コードの複雑さよりも、コードと業務の対応付けの部分の情報欠損によるところが大きいのではないか。

AIは複雑なロジックであれ、ソースコードという確定的な事象にはかなり強い。しかし業務の想定のような部分になると不確実さは確実に残り、推定は大きく外すように思える。

となると、ロジック事態を綺麗にすることよりも、JavaDocに契約プログラミングの契約を明示することに力を割く方が労力を投資する方が有益なのではないかと思える。

 


 

悪意溢れる難読化コード

public String hoge_6(int i) {
int a0 = i ^ (i << 3) ^ (i >>> 2);
int a1 = a0 ^ a0;
int a2 = (a1 | -a1) >>> 31;

int[] a3 = {
6 - 3,
10 / 2,
2 << 1,
21 / 3,
3 * 3,
22 - 11,
7 + 6,
((i | 1) & 1) + 16,
38 >>> 1,
23,
29,
31
};

int[][] a4 = new int[a3.length][10];

for (int a5 = 0; a5 < a3.length; a5++) {
int a6 = a3[a5];
int a7 = i / a6;
int a8 = a7 * a6;
int a9 = i ^ a8;
int b0 = ((a9 | -a9) >>> 31) ^ 1;

int b1 = ((a6 ^ 3) | -(a6 ^ 3)) >>> 31;
int b2 = ((a6 ^ 5) | -(a6 ^ 5)) >>> 31;
int b3 = ((a6 ^ 4) | -(a6 ^ 4)) >>> 31;
int b4 = ((a6 ^ 7) | -(a6 ^ 7)) >>> 31;
int b5 = ((a6 ^ 9) | -(a6 ^ 9)) >>> 31;
int b6 = ((a6 ^ 11) | -(a6 ^ 11)) >>> 31;
int b7 = ((a6 ^ 13) | -(a6 ^ 13)) >>> 31;
int b8 = ((a6 ^ 17) | -(a6 ^ 17)) >>> 31;

a4[a5][0] = b0 & (b1 ^ 1);
a4[a5][1] = b0 & (b2 ^ 1);
a4[a5][2] = b0 & (b3 ^ 1);
a4[a5][3] = b0 & (b4 ^ 1);
a4[a5][4] = b0 & (b5 ^ 1);
a4[a5][5] = b0 & (b6 ^ 1);
a4[a5][6] = b0 & (b7 ^ 1);
a4[a5][7] = b0 & (b8 ^ 1);
a4[a5][8] = (a4[a5][2] ^ a4[a5][2]) | (a4[a5][3] & 0) | (a4[a5][4] & 0);
a4[a5][9] = ((a4[a5][5] | a4[a5][6] | a4[a5][7]) & a2);
}

int c0 = 0;
int c1 = 0;
int c2 = 0;
int c3 = 0;

for (int c4 = 0; c4 < a4.length; c4++) {
c0 |= a4[c4][0];
c1 |= a4[c4][1];
c2 ^= a4[c4][8];
c3 |= a4[c4][9];
}

int[] c5 = new int[32];
c5[0] = c0;
c5[1] = c1;
c5[2] = c0 ^ c1;
c5[3] = c0 & c1;
c5[4] = c0 | c1;
c5[5] = (c5[4] ^ c5[2]) ^ c5[3];
c5[6] = ((c5[5] | -c5[5]) >>> 31);
c5[7] = c2;
c5[8] = c3;
c5[9] = c5[7] ^ c5[7];
c5[10] = c5[8] & c5[9];
c5[11] = (c5[0] & c5[10]) | (c5[1] & c5[10]);
c5[12] = ((i + c5[11]) ^ (c5[11] + i));
c5[13] = ((c5[12] | -c5[12]) >>> 31) ^ 1;
c5[14] = c5[13] & 1;
c5[15] = c5[14] ^ 1;
c5[16] = c5[15] & 0;
c5[17] = c5[16] ^ c5[16];
c5[18] = (c5[2] & c5[17]) | (c5[3] & 0);
c5[19] = ((c5[18] | -c5[18]) >>> 31);
c5[20] = c5[19] ^ c5[19];
c5[21] = (c5[0] | c5[20]) ^ c5[20];
c5[22] = (c5[1] | c5[20]) ^ c5[20];
c5[23] = c5[21] ^ c5[0];
c5[24] = c5[22] ^ c5[1];
c5[25] = c5[23] | c5[24];
c5[26] = c5[25] & 0;
c5[27] = ((i ^ i) | c5[26]);
c5[28] = ((i + 2) ^ (2 + i));
c5[29] = c5[28] | c5[27];
c5[30] = ((c5[29] | -c5[29]) >>> 31) ^ 1;
c5[31] = c5[30] & 1;

int[] d0 = {
c0,
c1,
c0 & c1,
c0 ^ c1,
c5[9],
c5[10],
c5[17],
c5[18],
c5[20],
c5[26],
c5[27],
c5[29],
(c0 & c5[20]) | (c1 & c5[20]),
((c0 | c0) ^ c0) & 0,
((c1 | c1) ^ c1) & 0,
((i ^ i) & 1)
};

int[][] d1 = new int[d0.length][8];

for (int d2 = 0; d2 < d0.length; d2++) {
int d3 = d0[d2];

for (int d4 = 0; d4 < d1[d2].length; d4++) {
int d5 = ((d2 ^ d4) | -(d2 ^ d4)) >>> 31;
int d6 = (d5 ^ 1) & d3;
int d7 = ((d4 ^ 0) | -(d4 ^ 0)) >>> 31;
int d8 = ((d4 ^ 1) | -(d4 ^ 1)) >>> 31;
int d9 = ((d4 ^ 2) | -(d4 ^ 2)) >>> 31;
int e0 = ((d4 ^ 3) | -(d4 ^ 3)) >>> 31;

d1[d2][d4] = d6 & (((d7 ^ 1) | (d8 ^ 1)) | (((d9 ^ 1) | (e0 ^ 1)) & 0));
}
}

char[][] e1 = {
{
(char) ((23 * 4) + 10),
(char) ((25 * 4) + 5),
(char) ((30 * 4) + 2),
(char) ((40 * 3) + 2)
},
{
(char) (200 - 102),
(char) (100 + 17),
(char) (61 * 2),
(char) ((11 * 11) + 1)
},
{
(char) (90 + 23),
(char) (120 - 9),
(char) (60 * 2),
(char) (114)
},
{
(char) (50 + 59),
(char) (200 - 95),
(char) (230 / 2),
(char) (116)
},
{
(char) (100 + 8),
(char) (110 + 7),
(char) (228 / 2),
(char) (101)
},
{
(char) (55 * 2),
(char) (111),
(char) (112),
(char) (101)
},
{
(char) (99),
(char) (97),
(char) (103),
(char) (101)
},
{
(char) (109),
(char) (97),
(char) (122),
(char) (101)
}
};

int[][] e2 = new int[d1.length][4];

for (int e3 = 0; e3 < d1.length; e3++) {
int e4 = 0;

for (int e5 = 0; e5 < d1[e3].length; e5++) {
e4 |= d1[e3][e5];
}

int e6 = ((e3 ^ 0) | -(e3 ^ 0)) >>> 31;
int e7 = ((e3 ^ 1) | -(e3 ^ 1)) >>> 31;
int e8 = ((e3 ^ 2) | -(e3 ^ 2)) >>> 31;
int e9 = ((e3 ^ 3) | -(e3 ^ 3)) >>> 31;

e2[e3][0] = e4 & (e6 ^ 1);
e2[e3][1] = e4 & (e7 ^ 1);
e2[e3][2] = (e4 & (e8 ^ 1)) & 0;
e2[e3][3] = (e4 & (e9 ^ 1)) & 0;
}

int[][] f0 = new int[e2.length][e1.length];

for (int f1 = 0; f1 < f0.length; f1++) {
for (int f2 = 0; f2 < f0[f1].length; f2++) {
int f3 = e2[f1][0] | e2[f1][1] | e2[f1][2] | e2[f1][3];

int f4 = ((f1 ^ 0) | -(f1 ^ 0)) >>> 31;
int f5 = ((f1 ^ 1) | -(f1 ^ 1)) >>> 31;

int f6 = ((f2 ^ 0) | -(f2 ^ 0)) >>> 31;
int f7 = ((f2 ^ 1) | -(f2 ^ 1)) >>> 31;

int f8 = ((f4 ^ 1) & (f6 ^ 1)) | ((f5 ^ 1) & (f7 ^ 1));
int f9 = ((f1 + i) ^ (i + f1));
int g0 = (((f9 | -f9) >>> 31) ^ 1);

f0[f1][f2] = f3 & f8 & g0;
}
}

StringBuilder g1 = new StringBuilder();

for (int g2 = 0; g2 < f0.length; g2++) {
for (int g3 = 0; g3 < f0[g2].length; g3++) {
int g4 = f0[g2][g3];

for (int g5 = 0; g5 < e1[g3].length * g4; g5++) {
int g6 = g5 ^ g5;
int g7 = g6 + g5;
int g8 = (g7 ^ g6) ^ g6;
char g9 = (char) (e1[g3][g8] ^ g6);
g1.append(g9);
}
}
}

String h0 = g1.toString();
String h1 = new StringBuilder().append(i).toString();

String[] h2 = new String[16];
h2[0] = h1;
h2[1] = h0;
h2[2] = h1.substring(0);
h2[3] = new StringBuilder(h0).reverse().reverse().toString();
h2[4] = h1 + "";
h2[5] = h0 + "";
h2[6] = new StringBuilder(h1).reverse().reverse().toString();
h2[7] = new StringBuilder(h0 + "").substring(0);
h2[8] = h2[0].concat("");
h2[9] = h2[1].concat("");
h2[10] = h2[2].replace("", "").length() == 0 ? h2[2] : h2[2];
h2[11] = h2[3].replace("", "").length() == 0 ? h2[3] : h2[3];
h2[12] = new StringBuilder().append(h2[4]).toString();
h2[13] = new StringBuilder().append(h2[5]).toString();
h2[14] = h2[6].substring(0);
h2[15] = h2[7].substring(0);

int h3 = h0.length();
int h4 = (h3 | -h3) >>> 31;
int h5 = h4;
h5 ^= ((i ^ i) & 1);
h5 |= (((h3 ^ h3) | -(h3 ^ h3)) >>> 31);
h5 += ((h5 ^ 1) & 0);

int h6 = h5 & 1;

int h7 = ((h6 ^ 0) | -(h6 ^ 0)) >>> 31;
int h8 = ((h6 ^ 1) | -(h6 ^ 1)) >>> 31;

int h9 = ((h7 ^ 1) * 0) + ((h8 ^ 1) * 1);
int i0 = h9 | (((h9 ^ h6) | -(h9 ^ h6)) >>> 31 & 0);

return h2[i0];
}

これを解析して作成されたJavaDoc

/**
* 入力値に応じて文字列を返す。
*
* <p>このメソッド内のコードだけを読む限り、まず入力値 {@code i} から複数の中間値を作成し、
* その後、複数の整数候補を持つ配列を用意している。候補値には、実質的に
* {@code 3}、{@code 5}、{@code 4}、{@code 7}、{@code 9}、{@code 11}、{@code 13}、
* {@code 17}、{@code 19}、{@code 23}、{@code 29}、{@code 31} に相当する値が
* 含まれているように見える。</p>
*
* <p>各候補値について、{@code i / 候補値 * 候補値} が元の {@code i} と一致するかどうかを、
* XOR、ビット OR、符号なし右シフトなどを使って判定しているように見える。
* この処理は、入力値 {@code i} が各候補値で割り切れるかどうかを調べているように読める。</p>
*
* <p>判定結果は 2 次元配列に格納され、その後、複数の集約変数や中間状態配列へ変換される。
* このうち、入力値 {@code i} が {@code 3} で割り切れるかどうかに対応する値と、
* {@code 5} で割り切れるかどうかに対応する値が、最終的な文字列生成に影響しているように見える。</p>
*
* <p>{@code 4}、{@code 7}、{@code 9}、{@code 11}、{@code 13}、{@code 17} などに
* 関連する判定結果も作成されているが、それらの多くは同じ値同士の XOR、{@code & 0}、
* 常に {@code 0} になるように見える中間値との AND などによって、最終結果に影響しないように
* 無効化されているように見える。ただし、このメソッドだけを読んだ段階では、それらすべてが完全に
* 無意味であるとまでは断定しにくい。</p>
*
* <p>文字列生成には、複数の 4 文字候補を持つ {@code char} の 2 次元配列が使われている。
* 先頭の候補は文字コード計算により {@code "fizz"} に相当する文字列を表し、
* 2 番目の候補は {@code "buzz"} に相当する文字列を表しているように読める。
* それ以外にも複数の候補文字列が定義されているが、後続のマスク処理や選択処理を見る限り、
* 実際に最終結果へ追加される経路には到達しない、または到達しにくいように制御されているように見える。</p>
*
* <p>文字列を構築するまでに、複数の 2 次元配列、状態配列、マスク値、インデックス判定が使われる。
* これらの処理では、インデックスが特定値と一致するかどうかをビット演算で判定し、
* どの文字列候補を追加するかを間接的に決めているように見える。
* 実質的には、入力値が {@code 3} に関する条件を満たす場合に先頭の文字列候補を追加し、
* {@code 5} に関する条件を満たす場合に 2 番目の文字列候補を追加しているように読める。</p>
*
* <p>ループ内には、{@code (x + i) ^ (i + x)} のように、加算の交換法則により常に
* {@code 0} になるように見える式や、同じ値同士の XOR によって常に {@code 0} になるように
* 見える式が含まれている。これらは、実質的には結果を変化させない固定的な値として使われているように
* 見えるが、コード上は解析対象の中間状態を増やしている。</p>
*
* <p>最終的に構築された文字列が空でない場合は、その文字列またはそれと同等に見える候補を返す。
* 構築された文字列が空の場合は、入力値 {@code i} を文字列化したもの、またはそれと同等に見える
* 候補を返すように見える。返却候補は 16 要素の配列に格納されており、
* {@code substring}、文字列連結、二重 {@code reverse}、{@code concat} などによって、
* 実質的に同じ値を持つ候補が複数作られているように見える。</p>
*
* <p>総合すると、このメソッドは、入力値 {@code i} が {@code 3} で割り切れる場合に
* {@code "fizz"} に相当する文字列を追加し、{@code 5} で割り切れる場合に
* {@code "buzz"} に相当する文字列を追加しているように見える。
* 両方の条件を満たす場合は、それらを順に連結した文字列を返すように見える。
* どちらの条件にも該当しない場合は、入力値 {@code i} の文字列表現を返すように見える。</p>
*
* <p>ただし、このメソッドには、結果に影響しないように見える候補値、文字列候補、状態配列、
* 2 次元配列、ビット演算、到達しないように見える経路、同等値を作る可逆変換が多数含まれている。
* そのため、このメソッドだけを読んだ段階では、すべての中間値や候補経路の意味を完全に断定することは
* 難しい。</p>
*
* @param i 判定対象の整数
* @return 条件に応じて組み立てられた文字列、または {@code i} の文字列表現
*/
 

リファクタリングの価値の考察シリーズ3 - 瞭然化

前回は Epiplexity  構造抽出性という概念でリファクタリングを捉えると良いのではないかという提言を行った。

今回はこの構造抽出性でリファクタリングを捉えるとどのように概念拡張されるのかについて検討する。

構造抽出性についておさらい

Epiplexity  (本blogではこれに構造抽出性という訳語を充てているが、これは筆者独自の訳語なのでこのblogが書かれた時点では定訳ではない点に注意)というのは From Entropy to Epiplexity という論文で提唱された概念で、実際のAIが有限の計算資源のもとで実際に見出せる構造のような概念。実際にデータから学習可能な情報量みたいな感じだろうか。本質的に含んでいる情報量に加えてその分かりやすさみたいなものを含んだ概念とでも言えようか。

 

本blogでは人工知能も自然知能(要するに人間の脳みそを想定している)も、ある計算能力の知能というように同一視して考えている。

ある計算資源のもとで、どれだけ情報を取り出せるか。理解できるか。データがどれだけの情報を含んでいようが、理解できないなら意味がない。実際に意味や構造を抽出できるかに注目している点が面白い概念だ。

 

従来リファクタリングは

リファクタリング(名詞):外部から見たときの振る舞いを保ちつつ、理解や修正が簡単になるように、ソフトウェアの内部構造を変化させること。

といったように定義づけられてきた。ここで「理解や修正が簡単になるように」と言ってしまっているがこれはかなり人間主観の評価である。

この定義は2000年のマーチン・ファウラーの書籍「リファクタリング」のものだが、当時にはAIは当然存在せず「理解や修正が簡単とはどういうことか?」に具体的な尺度が与えることは出来なかった。

 

このような概念についてメトリクス(要するに数値で計測すること)はいろいろと試みられてきたが、あまり成功していない。

 

この Epiplexity 構造抽出性がそのままメトリクスになるわけではないが、AIを用いて一応の計測可能な指標として示されたことは興味深く、リファクタリングが向かうべき方向性を「外部から見たときの振る舞いを保ちつつ、構造抽出性を高める」というように定義づけることができる点は面白い。

意味情報の分散メッシュモデルと契約プログラミング

JJUG CCC 2026 Spring での発表で意味情報の分散メッシュモデルというのを提唱した。

情報をもった人、モノ、AIといったものたちが、情報をやり取りするという捉え方で、システム開発における情報伝達をモデル化したものだ。

意味情報の分散メッシュモデル

意味情報の分散メッシュモデルでは人間の通信がボトルネックであると主張した。また、このボトルネックを解消するには共通の知識・語彙を持つことが大事で、つまるところシステム開発に明るい人間でなければ、結局はAIと協業してプログラムを作ることが困難なのではないか。情報交換をすることがボトルネックになるからではないか、と予想した。

 

ここで、AIは文書を読み込んだり、ネットなどの既知の情報を読み取ったりすることが高速で出来るものの、人間をヒアリングして情報を引き出そうとすればそこがボトルネックになる。これを都度行っていてはとてもやってられなので、如何に仕様などドキュメントを整理整頓した形でアウトプットしておく。AIが読み取りやすい形にしておく。

 

さてここで、人間とAIと共通してプログラムというモノを扱うのであれば、ソースコードは適度に塊にして独立したコードで階層化するほうが良さそうだ。塊の間は約束事を明確にすると良さそうだ、それはどうも、契約プログラミングの思想がどうも都合が良さそうだ、となる。

JavaDoc は契約プログラミングを土台にしており、メソッドやクラスなどに「契約」を記述する。こうした「契約」の記述は、AIがコードを読んで解釈する際にうまく探索空間を小さくすることができる。こういう背景であれば、こういうことだろう、とAIが推定する助けになる。

 

コードとドキュメントの連動性

さて、AIがコードを読むにあたって、コメントやドキュメントは結構参考にしてくれている。昔から、コードの対訳のようなコメントは不要だが、契約プログラミングの契約を明示するJavaDocのようなコメントは有用だとされていた。

 

AIがコードを読んで解析するにあたっても、JavaDocに契約を明示するとよく理解してくれる。JavaDocと契約プログラミングはどうも構造抽出性という観点では良い効果を与えそうだ。

 

従来、「リファクタリング」という概念はコードの設計に関するものだった。コメントは「リファクタリング」の埒外の存在であった。

 

書籍「リファクタリング」には第三章に「コメント」という見出しの項があって

コメントの必要性を感じたときにはリファクタリングを行って、コメントを書かなくとも内容が分かるようなコードを目指すこと。

という記載がある。コメントはコードとの乖離を起こすことがあり、一目瞭然なコードであれば対訳のようなコメントは不要であろうという思想が感じられる。続けて、

コメントの重要な使い方として、不明点を書き留めておくということがあります。処理の説明だけでなく、よくわからない事項のメモとしてコメントを使うことができます。そして「なぜ」このような処理を選択したのかを書いておくのです。この種の忘れやすい情報は、以降の修正のときに非常に役立ちます。

と記されており、この内容について筆者も異論ないのだが、コメントについてはこれで項目は終わりで、契約プログラミングの契約を明示するJavaDocのような存在を書籍「リファクタリング」では触れていない。

 

JavaDocは現世利益

 

JJUGでの発表で私が強調したのは「JavaDocは現世利益」ということだった。

 

かつて人間だけでコードを書く時代、JavaDocに契約プログラミングの契約を明示したところで、それで救われるのは自分ではない未来の赤の他人であることが多かった。自分の利益として還元される可能性が少なく、どうしてもドキュメントが疎かになりがちであった。

 

しかし、AI時代になってAIがコードを読み取り空気を読んでコードを書いてくれるとか、AIがコードを読み取りレビュー指摘してくれるとか、そういった事象が増えてくると、JavaDocに契約プログラミングの契約を明示することが、すぐ明日の自分の利益となって還元されるようになる。なので、それをモチベーションに契約をJavaDocに書きましょう、という話だった。

 

これは、ひとえにJavaDocに契約を明示することで構造抽出性を高めるという話にほかならない。そして構造抽出性を高めることでAIがより効果的に働くという主張である。

 

リファクタリングから瞭然化へ

リファクタリングは

リファクタリング(名詞):外部から見たときの振る舞いを保ちつつ、理解や修正が簡単になるように、ソフトウェアの内部構造を変化させること。

であった。これを拡張した概念として「瞭然化」を提唱したい。

瞭然化:外部から見たときの振る舞いを保ちつつ、構造抽出性を高めること

これは、コードのみならず、ドキュメントの記載をも含む概念である。

 

次回、リファクタリング不可能点。

リファクタリングの価値の考察シリーズ2 - 構造抽出性

前回「リファクタリングの価値の考察」という記事を書いたのは4年前だった。

nagise.hatenablog.jp

改めてリファクタリングの価値の考察をしたい。当時と大きく違うのはAIの存在だ。

 

 

読みにくいコードでもAIは読んでくれる。AIは今後、さらに性能が向上するだろう。ならば人間が読みやすいような「リファクタリング」は不要ではないか?という問いはとても興味深い。

本稿ではそれでもリファクタリングをする意義があると主張する。組み合わせ爆発とAIの限界についての考察、そして2026年の話題の “ From Entropy to Epiplexity “ という論文から Epiplexity (エピプレキシティ、 構造抽出性) という概念の導入をし、リファクタリングの価値について再定義してみたい。

動いているコードに触るな!

産業界では古の教えとして「動いているコードに触るな!」というものがあった。コードを不用意に触ると予期せぬバグが生じる可能性が高まる。動いてビジネスとして価値を生み出しているシステムをバグによって止めるというのは大罪であった。よって、古い時代にはとにかくコードというのは作りが良くないと思っていようが、リスクを避けるために不用意に触らないということが言われていた。

 

リファクタリングは、それを覆すものだった。

 

マーチン・ファウラーの2000年の書籍「リファクタリング」は衝撃的だった。良くないコードを見て見ぬふりをすることを強いられていた若き日の筆者の心に、明るく道を照らした光のようであった。

 

書籍「リファクタリング」ではリファクタリング手順を細かく定めカタログ化していた。2000年という時代背景を想像して欲しい。WindowsはXPの前の2000が出るかという時代である。AIはもちろん存在しない。自動テストも普及していない。「動いているコードに触るな!」の時代である。その教えの理由を深く理解していたからこそ、書籍「リファクタリング」ではそのリスクを徹底的に排除しようという意図が感じられる。その時代の価値観を踏まえ、そしてそれに立ち向かった書籍に思えた。

 

それは、ここまで新たにバグを埋め込むエンバグの対策したのだから、どうか不吉な匂いのするコードを直させてください!という魂の叫びのようであった。

 

 

……しかし、現代ではリファクタリングで「振る舞いを保つ」ということにかけるマーチン・ファウラーの情熱はどうもあまり伝わっていないように思える。外的振る舞いを保つことのない単なるリライトでさえも「リファクタリング」と呼ばれているのを見ることすらある。

 

「動いているコードに触るな!」の時代に、それでも保守性のために不吉な匂いのするコードを直したかった。未来に発生する「良くないこと」を予見するからこそ、それを事前に排除したかった。コードを変更することで生じるエンバグのリスクと天秤にかけてさえも!

 

この思いをまず語っておきたい。

 

リファクタリングのおさらい

リファクタリング(名詞):外部から見たときの振る舞いを保ちつつ、理解や修正が簡単になるように、ソフトウェアの内部構造を変化させること。

 

リファクタリングする(動詞):一連のリファクタリングを行って、外部から見た振る舞いの変更なしに、ソフトウェアを再構築すること。

 

これは書籍「リファクタリング」での記述だ。本稿ではこの定義に則ってリファクタリングについて考察する。

 

 

書籍「リファクタリング」では4章で「テストの構築」と題して自動テストの構築を解説しており、《リファクタリングを自動化するツールがたまたま使える状況にあったとしても、依然としてテストは必要です。》と述べている。

 

そして《よいバグ検出器を作り、それを頻繁に実行してください。これは、どのような開発にとっても強力なツールです。また、リファクタリングの事前条件でもあるのです。》と語って章を締めくくっている。

 

《外部から見たときの振る舞いを保ちつつ》を確認するため、保証するため、テストが必要である。

 

リファクタリングの価値考察のおさらい

2022年の前回の記事では

  • リファクタリングは保守性に作用する
  • 保守性の価値は将来の変更コストを下げることにある
  • しかし未来の変更は不確定なので、価値を算出しにくい
  • また経済的価値となるとシステムのビジネス上の価値と関係する

といった評価を行った。

 

結局のところシステムの価値というものが、そのシステムがどれほどの利益を産むビジネスの礎であるか?というところに依存する。その価値がまずあり、それが変更容易な状態であるとか、そういったものが副次的にどのぐらいの価値があるか?という話になる。大きなビジネスの礎となるシステムであれば、その保守性の価値も大きくなろう。さほどのビジネス的重要性のないシステムであれば、その保守性の価値もまた低く見られるだろう。

 

AIの手に負えないもの

2022年の前回の考察とIT業界の様相は随分と変わった。ひとえにAIという要素がIT業界の産業構造に大きな影響を与えている。

昨今のAIの発展をみるに、このままAIがどんどん強化されていけばAIがなんでもやってくれそうに思えてくる。

しかし、AIでも不可能なことがある。

 

例えば、組み合わせ爆発に属する問題。将棋のようなゲームは差し手の組み合わせが1手ごとに数十倍に増えていく。これを全網羅して完璧な手を探し出すことは出来ない。将棋の盤面は10の68乗にもおよぶと言われる。これを網羅的に確認しようとするとコンピュータをもってしても宇宙の終わりまでもの時間をもってしても全然足りない。組み合わせ爆発に対してはコンピュータの速度をもってしても正面突破は不可能である。

 

しかし、現実にAIは将棋を指せる。人類よりもはるかに強い。将棋AIが強いのは、全ての手を読むことができるからではない。むしろ、読む必要のない手を大量に捨てられるからである。

組み合わせ爆発を正面突破するのではなく、枝刈りをして組み合わせ爆発を回避して尤もらしい分岐だけを読むわけだ。

AIの知能とは、巨大な探索空間を無制限に探索する能力ではなく、限られた計算資源の中で探索空間を絞り込む能力でもある。

 

これはソースコードを読む場合にも同じことが言えるのではないか。

 

AIがソースコードを読み、解釈し、その構造を捉えるにあたっても、探索空間が広いと当然、全網羅的に探索することは不可能である。しかしうまく周辺の情報を活用して「こういう話ではない」とバッサリと切って捨てれる部分が多ければ、うまく良さそうな形にもっていくことができる。

 

ここで出てくるのが Epiplexity (エピプレキシティ、 構造抽出性)という概念である。

 

From Entropy to Epiplexity

まだプレプリントだが、ある論文が注目されている。From Entropy to Epiplexity という論文で、Entropy 情報量から、Epiplexity エピプレキシティ 観察者が学習可能な構造の量という概念へ。本稿ではこれを構造抽出性と呼ぶことにしよう。

 

古典的な情報理論ではシャノンの定義した Entropy (エントロピー)という情報量の概念が出てくる。シャノン情報量は、データがどれほど予測困難かを扱う。

一方で、シャノン情報量は、あるデータを観測者が実際にどれだけの計算資源を使って解析できるかという問題を直接は扱わない。

現実の観測者は有限の計算資源しか持たない。そのため、「データに構造が存在するか」と「その構造を有限の時間・計算資源で取り出せるか」は別の問題になる。

この観点から、構造抽出性という概念を考える。

 

まず、シャノン情報量が扱う情報量と、有限の計算資源しか持たない観測者が、実際にデータから取り出せる構造は、必ずしも同じではない。例として疑似乱数が挙げられる。

疑似乱数生成器は決定的な計算なので、乱数のシードが分かれば生成されたデータを完全に再現できる。つまり、そのデータには明確な生成構造が存在している。

 

ところが、seedも生成アルゴリズムも知らない観測者から見ると、そのデータはほとんどランダムにしか見えない。有限の計算資源しか持たない観測者にとって、その生成構造を発見することは非常に困難になり得る。

つまり、

構造が存在することと、その構造を現実的な計算時間で抽出できることは別の問題である。

 

この構造抽出性という概念は、ISOのソフトウェアの品質特性のうち解析性(analyzability)という概念をより具体化・客観化したモノとも言えるかもしれない。

 

ISOの品質特性というものは品質について議論するための分類・語彙・評価軸として作られたもので、その客観的な計測というのは示されていなかった。そして過去にその計測を試みた研究は多数ある一方、その数値が本当に品質を表しているのか?という妥当性についてはかなり弱い。ソフトウェアの品質を計測しようというソフトウェアメトリクスの分野はあまり成果を挙げてこれなかった。

 

私はこの構造抽出性という概念でリファクタリングを捉えると面白いのではないかと考えたのだ。

 

AI時代のリファクタリングの評価基準

まず、リファクタリングがコードリーディングやAIによる解析に与える影響

 

  • 「リファクタリング」は解析性(analyzability)を上げる
  • 解析性(analyzability)が上がるとは、構造抽出性が向上するということ。有限の計算資源でソフトウェアの構造を読み解くことが容易である状態になるということ
  • 人間の脳みその計算資源で抽出できる構造情報が増える方向のアプローチは、AIの計算機資源で抽出できる構造情報も増やすだろう
  • AIも組み合わせ爆発を正面突破できないので、探索空間は小さい方が良いし、コードを読むにあたって少ない計算機資源で多くの構造情報を抽出できるならその方が有利だろう
  • ただし、この構造抽出性の差のスケールが小さければ、AIがシステム全体を捉える計算量にさして差を生まないかもしれない。大きなスケールでは影響しそう
    • 現代のAIが生成するコードはそもそも可読性がそれなりに良い。そういう意味で、AIが生成したコードをさらにAIがリファクタリングしても劇的な変化は感じられないかもしれない

リファクタリングが人間による「読みやすさ」というもやっとした話から、人間にしろAIにしろコードという情報から「構造の読み取りやすさ」という概念でリファクタリングを評価する。

 

すると、「読みにくくてもAIがコードを読んでくれるから」というのはAIが読み取ることができるかどうかという「可能」の話と、さらにその読み取りに費やした「計算量」の話になる。計算量が関わってくるならば、これは経済的価値の話になる。

 

ここまで考えてくると、リファクタリングを「コードをきれいにすること」と捉えるよりも、「ソフトウェアに存在する構造を、観測者から見て明らかにすること」と捉えた方が、AI時代には本質に近いのではないかと思う。

ここでは、この行為を仮に「瞭然化」と呼んでみたい。

 

次回

本件はシリーズ化して関連テーマについて検討していきたい。本稿では  Epiplexity という概念をもってリファクタリングを再考するというアイデアを提示した。この Epiplexity  エピプレキシティという単語、どうにも日本人には読みにくい。私はこの訳語として構造抽出性を提案する。

 

次回は「瞭然化」について掘り下げ、リファクタリングを拡張することを提唱する。

入門 Java言語仕様を読もう! with Java Puzzlers

2025年6月7日(土) JJUG CCC 2025 Spring お疲れさまでした。登録者は1000人を超えたようで、会場もなかなかの人手だったかと思います。

タイムテーブルを見るとよく分かりますが、8トラックも並行しているのでどのセッションを見ようか悩ましかったのではないでしょうか。

今回の私のセッションは「入門 Java言語仕様を読もう! with Java Puzzlers」という題目で、Java言語仕様を読むための基礎の話と、Java PuzzlersというJava言語仕様にまつわるクイズの出題、関連するJava言語仕様の解説、という内容でした。

JJUGのセッションはどうも雰囲気が硬くなりがちなので、聞き手にゆとりさんを迎えて対話形式で進めました。資料部分は事前に共有していましたが、問題部分は見せていないので、新鮮なリアクションをいただけて良かったと思います。

Java言語仕様を1次資料として参照してくれる人が増えると嬉しいな。

書籍 Java Puzzlers (2005年 ジョシュア・ブロック, ニール・ガフター)は絶版ですが、運が良ければ中古で安く入手できるかも? 今回はこの書籍からの改題したものを中心に出題しました。

枕の話の部分だけまとめた資料を別途公開します。問題とその解説はスライドでは断片的なのでこのblogでやります。

Q1 拡張forでEnumerationのasIterator()を回そうとしたらどうなるか?

Q1 のポイントはfor文です。 e::asIterator とメソッド参照でfor文を回そうとしています。

import java.util.*;

public class Q01 {
 public static void main(String[] args) {
   Enumeration<String> e = Collections.enumeration(
       List.of("Hello", "JJUG", "2025"));
   for (String s : e::asIterator) {
     System.out.print(s);
   }
 }
}

選択肢は

  1. HelloJJUG2025 が表示される
  2. コンパイルエラー
  3. その他

Q1の解説

正解は 2.コンパイルエラー。メソッド参照の登場できる場所には制約があります。

It is a compile-time error if a method reference expression occurs in a program in someplace other than an assignment context (§5.2), an invocation context (§5.3), or a casting context (§5.5).

代入、メソッドやコンストラクタの呼び出し、キャスト以外に現れたメソッド参照はコンパイルエラーとなります

§15.13. Method Reference Expressions

これは過去のblog Enumeration を for-each ループする方法 が元ネタ。

言語仕様のバッカス・ナウア記法(BNF : Backus–Naur form)の解説をやるために、拡張for文がらみの問題をチョイスしました。

拡張for文のBNFは §14.14.2. The enhanced for statement に書かれていて、右辺はExpressionとなっています。ここから追いかけて行って「メソッド参照がない」ことを確信するのはちょっと大変かもしれない。

blog側に解説していますが、キャストを書けばコンパイルが通ります。

Q2 突然の URL

import java.util.*;
import java.net.http.*;

public class Q02 {
   public static void main(String[] args) throws Exception {
       HttpClient httpClient = HttpClient.newHttpClient();
       URI https = URI.create("https://www.google.com");
       HttpRequest request = HttpRequest.newBuilder(https).build();
       HttpResponse<String> response = httpClient.send(
              request, HttpResponse.BodyHandlers.ofString());
       https://www.google.com
       System.out.println(response.body());
   }
}
  1. 正常終了
  2. コンパイルエラー
  3. その他

コード中に現れる https:// の文字……。果たして。また、httpsという名前の変数も宣言しているのですが、影響するのか否か?

Q2 解説

これは一部界隈では有名なネタで……(なんの界隈かと言われるとアレなんですが)Javaの構文的に https: 部分がラベルで、//より後ろが1行コメントとなって合法になります。正常終了……と言いたいところですが、セッション中はネットワーク不調で実行時エラーが出るというはハプニングが。URLにちなんでネットワーク使うコードになんてしなければ良かった……。

これはJava Puzzlers より パズル22 を改題。書籍Java Puzzlersは2005年の本なので http://www.google.com としてましたが、時代に合わせて https としました。

今回はちょっとひねっていて、変数 https を用意していて、名前が被っている時はどうなの?をネタを知っている人には問うているわけですね。

There is no restriction against using the same identifier as a label and as the name of a package, class, interface, method, field, parameter, or local variable.

同じ識別子をラベル名と、パッケージ、クラス、インターフェース、メソッド、フィールド、パラメータ、またはローカル変数名として使用することに制限はありません

§14.7. Labeled Statements

とわざわざ記載があって、ラベル名は変数名と被っていても問題ありません。

Q3 i != i をtrueにせよ

public class Q03 {
 public static void main(String[] args) {
   int i = 0;
   if ( i != i ) {
     System.out.println("Hello, JJUG 2025!");
   }
 }
}

このコードの int i = 0; 部分を書き換えてif文内を動くようにしてください

Q3 解説

これも書籍Java Puzzlersよりパズル29 を改題。想定解としては Float.NaN ないし Double.NaN を使います。

public class Q03 {
 public static void main(String[] args) {
   float i = Float.NaN;
   if ( i != i ) {
     System.out.println("Hello, JJUG 2025!");
   }
 }
}

intで 0/0 をやるとjava.lang.ArithmeticExceptionとなりますが、浮動小数点数ではNaN (Not a Number、非数)となります。要するに「解なし」みたいな感じですね。

0.0/0.0 の他、Math.sqrt(-1) や 10.0 % 0などでNaNが発生しますが、これらは同値ではないですよね? そのためNaN同士の==比較はfalseになりますし、!=比較はtrueになるように設計されています。言語仕様上は以下の通り。

The equality operator == returns false if either operand is NaN.
The inequality operator != returns true if either operand is NaN.

等価演算子==は、 いずれかのオペランドが NaN の場合にfalseを返します
不等号演算子 != は、どちらかのオペランドが NaN の場合にtrueを返します

§4.2.3. Floating-Point Types and Values

あるいは §15.21.1. Numerical Equality Operators == and != を参照してみてください。

Q4 long と int の足し算

public class Q04 {
 public static void main(String[] args) {
   System.out.println(
       Long.toHexString(0x1_0000_0000L + 0xcafebabe));
 }
}

選択肢は

  1. 1cafebabe が出力される
  2. コンパイルエラー
  3. その他

Q4 解説

ポイントは0x1_0000_0000L がlongで 0xcafebabe がintというところ。intがワイドニングされてlongになるのだけども、ここで0xcafebabeは最上位ビットが立っているのでマイナス値(2の補数表現)で0xffff_ffff_cafe_babe L とされてからlong同士の足し算がされるので結果がcafebabeとなってしまいます。つまり正解は3のその他です。

言語仕様には以下のような記載があり

A widening conversion of a signed integer value to an integral type T simply sign-extends the two's-complement representation of the integer value to fill the wider format.

符号付き整数値を整数型Tに拡大変換すると 、整数値の 2の補数表現が単純に符号拡張され、より広い形式が満たされます

§5.1.2. Widening Primitive Conversion

2の補数表現で符号拡張される、つまりintのマイナス値はlongのマイナス値になるわけです。

Q5 int最小値

public class Q05 {
 public static void main(String[] args) {
   System.out.println(-2147483648);
 }
}
// ※ 2147483648 = 2^31

選択肢は

  1. -2147483648 が出力される
  2. コンパイルエラー
  3. その他

Q5 解説

Q5は後続の問いへの枕になっていて、これはそのまま素直に動くというひっかけ問題。正解は1の-2147483648 が出力されるです。

壇上からは会場で聞いている人々の表情が良く見えるのだけど、だんだん疑心暗鬼になってきたところで普通の問題を出したので割とみんな間違えましたね。自分の脳内コンパイラ脳内JVMが信じれなくなってくるのがJava Puzzlersの醍醐味ですね。

言語仕様解説はまとめて後ろで。

Q5-2

public class Q05_2 {
 public static void main(String[] args) {
   System.out.println(-(2147483648));
 }
}
// ※ 2147483648 = 2^31

選択肢は

  1. -2147483648 が出力される
  2. コンパイルエラー
  3. その他

先ほどのQ5に対して()が付け加えられました。さてどうなるでしょうか?

Q5-2 解説

Java Puzzlers より パズル86 を改題。Q5は普通に-2147483648 が出力されました、では -(2147483648) はどうですか?というのが本来の問い。そんな、カッコつけたからって……と思いきや、これはコンパイルエラー。

もう一問見てから解説をしましょう。

Q5-3

public class Q05_3 {
 public static void main(String[] args) {
   System.out.println(-(0x8000_0000));
 }
}
// ※ 2147483648 = 2^31 = 0x8000_0000

今度は16進数表記でカッコ付きです。さてどうなるでしょうか。

  1. -2147483648 が出力される
  2. コンパイルエラー
  3. その他

Q5-3 解説

これは正常動作して-2147483648 が出力されます。さてどういうことなのでしょう?

It is a compile-time error if the decimal literal 2147483648 appears anywhere other than as the operand of the unary minus operator; or if a decimal literal of type int is larger than 2147483648 (2^31).

10進リテラル2147483648が単項マイナス演算子のオペランド以外の場所に出現した場合、またはint型の10進リテラルが2147483648(2^31)より大きい場合は、コンパイル時エラーになります。

§3.10.1. Integer Literals

とピンポイントに2147483648(2^31)についての記載があるのが面白ろポイントで、これは 2の補数表現の関係上、int最大値は 2147483647 (2^31-1)で、マイナスは-2147483648(-2^31)と絶対値が1大きい。

そして、-2147483648は構文上はマイナス値のリテラルではなくて、"-"の単項演算子と数値リテラル2147483648になっており、単項演算子"-"の後ろだけ特別扱いで2147483648が許されるという特例になってるんですね。

そしてこれは10進リテラルの場合限定なので16進数表記の0x8000_0000はセーフ。Q5の -2147483648 は正常に動いて、Q5-2の -(2147483648) はコンパイルエラーになるというわけです。

Q6 finallyでreturn

public class Q06 {
 public static void main(String[] argos) {
   System.out.println(decision());
 }
 static boolean decision() {
   try {
     return true;
   } finally {
     return false;
   }
 }
}

try で return しつつ、 finally でも returnしています。さてどうなるでしょうか。

  1. true が出力される
  2. false が出力される
  3. コンパイルエラー
  4. その他

Q6 解説

Java Puzzlers より パズル36 を微修正。この問題は比較的成果率が高かった気がします。finally節でreturnすると値が上書きされる感じですね。正解は 2のfalseです。

言語仕様的にはちょっと独特な表現になっていて

If the finally block completes abruptly for reason S, then the try statement completes abruptly for reason S.
finallyブロックが理由S により突然完了した 場合、try文も理由 Sにより突然完了します

§14.20.2. Execution of try-finally and try-catch-finally

突然完了というのは、要するに順次処理して抜けるんじゃなくて、例外が出る、ないしreturnする、の両方を共通的に仕様として記述しているのかな、と。

finally節がreturn false で完了すると、try節もreturn false で完了、というわけですね。

Q7 発生しない例外をcatch

public class Q07 {
 public static void main(String[] args)  throws Exception {
   try {
     System.out.println("Hello, JJUG 2025!");
   } catch (java.io.IOException e) {
     System.out.println("起きないIO例外");
   }
 }
}

発生しない IOExceptionをcatchしています。どうなるでしょうか。

  1. 正常終了
  2. コンパイルエラー
  3. その他

Q7 解説

tryで発生しない例外をcatchする場合、catch節が到達不能になるんですね。これは 2のコンパイルエラーになります。Javaは到達不能なコードは検出できる限りはコンパイル時にエラーにする方向性なので、そこに寄せた仕様なのかなと思います。詳しい解説は後続の問題の後で。

Q7-2 発生しない java.lang.Exceptionをcatch

public class Q07_2 {
 public static void main(String[] args)  throws Exception {
   try {
     System.out.println("Hello, JJUG 2025!");
   } catch (Exception e) {
     System.out.println("起きない例外");
   }
 }
}

今度は例外が java.lang.Exceptionになりました。

  1. 正常終了
  2. コンパイルエラー
  3. その他

Q7-2 解説

IOExceptionがtryでthrowされないのにcatch(IOException ioe) とするとこれはコンパイルエラー。でも、Q7-2でcatch(Exception e)とするとこれは通ります。正解は1です。
java.lang.Exceptionは特例になっていて、これは java.lang.RuntimeException が extends java.lang.Exception であるという継承階層の不都合がこのあたりの仕様をややこしくしているのでしょう。

該当言語仕様は次のQ8の後に合わせて解説します。

Q8

import java.io.*;
public class Q08 {
 public static void main(String[] args) throws Exception {
   try {
     throwIOException();
   } catch (FileNotFoundException e) {
     System.out.println("ファイルがないケース");
   }
 }
 static void throwIOException() throws IOException {
   throw new IOException();
 }
}

選択肢は

  1. 正常にIOExceptionがthrowされて終了
  2. コンパイルエラー
  3. その他

Q8 解説

Q7, Q7-2, Q8では例外のcatchの型の制約にまつわるパズルでした。Q8の正解は1でコンパイルエラーにはなりません。

It is a compile-time error if a catch clause can catch checked exception class E1 and it is not the case that the try block corresponding to the catch clause can throw a checked exception class that is a subclass or superclass of E1, unless E1 is Exception or a superclass of Exception.

catch節が検査例外E1をキャッチする場合に、対応するtry節がE1のサブクラスまたはスーパークラスの検査例外をthrowできない場合は、コンパイルエラーとなる。
ただし、E1がjava.lang.Exceptionの場合、もしくはjava.lang.Exceptionのスーパークラスの場合を除く。

§11.2.3. Exception Checking

ここで、catch(E1 e1)の場合を仮定しているわけですが、try節でE1がthrowされる、あるいはE1のサブクラスがthrowされるケースは分かると思います。E1のスーパークラスの場合も大丈夫なのはどういうことでしょう?

Q8ではtryで throws IOExceptionなメソッドを呼び出していて、catchはサブクラスのFileNotFoundExceptionなんですね。FileNotFoundException以外のIOExceptionないしそのサブクラスはcatchされずにthrows Exception な mainメソッドを例外で抜けていく。

catchしきらないけど、漏れたものはthrowされるのでセーフ。そしてthrows IOExceptionなメソッドであれば、具象型がFileNotFoundExceptionな例外を投げるかもしれないので、到達可能性的にもセーフ、そういう扱いになります。

これが《対応するtry節がE1のサブクラスまたはスーパークラスの検査例外》の意味合いです。

続く!

さて、問題は全部で15問用意してたのですが、解説が長くなってきたのでいったん区切ります。続きは後編で!

推敲のロジック

技術記事とかを書くとき、推敲をする。僕がどういうロジックで推敲をしているかの話。

 

前提知識

 

想定読者をまず決める。この記事を読むには前提知識としてこのぐらいは知っていてね、のライン。例えばJavaの記事を書くなら基礎的な構文は分かってる前提とする、とか、マニアックな記事なら、もっと深い知識を持っている前提とする、とか。一般向けの記事だと高校までで習うような前提知識があれば良いとするとかとか。

 

これは、前提知識の部分なら解説なしに「知ってるよね」の前提で話をする。知ってるかどうか危うい時はちょっと注釈をつける。知らない想定の部分は丁寧に解説をする。そのラインを決めるのが想定読者の前提知識。

 

知識は前提知識の上に積みあがる。積み重ねていく積み木のようなもので、この記事は既にここまで積んである人に対して、追加で何個の積み木を積ませよう、みたいな目標を定める。

 

記事内での時系列と積み木

 

記事と言うのは上から下に読まれる。方向があり、読者は上から下に読む。下から読んでも構わないがそういう読者は対象外なのでそういう読者への配慮はしない。

 

上から順に読んだ場合、記事の中でも知識が積みあがっていく。一段落目で説明したことは二段落目で使っても良い。一段落目で積み木を積んだから、二段落ではその上に何かを積んでも良い。

 

でも、逆はダメ。

 

プログラミングで言えば、宣言せずに初期化せずにいきなり変数を使うようなもので、これはコンパイルエラーにならなくてはいけない。自然言語はコンパイルできないのでしょうがないから自分で推敲してエラーとして処理しなければならない。

 

推敲のロジック

 

技術記事であることを説明しようとする場合、この前提知識の積み上げを意識する。

 

枕の話として、Aを解説し、Bを解説し、AとBが揃えばCを解説でき、Cを前提知識としたDについて語れる、といった具合に。

 

文章の上から下に向かって順に積んでいくようになっているか?唐突に未解説の概念を投入していないか?

 

こうしたことをチェックするためには、自分が知っていることを「知らない体」で読み返す必要がある。自分が書いた文章なら当然自分は結末まで知っている。知っているのだけど知らないことにして、ここでこのAの概念が出てきた、ここで読者はAを知る、ここでCの概念が出てきた。おや?Cの前提のBの解説がされていないな?などとやる。

 

この宣言順の前後チェックをしっかりやる。そのうえで、それらの前提知識で自然な論理展開にできているか?などを考える。前提知識の積み木が正しく積みあがっているか?を検証するだけでも文章はずっと良くなる。

 

雑誌記事などはかなり気合を入れて推敲をする。ブログ記事は割と適当。Twitterなんかは思い付きで散文を書いている。