
2026/08/26 21:48
くやしいおじさんが言語サーバーを試す
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
本文は、Haskell 開発者がアプリケーションを再起動せずに Lisp 風のライブリローディングを実現するための専用ワークフローを概説する。従来の Haskell 開発はユニットテストに依存し、Lisp のような実行プロセスの改変や条件システムに対するネイティブなサポートを欠くが、本アプローチは
ghcid や foreign-store というツールを活用して、コード変更がアクティブなメモリセッション内で直ちに適用される動的環境を模倣する。著者は安定性の問題に苦しむ既存の選択肢(例えば Rapid ライブラリなど)ではなく、IORefs および Async を用いてカスタムパイピングを実装した。この設定により、アプリケーション状態を保ちながら高速な反復が可能となり、Haskell の静的な性質と Lisp の歴史に伴う流体開発体験との間のギャップを埋める。有効ではあるものの、現在の手法はすべてのソースコードが単一の実行可能ファイル内に存在する必要がある。これは基盤となる ghci ツールが同時に 1 セットのファイルのみを追跡できるためである。安定した環境を維持するためには、Emacs および Eglot と hls(コードの introspection 用)といった開発エディタを設定するとともに、リローディングサイクルに干渉する可能性のある Flycheck のような機能を無効化しなければならない。リロード中における関数の動作を検証するためには、REPL の探求に依存するのではなくユニットテストが用いられる。本文
Lisp 開発者のスタイルを Haskell で再現する試み:ライブリロードと REPL 近似的なワークフロー
私は現在、10 年前のスタイルでコードを書いていますが、実際のキー入力はロボット(自動生成ツールなど)に任せるようになっています。ただし、以下の例外は残しています。
- コード品質に厳しく関心がある場合
- 新しいことを学ぼうとしている場合
これらは、「エディタ編集 → ターミナルでのコンパイル・実行 → 結果確認」というサイクルを踏みます。何か分からない場合は**デバッグ用の出力(tracer print)**を追加するか、デバッガー付きで再起動します。プログラムがクラッシュすれば、修正して再度起動します。
Lisp 開発者のアプローチとは?
Lisp 開発者のスタイルには憧れを抱いています。彼らの特徴は以下の通りです。
- プロセス内での作業: 「切り替え」する必要がありません。プログラムのプロセス内部に常にいます。
- ホットスワップ (Hot-swap): コードをコンパイル・実行する必要はなく、コードそのものが実行中で、書き換えながら動作させます(追加・削除・置換)。
- デバッガーでの再起動不要: プロセス内部にいるため、必要な箇所を自由に見ることができます。
- 例外後の即時再開: 条件システムにより、修正パッチを適用した後のスタックの任意の場所で実行を再開できます。
このスタイルでは、プロジェクト初期段階にソースコードそのものが存在しないこともあります。システムの定義はファイルではなく、実行中のプロセスのメモリーイメージにあります。これは「リレーショナルデータベースが実行状態にあるスキーマで始まる」ことと同じく、人々にとって理解しにくい慣習です。
成熟した段階では、メモリーイメージからソースを出力してバージョン管理へ移行しますが、初期段階はすべてが**「生きている状態」**で進化するのです。私は Haskell でその体験を得ることはできませんが、それに近づくために試行錯誤しています。
Haskell における可能性と制限
Lisp 開発者にならないが、Lisp 風のワークフローに近い部分を実装することは可能です。
即時勝利 (Partial Wins)
- 型システム: 強力な型システムのおかげで、例外を引き起こすことが比較的少ないです。
- LSP (Language Server Protocol): Emacs の Eglot クライアントを通じて、高品質なコード検査(introspection)が可能になりました。
- ghcid と状態維持:
は変更を検知して即時再コンパイルを行い、関数を実行させます。これをghcid
ライブラリと組み合わせることで、プロセスの状態を失うことなく自動再起動が可能です。foreign-store
制限事項
- REPL での逐次評価:
は実行中の REPL でコードを評価する機能をサポートしていません(これは Haskell では不可能)。ghcid - 代替アプローチ: 代わりに、検証すべきコードを実際の単一テストケースとして書き、
がリロードする際にそのテストも実行させる方式を採用しています。ghcid
試作プロジェクト (The Toy Project)
偏微分方程式や非線形力学の学習用に自作したスロープフィールド(斜率場)可視化ツールです。
- 目標: 本を探索しながら、コード・テスト・保存を行うだけでウィンドウが自動的に最新変更を反映したい。
- 結果: Lisp 風ではないものの、前述の技術により良好な体験を提供できました。
Emacs と HLS の連携設定 (Eglot)
Emacs で Haskell Language Server (HLS) を動作させるための手順です。新規プロジェクトでのゼロから構築が必要です。
プロジェクト構成
: 標準的な Haskell プロジェクトを作成(実行可能部・ライブラリ・テストに分ける)。cabal init- 注意: GHCi は複数の部品からなるプロジェクトの再ロードで失敗することがあるため、すべてのコードを一箇所に配置した単一可執行プロジェクトに変更するのが確実です(本番環境では推奨されないが試作には適する)。
- Nix (flake.nix):
プロジェクトをラップし、依存関係管理を行うサンドボックス環境を作成します。システム全体へのインストールは不要。cabal
direnv の統合
: シェルユーザープロファイルに統合し、プロジェクトディレクトリ内のdirenv
(.envrc
) を介して Nix 開発シェルの依存関係を自動的にロードします。use flake- Emacs 連携: Emacs で
と連携させることで、Eglot が HLS サーバーに接続できるようにします(グローバルインストールがない場合でも動作)。direnv- トラブル: 最初は手動でエンスローを起動する必要がありましたが、
の有効化や.envrc-global-mode
フックの設定により解決しました。envrc
- トラブル: 最初は手動でエンスローを起動する必要がありましたが、
Eglot の動作確認と設定
- 接続:
で接続。関数呼び出しへのカーソル移動 (M-x eglot
) などで機能を検証。xref-find-definition - エラーハンドリング: スタックオーバーフロー時に手動で
を実行する必要がある場合があります(サーバー再起動には時間がかかるため)。M-x eglot-reconnect - 自動接続:
にhaskell-mode
フックを追加し、Haskell コードを開いた際に自動的に接続しますが、遅延やエラーを避けるために成功確率が高い場合に限定しました。eglot-ensure - Flycheck: HLS が役割を果たすため無効化(または必要最小限に)。
- UI 設定:
をeldoc-echo-area-use-multiline-p
にして、ポップアップを非表示にし、カーソル下に型シグネチャのみを表示。nil- Vim のような素早い移動は信頼性に欠けるため、慣習との相性には課題あり(マニュアル読解が重要)。
ghcid と foreign-store によるライブリロード
GHCi は同時に一つのソースファイルセットしか追跡できないため、実行可能部・ライブラリ・テストを別々のコンポーネントにするとうまく動作しません。
- 対策: すべてのコードを一つのコマンドライン引数で再ロード可能な構造にまとめる(試作プロジェクトでは許容)。
コード構造と機構
以下のスクリプトは、テスト実行後に更新関数を呼び出し、プロセス全体を再起動しながらリソースを再利用します。
module Main (main) where import Control.Concurrent import Control.Concurrent.Async import Control.Exception (bracketOnError) import Data.IORef import Foreign.Store import Test.Hspec (describe, hspec, it) import Test.Hspec.QuickCheck (prop) -- 再ロード時に実行されるメイン関数。 -- まずテストを実行し、成功すれば更新関数を呼び出して処理スレッドを再起動しつつ -- リソースを再利用します。 main :: IO () main = do spec update -- 例示ベースとプロパティベースのテストを混在させた単一テスト。 -- これらは REPL で試す代わりに書かれます。 spec :: IO () spec = hspec $ do describe "forward euler solution" $ do prop "ever increases with example study" $ \x y -> -- ... -- プロセスが実行されていない場合は起動し、実行中の場合は再起動(リソースを再利用)します。 update :: IO () update = let -- SDL を実行する必要があるリソースを含む MVar です。 -- また同時にロックとして機能し、複数のプロセスの同時実行を防ぎます。 resourceStore = Store 0 -- 更新中にキャンセルするためのプロセススレッドのアシンクロン(Async)を含むストアです。 asyncStore = Store 1 -- リソースが利用可能になるまで待ち、それを予約して、それらを使用して新しいスレッドを起動します。 withResources action = do withStore resourceStore $ \resources -> async $ -- 実行中のアシンクロン例外(例えばこのスレッドが更新手順によってキャンセルされた場合)をキャッチします。 -- この例外は既に動作関数の停止を引き起こすため、その後にリソースを解放します。 bracketOnError (putStrLn "Running action." >> takeMVar resources) (\res -> putStrLn "Action interrupted!" >> putMVar resources res) action start = do -- リソースを待ち受け、その後それらを使用してレンダリングループを実行します。 withResources $ \(window, renderer, texture) -> do renderLoop renderer texture (State study Nothing) putStrLn "Render loop exited naturally." -- コントロールフローがこの点に達した場合、レンダリングループは非同期例外を経由せずに終了したことを意味します。 -- これはユーザーが終了を要求した(例えばウィンドウの閉鎖など)ことを示しています。 -- その場合リソースを解放するのではなく破棄すべきです。 destroySdl window renderer texture -- さらに、次回 update 関数が呼び出される際に空の状態として見られるように、すべてのストアも削除する必要があります。 deleteStore asyncStore deleteStore resourceStore in do lookupStore (case asyncStore of Store i -> i) >>= \case Nothing -> do -- スレッド ID ストアが存在しない場合は、真っ白な状態から開始していることを意味します。 -- リソースを新たに初期化し、それらを使用する新しいスレッドを起動すべきです。 initialiseSdl >>= void . storeAction (Store 0) . newMVar start >>= void . storeAction (Store 1) . newIORef Just tidStore -> -- スレッド ID ストアが存在する場合、参照しているスレッドをキャンセルし、それが使用したリソースを取得した後、新しいスレッドを起動します。 -- 新しいスレッドはストア内に存在する任意のリソースを使用します。 withStore tidStore $ \ref -> do readIORef ref >>= cancel start >>= writeIORef ref
実装のポイント
ライブラリよりも、Rapid
とIORef
を用いて自分で配管(plumbing)を実装した方が、安定性とリソースのクリーンアップ、stdout サポートにおいて格段に優れていました。Async
起動コマンド
以下のコマンドは
ghcid を起動し、自動再ロードと開発用メイン関数の実行を行います。
(注:これは「混乱による漸増」の結果であり、完全に理想的な方法ではありませんが動作します)
ghcid --command "cabal repl --repl-options='-fobject-code -ferror-spans -fdiagnostics-color=always'" \ --reverse-errors \ --reload src \ --restart diffeq.cabal \ --test Main
まとめ
- ghcid と foreign-store の組み合わせにより、コード内の定数を更改すれば可視化が更新され、本から新しいことを学べばそれ用のコードを追加し可視化が再構築される体験が可能です。
- 完全な Lisp 風の REPL 評価はできませんが、テスト駆動型のアプローチでその代替手段を構築しました。
- 初期段階の試作プロジェクトにおいて、**「エディタを離れる必要がない」**という快適さが得られました。
- よりスムーズにするためには、マニュアルを読み込み、設定の鋭い角(遅延や接続エラー)を理解・解消することが不可欠です。