システムの柔軟性はプログラムだけで担保しなくともよい

業務的にはいきなりの大波乱を乗り越えて、これを書いています。

例によってあまり詳細なことは書けませんが、急な要件変更があったということです。ただ、今回は予想以上に早く計画を立て直せたので、その点ではちょっと自慢してもいいのではないかと思っています(えっへん)。

さて昨日、レガシーコードには変更に対する柔軟性がないということを書きました。したがって変更に強い設計を行うべきでは、という思いを新たにしたわけですが、今回の急激な要件変更に耐えられた理由は、決して実装側で吸収できたからというわけではありませんでした。具体的には、管理していたドキュメントが現状を表すために機能していたので、それを元に素早く新しい計画を立てることができたのでした。なかなかのお手柄です(えっへん)。

仕様変更に強いシステムと、要件変更に強い管理用資料、両方は別の性質なんでしょうか。ちょっと考えてみたんですが、共通する部分もあるように感じています。結局のところ、システムの構成要素を部品として捉えて、それぞれ個別の動作・状態はもちろんのこと、その構成要素同士の依存、包含関係などを把握しておくことが、システムへの変更依頼があったときに素早くまず現状認識ができるようになる、つまり判断が早くなる、ということかと思います。

ただし、もとのシステムがあまりにも硬直している場合は、状況がうまく管理され、素早く状況を把握したとしても、元々の実装のまずさが明確に浮き彫りになるだけのことで、根本的に対処が早くなるのかという点では別問題です。つまり、管理状況と柔軟な設計はそれぞれ違う役割でシステム全体の対応速度アップに寄与するわけですね。

そういう意味では、どれだけきれいに書かれた実装だとしても、(プログラムを超えた業務要件などに関する)説明用の資料や全体を俯瞰した一覧などがないと、少し判断がもたつくということかもしれません。経験上も似たようなことは思い出します。ただ、業務要件なども、ドメインモデル(DDD的な文脈でのそれです)があれば、ソースコードに「匂わせる」ことはできます。それだけでも、後からソースコードを確認した場合に調査上の安心感が大幅に上昇します。

話を現実から少し浮かせます。そもそもでいうと、ソースコードを書く側はできるだけ手間なく素早く書き終わりたい(そしてビールを飲みたい)わけなんですが、ソースコードを読む側もできるだけ手間なく現在の状況を把握して対応を終わらせたい(そしてビールを飲みたい)わけなんです。しかもその2つの立場を同一人物が体験することも多いです(個人開発など)。だから、プログラミングツールが、こちらが書いた情報を「解釈」したり整理したりしながらソースコードが読み取れる範囲の仕様を常にアップデートし続けることが必要なのかな、と考えています。IDEでも一部そういう機能は、静的解析という形で実現されているとは思いますが、それをもう少し超えたなにかをイメージして書いています。機械学習を使ったコーディング上のサジェストとかも最近見た気がするので、それほど夢物語でもない気もしますが、はてさて、どうなりますかね、ということで、今日も無事に終わったので、ビールを飲みますか。

レガシーコードを目の前にして

Engineers in VOYAGE ― 事業をエンジニアリングする技術者たち(電子書籍のみ)www.lambdanote.com

www.oreilly.co.jp

WebAssemblyの話ばかり書いてきましたが、現実の業務では、入社以来、ずっとレガシーコードの問題と向き合っているように感じています。個人的には、対処しないといけない範囲では、これまでソフトウェアエンジニアをやってきた経験の中では、一番大きなものですね。いろんな意味で、典型的なケースかと思います。ビジネスはうまくいっているので、余計にメスが入りづらい。入社直後が一番きつかったですが、ちょっと慣れてきている自分もいて、怖いなとも思います。

というわけで、気休めというか、自分なりに対処の方法を考えたいということで上記の書籍を読み直したりしています。いざ「ほんもの」を目の前にすると、ひとつひとつの文章の重みが違って見えます。。。

さて、現時点での考えを整理すると(あまり業務に関わるようなことは書けませんが、せめて事例紹介ということで)、

  • ソフトウェア設計が局所的で、統一感がない
  • ビジネス要求が実装と密結合になっている

ということかなと考えています。最初の点は、大きめの開発組織ならばよくあることかと思います。急激に人員が増えたということ、そしていままで仕組みが中途半端に作られてはアンドキュメントなまま、技術的負債が溜まっていくという話かと。。。わたしはまだ入社して日が浅いわけで主に負債の悪い影響を受けることが多いわけですが、次第に負債を積んでいく割合が増えていくことでしょう。なるべく自分の業務内では「立つ鳥跡を濁さず」をしたいんですが、修正前のコードがすでによろしくない状態の場合は、泣く泣く積み増してしまうことになることも(この短い期間の中でも)ありました。そう、問題箇所を引き剥がせないんですよね、ちょっとソースコードを見ただけでは。

二つ目の点は説明が必要かと思います。とはいえよくある話だとも思います。へーしゃ、結構、企画の人間もほとんどプログラミング経験がある(というかソフトウェアエンジニアだった)ことが多いんですよね。そうなると、ロジックレベルで話ができてしまう反面、修正方法がシステム的な整合性や統一性を欠いていても、実現はできてしまうので話が通りやすいんですよね。いや、それ自体は素晴らしいことだと思います。本当に。ただ、どうしてもこの方法だとすばやく企画を実行していく側に軸足を置く仕様になるので、これも技術的負債が溜まりやすくなる一因となるんですよね。意外と。みんなプログラミングできればハッピーかというと、そうでもなく。みんながドメインモデルとアーキテクチャを共有できていればいいんですけど(夢のまた夢)。

そんなこんなで、読書しながら引き続き、考えます。

WebAssemblyのstack polymorphicとは

ironoir.hatenablog.com

上記の通り、WebAssemblyの型検査を書いています。その中でstack polymorphicという見慣れない言葉が出てきて、それがなんなのか、またその部分をどう実装するのか考えている最中、という話です。

stack-polymorphic: the entire (or most of the) function type [𝑡1] → [𝑡2] of the instruction is unconstrained. That is the case for all control instructions that perform an unconditional control transfer, such as unreachable, br, br_table, and return.

仕様にも定義というか説明があります。関数型のすべて、もしくは一部が未制約であるような性質、と訳していいのかわかりませんが、具体的には以下の命令がstack-polymorphicだということです。

  • unreachable
  • br
  • br_table
  • return

検索しても特に追加の情報は出てこないので、独自の概念かと思われます。

ただ、polymorphicということで身構えてしまったものの、例を読んでいるとなんだか、「多相性」という用語から連想するあれこれと、実際に書こうとしている型検査のアルゴリズムが(少なくとも今回に限っては)関係ないような感触を持っています。

The unreachable instruction is valid with type [𝑡1] → [𝑡2] for any possible sequences of value types 𝑡1 and 𝑡2. Consequently,

unreachable i32.add

is valid by assuming type [] → [i32 i32] for the unreachable instruction.

unreachable命令がわかりやすい例として挙げられています。i32.addは2つの32ビット整数を足し合わせるわけなので、必要な引数(スタック上にあるべき値)の数は当然2つです。そしてunreachableはそれに合わせる形で[i32 i32]を返すことにされます。つまり、命令列の前後が必要としている型に柔軟に変化するのがstack-polymorhicな型付けと言えるようです。

これを実装する場合は、命令の列を型付けする際に上記のどれかの命令が出てきたら、その前後の型を当てはめることで辻褄合わせをするだけでいいような気がしています。まだ実装していないのでやってみて問題に気がつくかもしれませんが、思っていたよりも単純な話で済みそうです。

WebAssemblyのvalidation(型検査)を書いている

WebAssemblyの仕様には、しっかりした(Soundnessが証明された)Validationが、まるまる一つの章をかけて定義されています。自分が作っている処理系では、興味はあったんですけど今まで実装は後回しになっていました。しかしよくよく見るとこれは型推論のロジックが書いてあるに等しいわけで、めちゃくちゃ面白いので一見でいける部分は一気に実装してしまいました。数が多いinstructionも全て個別に検査のロジックが書いてありますが、物量は多いものの共通している動作も多いので、ある意味ではそこまで考えなくても実装できます。

さて、しかし、残ったのが「命令列」にどうやって型をつけるのか、という話。画像がそれですが、実装の方法をまだ考え中です。

f:id:ironoir:20210228174806p:plain

定義は大きく二つに分かれていて、最初は空の命令列に対してのもの。これは簡単…かとおもったら、実はそうでもなくて、

The empty instruction sequence is valid with type [𝑡 ] → [𝑡 ], for any sequence of value types 𝑡 .

問題は最後で、「型はなんにでもなるよ」と書いてあるようにも見えます。つまり単体では決まらず、具体的な命令列ががあって初めて決まる、ということらしいです。空列なのに?という思いもありますが…ちなみに、Nop命令(=何もしない命令)は() -> ()、つまり引数も戻り値もない型が単独で決定されます。空列は、これとは違います。

さて、もう一つのメインの定義も意外とややこしく見えます。再帰的に定義されていて、命令列の後ろ側から一つずつ見ていくように読めます。ざっくり考えてみると、命令列が順番に実行される場合、命令列全体の戻り値の型は、最後の命令の型になりそうです。反対に、命令列全体の引数の型は、最初の命令の引数の型になりそうですよね。だから型を推論するだけならば命令列の全体を読む必要はないんでしょうけど、今やろうとしているのは、この命令列がvalidかどうかの検査なので、そんな横着はできません。なので、

  • 最後の命令の引数が、それ以前の命令列が産みだす戻り値に含まれていないといけない

ですよね(これは画像の推論図式を見るとわかりやすいかも)。

加えていうと、

  • 最後の命令の引数の型=それ以前の命令列が産みだす戻り値の型である必要はない

です。前者が後者を含んでいればいい(ただしそれらはスタックの一番上にある必要はあるでしょうけど)。推論図式では添字が何もない、𝑡*がそれにあたりますね。

あと、おもしろいなと感じたのは、もし命令が複数の値を返す場合、スタック上にある値を次の命令がすぐ使うとは限らないということ。プログラミング言語での関数の合成だと、一つの関数の戻り値は基本的にそのまま次の関数に渡されますが、今回のWebAssemblyの仕様だと、前の命令の処理結果を次の命令が必ずしも使い切る必要はない、ってことですもんね。いや、推論図式で書いてあることを別の角度から言い直しているだけなんですけど。

Rustで単純に再帰で書くよりはループしたほうが良さそうですね。整理できてきた気がするので、また書いてみます。

あ、あと、stack-polymorphicの話が書けなかった。これもいずれ。

WebAssemblyのEmbedderからのインターフェース一覧

f:id:ironoir:20210227164137p:plain

WebAssemblyは単体で完結するよりは、ホスト環境に埋め込まれて使う用途が想定されているようです(ただし、最近はそれだけでもない)。例えば、JavaScriptが典型的なホスト環境だと言えます。specには、それらのEmbedderとWebAssemblyのインターフェースが規定されていて、整理のために以下、概略とともに一覧をRustコードで示します。詳細、気になる点もありますが別途。

なお、これらを実装していることがEmbedderの必須条件ではない、と書かれている点は注意です。

mod store;
pub use store::store_init;  // storeを初期化する

mod module;
pub use module::{
    module_decode,  // バイト列を読み込んでmoduleを返す
    module_parse,  // utf-8文字列を読み込んでmoduleを返す
    module_validate,  // moduleを検査する
    module_instanciate,  // moduleを初期化してinstanceを生成する(要素をstoreに入れる)
    module_imports,  // 指定したmoduleのimport情報を全て返す
    module_exports,  // 指定したmoduleのexport情報を全て返す
};

mod instance;
pub use instance::instance_export;  // 特定のinstanceから指定したexport要素の値を返す

mod func;
pub use func::{
    func_alloc,  // 指定したhostfuncをstoreに割り当てる
    func_type,  // 指定したfuncaddrの型を返す
    func_invoke,  // 指定したfuncaddrにひもづく関数を指定した引数で呼び出し、結果を返す
};

mod table;
pub use table::{
    table_alloc,  // 指定したtabletypeからtableを生成し、storeに割り当てる
    table_type,  // 指定したtableaddrのtabletypeを返す
    table_read,  // table内の指定した場所にあるfuncaddrを返す
    table_write,  // table内の指定した場所にfuncaddrを書き込む
    table_size,  // tableの長さを返す
    table_grow,  // 指定した長さだけtableを伸ばす
};

mod mem;
pub use mem::{ 
    mem_alloc,  // 指定したmemtypeからmemを生成し、storeに割り当てる
    mem_type,  // 指定したmemaddrのmemtypeを返す
    mem_read,  // mem内の指定した場所にあるbyteを返す
    mem_write,  // mem内の指定した場所にbyteを書き込む
    mem_size,  // memの長さを返す
    mem_grow,  // 指定した長さだけmemを伸ばす
};

mod global;
pub use global::{
    global_alloc,  // 指定したglobaltypeからglobalを生成し、storeに割り当てる
    global_type,  // 指定したglobaladdrのglobaltypeを返す
    global_read,  // global内の指定した場所にある値を返す
    global_write,  // global内の指定した場所に値を書き込む
};

PlantUMLコード

@startuml rectangle hostenv { rectangle embedder interface funcs as funcs rectangle wasm { } wasm -up-( funcs embedder -down- funcs } @endum

WebAssemblyでクロージャ実現にWeak<T>を使ってみる

ラノベのような雰囲気のタイトルになってしまった。 順番にいきましょう。

RustのWeak<T>

RustのWeak<T>に関しては、公式がすばらしい文章を日本語で出してくれています。本当に感謝しかないです。とにかく読みましょう。

doc.rust-jp.rs

WebAssemblyのクロージャ

さて、それはさておいて、突然クロージャの話になりました。クロージャとはざっくりいうと環境付きの関数ということですが、この場合はStoreが環境にあたります。ほかのプログラミング言語だと環境がネストできたりしますが、今回はグローバル環境(=Store)だけが対象の話です。

Rustで実装しようとすると困るのが、以下のように、参照関係が循環していることです。

f:id:ironoir:20210224125520p:plain

storeは関数実体のリストであるfuncsを所有しています。そして、その関数実体ひとつひとつからは、環境であるmoduleinstが参照されています。moduleinstの中身はほとんどがstore実体へのポインタなので、当然moduleinstからstoreへの参照も存在します。こんな時にはRustだとWeak<T>を使うとよいみたいです。

それを踏まえると、宣言は以下のようになる感じでしょうか。

struct Store {
    funcs: Vec<FuncInst>,
    tables: Vec<TableInst>,
    mems: Vec<MemInst>,
    globals: Vec<GlobalInst>,
}

struct FuncInst {
    tp: FuncType,
    module: ModuleInst,
    code: Func,
}

struct ModuleInst {
    funcaddrs: Weak<Vec<FuncInst>>,
    tableaddrs: Weak<Vec<TableInst>>,
    memaddrs: Weak<Vec<MemInst>>,
    globaladdrs: Weak<Vec<GlobaInst>>,
}

木構造の子から親を見たいときにWeak<T>を使うのが最もオーソドックスなユースケースだと思いますが、今回もたぶん、そうですね。このままうまくいくかどうか、実装を続けたいと思います。

PlantUMLコード

@startuml

rectangle store
rectangle funcs
rectangle moduleinst

store -up-> funcs
funcs -up-> moduleinst
moduleinst .up.> store
@enduml

WebAssemblyのRuntime Structureを思い出す

一回インタプリタを作ったんですが、結構前なので忘れてるんですよね。というわけで仕様を再確認します。

WebAssemblyのVMはスタックマシンで、内部は大きくStoreStackの2つに分かれます。

The store represents all global state that can be manipulated by WebAssembly programs.

ということで、storeは関数、テーブル(関数ポインタを持っていると思ってください)、線形メモリ、グローバル変数など、全域的な状態を保持しています。stackはもちろん読んで字の如し、スタックなんですが、種類としてはさらに以下の3つに分かれます。

Values: the operands of instructions. Labels: active structured control instructions that can be targeted by branches. Activations: the call frames of active function calls.

Valueは通常の即値のことでi32 i64 f32 f64の4種類。Labelは、一言で言うとジャンプ先の目印のことで、ループなどのブロック構造からの脱出先になったりします。というかプログラミング言語でも使えるものも多いですよね。最後のActivationはいわゆる関数のローカル変数などを持つcall frameのことです。ちなみに、仕様上、WebAssemblyはこれらの実装方法にまでは踏み込んでいません。つまり、これらを同一のスタックで実現してもいいし、種類ごとに3つに分けても構いません。

もう一つ、重要な構造としてはモジュールインスタンスがあります。

A module instance is the runtime representation of a module. It is created by instantiating a module, and collects runtime representations of all entities that are imported, defined, or exported by the module.

これはモジュールを初期化すると出来上がるもので、上記Storeが持っている内容へのポインタを所持しています。Storeにない情報としてはexport instanceがあって、ここにはexportしている要素(関数やグローバル変数)の情報が入っています。

たぶん、一読して難しいのはStoreとモジュールインスタンスの違いではないかと思います。鍵は、モジュールはimportやexportが可能であり、複数のモジュールで定義された要素がVMに入る可能性がある、ということだと思います。言い換えれば、Storeは各要素の、VM上で扱う全ての実体を持っていている一方、モジュールインスタンスはあくまでも1つのモジュールに関する情報をStore内要素へのポインタとして保持している、ということです。