0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

marimo v0.23.1 の死角。WASM上に新NISAシミュレーターを構築したら、ランタイムバリデーションにセルを全滅させられた記録

0
Posted at

はじめに:論理テスト(Pytest)ALL GREENの先にある「ランタイムの罠」

Pythonをサーバー不要、維持費0円でブラウザ上にデプロイできる marimo × WASM (Pyodide) のスタックは、現代の個人開発における非常に強力な選択肢です。
データが外部に一切漏れないため、機密性の高い資産データを扱う金融シミュレーターなどとの相性は抜群に良いと感じています。

今回は第11弾プロジェクトとして、新NISAの「簿価復活枠」をFIFOで厳密管理し、過去の暴落相場(リーマンショック等)のデータをNumPyで適用するツール『NISA Simulator v4.0』の開発に挑戦しました。
計算エンジンのロジックは完璧に組み上がり、ローカルでのHeadless自動テスト(Pytest)も無事にすべて一発で通過(ALL GREEN)しました。

「これでいける!」と確信してブラウザ上でUIをロードしたのですが、なんと画面がクラッシュしてしまいました。
自動テストでは検知できなかった、marimoのランタイムバリデーションと、v0.20+から導入された強力な構文解析器(AST)の仕様に引っかかってしまったのです。

この記事では、AIに「なんとなく(Vibe)」でコードを書かせた結果ぶつかってしまった、marimo固有の特殊なスコープ規則やライフサイクルの罠と、その解決策について、泥臭い奮闘の記録を共有したいと思います。

1. 🛠️ ドロップダウン初期値の罠(ValueErrorとの遭遇)

Webアプリケーションにおいて、選択肢(ドロップダウン)の表示名と内部で扱う実体をマッピングするのは基本ですよね。
marimoでは mo.ui.dropdown に辞書型 {"表示ラベル": "内部データ実体"} を渡すことでこれを実現します。

今回、AI(Code Architect)は、初期選択状態を指定するために一般的なWebフレームワークの感覚で、引数 value に内部実体(例: "normal")を指定したコードを出力してくれました。

Sample_code-1
# 💣 クラッシュを引き起こしてしまったコード
inputs = mo.ui.dictionary({
    "scenario": mo.ui.dropdown(
        options={"通常運用 (年利 +5%)": "normal", "歴史的暴落": "stress"},
        value="normal",  # 👈 これを指定すると、ValueErrorでセルが停止します
        label="市場リターン・シナリオ"
    )
})

自動テストをパスしたはずのコードは、ブラウザにロードされた瞬間、ValueError: The option name 'normal' is not a valid option. という例外を出して止まってしまいました。

【原因と解決策】
marimoのバリデーションエンジンは、辞書型が渡された際、内部実体ではなく「辞書のキー(表示ラベル名)」を選択肢の正当性検証の基準として扱う仕様になっていました。解決策としては、初期値 value に実体ではなく「キーの文字列(ラベル名)」を指定します。選択された後に下流へ渡される .value は、正常に対応する実体("normal")へと内部で翻訳されるため、ロジック層を汚すことなく無事にバリデーションを通過できました。

2. 🛠️ 過剰な隠蔽とセル隔離の罠(連鎖するNameError)

marimoのDAG(有向非巡回グラフ)をクリーンに保つための大原則に、「セル内で完結する一時変数にはアンダースコア(_)を付与し、グローバルスコープへの漏洩を防ぐ」というルールがあります。

しかし、このルールを叩き込まれたAIは、少し過剰に守りに入ってしまったようです。他のセルから呼び出されるべき中核関数名やインポートオブジェクトの先頭にまで、盲目的に _ を付与してしまいました。

Sample_code-2
# ❌ AIが過剰に隠蔽してしまったコード(関数定義セル)
def _nisa_quota_revival_engine(current_book_value_total, ...):
    # 内部ロジック
    return { ... }

# グローバル空間にパブリックとして公開したいのに先頭に「_」を付けてしまっている
return _nisa_quota_revival_engine,

下流の計算セルは、この非公開にされたノードを呼び出そうとした瞬間に以下のエラーを出してしまいます。

Sample_code-3
# 💣 下流の計算セルで発生するエラー
# NameError: name '_nisa_quota_revival_engine' is not defined
_quota_res = _nisa_quota_revival_engine(...)

関数定義セルの最下部でどれだけ明示的にオブジェクトを return していたとしても、marimoの静的解析器は「先頭に _ がついたオブジェクトは、そのセル内部に完全隔離される」と解釈します。そのため、他セルへの参照が物理的に切断され、下流のセルが呼び出そうとした瞬間に NameError を起こしてアプリ全体が停止してしまいました。

さらに、 marimo ファイルの最上部にある定型文の import marimo に依存し、セル内部での import marimo as mo および return mo を怠ると、各セルの独立スコープの壁に阻まれ、下流の全セルで mo が未定義となりエラーになってしまいます。

【原因と解決策】
インターフェース(外部に流通させる関数やUIコンポーネント名)は、必ずアンダースコアなしのパブリックとしてエクスポートする必要があります。アンダースコアを使うのは、あくまで「関数内部」のローカル変数や、ループ変数などの使い捨てノードに限定します。この境界線を意識してコードを修正することで、各セルの連携は正常に動作するようになりました。

3. 🛠️ 早期Returnの罠(SyntaxErrorとUIが消える現象)

marimo スクリプトの実体は、単一の .py ファイル内に @app.cell デコレータでラップされたPython関数の並びです。AIはこの構造から「処理を途中でスキップさせたい」と考えたようで、セルの直下(トップレベル)に直接 return 文を記述したり、最終統合UIセルの最下部に return final_assembly_ui, を記述するという構文エラーを出力してしまいました。

Sample_code-4
# 💣 SyntaxError、あるいは画面が「真っ白」になるコード
if inputs.value is None:
    return  # 👈 SyntaxError: 'return' outside function を誘発

final_assembly_ui = mo.vstack([ ... ])
return final_assembly_ui,  # 👈 画面のUIが1ピクセルも描画されなくなってしまいます

【原因と解決策】
marimoのセルは、記述上は「通常のPythonスクリプトのトップレベル」として書かなければなりません。そのため、単独で return を配置した瞬間にインタプリタから構文エラーを宣告されてしまいます。

さらに気をつけたいのが、最下部の最終統合セルでの明示的な return です。
ここでオブジェクトを関数のようにリターンしてしまうと、marimoはそれを「下流のセルへ引き渡すための静的な変数ノード」としてエクスポートするだけで、ブラウザへの視覚的なレンダリング(描画)をシステムレベルでスキップしてしまいます。
結果として、コンパイルエラーも出ずに「画面が完全に真っ白」になる現象が発生してしまいました。

これを解決するため、「単独のreturnを使わない」 ことと、 「暗黙の出力(Implicit Output)の徹底」 を行いました。早期終了を狙うのではなく、if/else 構文によって変数への代入を完全に条件分岐させ、セルの最終行にその変数をただ「置いて評価させる」という marimo 正規の出力方法に従います。

Sample_code-5
# ⭕ エラー時でも止まらないオブジェクト流通構造(関数の外なのでreturnは不使用)
if df_results is None:
    # 不正入力時もプロセスを停止させず、警告UIオブジェクトを流通させる
    chart_ui = mo.ui.callout(mo.md("⚠️ 正しい数値を入力してください"), kind="warn")
else:
    # 正常系の描画
    _chart = alt.Chart(df_results).mark_line().encode(x="経過年:Q", y="総資産:Q")
    chart_ui = mo.ui.altair_chart(_chart)

# セルの最終行に配置し、暗黙の出力でレンダリングを強制させる
chart_ui  

エラー時であっても mo.stop() で処理を切断せず、正常な None や警告UIオブジェクトとして下流に渡します。この形に移行することで、ユーザーが誤ったパラメータを入力してもクラッシュせず、入力パネルを残したまま警告を動的に表示できる、堅牢なUXを作ることができました。

🎯 まとめ:人間の役割は「境界線の設計と理解」

今回の開発で痛感したのは、AIエージェントに「雰囲気(Vibe)」だけでコードを書かせ、インターフェース設計まで委ねることの難しさです。AIの自律的なコーディング能力を最大限に活かすためには、人間側が「marimo固有の特殊なライフサイクル」をしっかり把握し、AIが脱線しないように適切な制約やルール(ハーネス)を設けてあげる必要があります。

WASMというサーバーレス環境で、正確な計算処理と安定した画面制御を両立させるために、ツールの仕様を深く理解してうまく付き合っていくこと。それこそが、AIと一緒に開発を進める上で、私たち人間に求められる役割だと感じました。

おまけ

今まで開発したプロダクト一覧については、こちらからどうぞ

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?