mcpField
G
gleam-practice

gleam-practice

github.com/mizchi開発41663 回インストール1 週間前に更新
リポジトリ

gleam-practice について

gleam-practiceは、Gleamプロジェクトの新規作成や保守を支援するスキルです。wisp、mist、gleam_otpを使う構成を含め、構成設計、テスト、監督ツリー、CIの指針を提供します。

主な機能

  1. 01Gleamプロジェクトのセットアップ
  2. 02ドメインとWeb層の分離
  3. 03OTPアクターと監督パターン
  4. 04単体・スナップショット・プロパティテスト
  5. 05ローカルとCIの共通ワークフロー

活用シーン

  1. 01ErlangターゲットのGleamプロジェクト作成
  2. 02wispとmistを使うWebアプリの構成
  3. 03純粋なロジックとアクターAPIのテスト
  4. 04一貫したCIチェックの設定

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/mizchi/skills --skill gleam-practice

インストール後、Claude Code で/gleam-practiceを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

README.md
SKILL-ja.md
assets/
justfile
README.md
assets/bench/
http.js
assets/github-actions/
ci.yml
assets/test/
property_test.gleam
qcheck_parallel_test.gleam
snapshot_test.gleam
timeout_test.gleam

詳細

Gleam の実践

新しい Gleam プロジェクトを作成する場合、既存のプロジェクトを改善する場合、または wisp + mist + gleam_otp + just のスタックで実装する場合に使います。

デフォルトのワークフロー

  • gleam new と gleam add から始めます。依存関係は手動で記述せず、ソルバーに選ばせます。
  • TDD スタイルで進めます:explore -> Red -> Green -> Refactor。
  • 純粋なロジックと状態管理を分離します。ドメインモジュールとアクター/サーバーモジュールを分けます。
  • 公開 API は小さく保ちます。pub opaque type を優先し、内部コンストラクターやプロトコル型を安易に公開しないでください。
  • @external は最後の手段として扱います。使う場合は、薄いアダプターモジュール内に閉じ込めます。
  • Web レイヤーは薄く保ちます。wisp ハンドラーはデコード、サービスの呼び出し、レスポンスのエンコードに集中させます。
  • タスクの実行は justfile に集約し、CI でも just ci を実行します。

プロジェクトのセットアップ

Erlang ターゲットの新規プロジェクトでは、これを出発点として使います。

gleam new my_app --template erlang
cd my_app
gleam add wisp mist gleam_otp gleam_erlang
gleam add gleam_json envoy
gleam add --dev gleeunit

必要に応じて、以下を追加します。

  • HTTP クライアント:gleam add gleam_http gleam_httpc
  • ファイル I/O:gleam add simplifile filepath
  • スナップショットテスト:gleam add --dev birdie
  • プロパティテスト:gleam add --dev qcheck qcheck_gleeunit_utils
  • インタラクティブなテストループ:gleam remove gleeunit && gleam add --dev glacier
  • 未使用エクスポートのチェック:gleam add --dev cleam
  • ベンチマーク:gleam add --dev glychee
  • ログ設定:gleam add logging
  • ライブリロードの開発者体験:gleam add --dev olive

外部 CLI:

  • HTTP 負荷テスト:k6

wisp と mist は互換性のある組み合わせを使います。バージョンを個別に固定するより、gleam add wisp mist をまとめて実行し、ソルバーに同時に解決させるほうが安全です。

テンプレート:

  • assets/README.md
  • assets/justfile
  • assets/github-actions/ci.yml
  • assets/bench/http.js
  • assets/test/snapshot_test.gleam
  • assets/test/property_test.gleam
  • assets/test/qcheck_parallel_test.gleam
  • assets/test/timeout_test.gleam

gleam.toml のデフォルト設定

最低限、以下を設定します。

name = "my_app"
version = "0.1.0"
target = "erlang"
description = "Short package summary"
licences = ["Apache-2.0"]
repository = { type = "github", user = "owner", repo = "repo" }

# Hide modules that are only used inside the package
internal_modules = [
  "my_app/internal",
  "my_app/http/internal",
]

原則:

  • メタデータを空のままにしないでください。
  • internal_modules でパッケージ内部のモジュールを非公開にします。
  • 広い semver 範囲を指定してもかまいませんが、CI では常にロックファイルを固定するようにしてください。

推奨レイアウト

まずは次の分割から始めます。

src/
  my_app.gleam                # entrypoint
  my_app/app.gleam            # DI and supervision startup
  my_app/web.gleam            # router / request handling
  my_app/domain.gleam         # pure domain logic
  my_app/domain_server.gleam  # actor wrapper
  my_app/http_json.gleam      # encoder / decoder
test/
  my_app_test.gleam
docs/
  reference-ja.md
  reference-en.md
bench/
  main.gleam
justfile

分離の基準:

  • domain.gleam:純粋関数のみ
  • *_server.gleam:アクター/監視/タイムアウト/メールボックス
  • web.gleam:ルーティングと HTTP マッピング
  • http_json.gleam:JSON スキーマとコーデック
  • app.gleam:配線のみ

ルーター、JSON コーデック、ビジネスロジック、アクターの状態を1つのファイルに混在させないでください。

規模が大きくなったら、次のように分割します。

  • web_*.gleam:機能ごとのハンドラー
  • web_json_*.gleam:ドメインごとのエンコーダー/デコーダー
  • agent_*.gleam:プロトコル、ランナー、ツール、トランスポート
  • workspace_*.gleam:オーバーレイ、パッチ、git、セッション、ランタイム
  • *_test_support.gleam:テストヘルパーを責務ごとに分割

Wisp + Mist のスケルトン

src/my_app.gleam

import envoy
import gleam/erlang/process
import gleam/int
import gleam/result
import mist
import my_app/app
import my_app/web
import wisp/wisp_mist

const default_port = 4000
const default_secret_key_base =
  "local-dev-secret-key-base-local-dev-secret-key-base-1234567890"

pub fn main() -> Nil {
  let port =
    envoy.get("PORT")
    |> result.map(int.parse)
    |> result.flatten
    |> result.unwrap(default_port)

  let secret_key_base =
    envoy.get("SECRET_KEY_BASE")
    |> result.unwrap(default_secret_key_base)

  let assert Ok(app_state) = app.start()
  let assert Ok(_) =
    web.app(app_state)
    |> wisp_mist.handler(secret_key_base)
    |> mist.new
    |> mist.bind("0.0.0.0")
    |> mist.port(port)
    |> mist.start

  process.sleep_forever()
}

src/my_app/web.gleam

import gleam/http
import my_app/app.{type App}
import wisp

pub fn app(app: App) -> fn(wisp.Request) -> wisp.Response {
  fn(request) {
    case request.method, wisp.path_segments(request) {
      http.Get, ["healthz"] -> wisp.text_response(200, "ok\n")
      _, _ -> wisp.not_found()
    }
  }
}

Web レイヤーの原則:

  • wisp.require_json でペイロードを読み込みます。
  • デコーダーに失敗した場合は 400 を返します。
  • ドメインエラーを明示的に 4xx/5xx にマッピングします。
  • ハンドラーは規模が大きくなったら機能ごとに分割します。

OTP パターン

基本形は2つのレイヤー、pure state と server です。

重要:gleam_otp は Erlang の gen_server を静的型付きでラップしたものであり、その API は gen_server と互換性がありません。:gen_server.call/:gen_server.cast を直接呼び出さないでください。すべてを Subject と actor.Message の世界に収めてください。

pure state
pub opaque type State {
  State(hits: Dict(String, Int), limit: Int)
}

pub fn new(limit: Int) -> State {
  State(hits: dict.new(), limit: limit)
}

pub fn incr(state: State, key: String) -> #(State, Bool) {
  let n = dict.get(state.hits, key) |> result.unwrap(0) + 1
  #(State(..state, hits: dict.insert(state.hits, key, n)), n <= state.limit)
}
actor wrapper(具体例)
import gleam/erlang/process.{type Subject}
import gleam/otp/actor
import gleam/otp/supervision

pub opaque type Message {
  Check(key: String, reply_to: Subject(Bool))
  Reset(key: String)
}

pub opaque type Server { Server(subject: Subject(Message)) }

pub fn start(limit: Int) -> Result(Server, actor.StartError) {
  actor.new(new(limit))
  |> actor.on_message(handle)
  |> actor.start
  |> result.map(fn(started) { Server(subject: started.data) })
}

pub fn supervised(limit: Int) -> supervision.ChildSpecification(Server) {
  supervision.worker(fn() { start(limit) })
}

fn handle(state: State, msg: Message) -> actor.Next(State, Message) {
  case msg {
    Check(key, reply_to) -> {
      let #(next, ok) = incr(state, key)
      process.send(reply_to, ok)
      actor.continue(next)
    }
    Reset(key) ->
      actor.continue(State(..state, hits: dict.delete(state.hits, key)))
  }
}

// sync call pattern: the caller creates a reply subject and passes it in
pub fn check(s: Server, key: String) -> Bool {
  let reply = process.new_subject()
  process.send(s.subject, Check(key, reply))
  process.receive(reply, 1000) |> result.unwrap(False)
}

pub fn reset(s: Server, key: String) -> Nil {
  process.send(s.subject, Reset(key))
}
3 種類のスーパービジョンツリー

gleam_otp では、ユースケースに応じてスーパーバイザーを 3 つのモジュールに分けています。

種類モジュール使用する場面
staticgleam/otp/static_supervisor子プロセスが起動時に固定されている場合(DB プール、事前設定済みのアクターなど)。再起動のみを処理します。
factorygleam/otp/supervisor_factoryID を指定して、同じ仕様のプロセスを動的に起動する場合(ユーザーごとのセッションなど)。start_child(name, arg)
dynamicgleam/otp/supervisor_dynamic任意の仕様を動的に追加・削除する場合(ジョブワーカープールなど)。start_child(name, spec) / terminate_child

API の正確な名前はバージョンによって異なります。毎回、公式の gleam_otp hex ドキュメントを確認してください。以下のテンプレートは v0.16+ を想定しています。

// src/my_app/app.gleam
import gleam/otp/static_supervisor as sup
import my_app/db_pool
import my_app/rate_limiter

pub fn start() -> Result(Nil, actor.StartError) {
  sup.new(sup.OneForOne)
  |> sup.restart_tolerance(intensity: 3, period: 60)  // allow up to 3 restarts in 60s
  |> sup.add(db_pool.supervised())                    // static child
  |> sup.add(rate_limiter.supervised(limit: 100))     // static child
  |> sup.start
  |> result.replace(Nil)
}

再起動戦略の選び方:

  • Permanent:終了時に常に再起動します(DB プールや認証サーバーなどのインフラストラクチャ)。
  • Transient:正常終了は許容し、異常終了時のみ再起動します(ジョブワーカー)。
  • Temporary:再起動しません(単発の処理)。

period(デフォルトは 5 秒)の間に restart_intensity(デフォルトは 3)を超えると、スーパーバイザー自体がクラッシュし、上位へ伝播します。

ガイドライン
  • Server は不透明に保ちます(呼び出し元が Subject を直接操作できないようにします)。
  • HTTP から状態を直接操作しないでください(必ずアクターを経由します)。
  • スーパービジョンツリーは app.gleam にまとめます。
  • 再起動戦略には OneForOne を使い、最上位では static_supervisor を使用します。
  • テストを容易にするため、名前付きアクターを使います。
  • タスクに適した種類のスーパーバイザーを使用します:static(固定)、factory(同じ仕様を動的に起動)、dynamic(任意の仕様を動的に起動)。

JSON コーデック(gleam/dynamic/decode + gleam/json)

Gleam の JSON API はバージョン間で変わる傾向があります(gleam_json v2+)。現在の慣用的な使い方は次のとおりです。

デコード
import gleam/dynamic/decode
import gleam/json

pub type NewTodo { NewTodo(title: String, priority: Int) }

pub fn new_todo_decoder() -> decode.Decoder(NewTodo) {
  use title <- decode.field("title", decode.string)
  use priority <- decode.optional_field("priority", 0, decode.int)
  decode.success(NewTodo(title:, priority:))
}

// Inside a Wisp handler
case decode.run(body, new_todo_decoder()) {
  Ok(nt) -> // ... use nt
  Error(errors) -> wisp.bad_request("invalid json: " <> string.inspect(errors))
}

decode.field は必須フィールドに使います。decode.optional_field(key, default, decoder) は省略可能なフィールドに使います。ネストしたオブジェクトには decode.field("user", user_decoder()) を使います。

エンコード
pub fn encode_todo(t: Todo) -> json.Json {
  json.object([
    #("id", json.int(t.id)),
    #("title", json.string(t.title)),
    #("done", json.bool(t.done)),
  ])
}

// Return it from Wisp
wisp.json_response(json.to_string_tree(encode_todo(t)), 201)

リストの場合:json.array(items, of: encode_todo)。

LSP コードアクション:gleam-language-server v1.2+ には、「Generate JSON encoder/decoder」機能が含まれています。Todo 型にカーソルを置いてコードアクションを実行すると、encode_todo / todo_decoder を自動生成できます。手動で記述するよりも正確です。

justfile テンプレート

再利用するには、まず assets/justfile をプロジェクトのルートにコピーしてから、必要に応じて調整します。

set shell := ["bash", "-cu"]

default:
  @just --list

deps:
  gleam deps download

format:
  gleam format

format-check:
  gleam format --check .

typecheck:
  gleam check

build:
  gleam build --warnings-as-errors

test:
  gleam test

run:
  gleam run

docs:
  gleam docs build

check:
  gleam format --check .
  gleam check
  gleam build --warnings-as-errors
  gleam test

ci: deps check

clean:
  gleam clean

bench *args:
  gleam run -m bench/main -- {{args}}

方針:

  • シェルには bash を使います。
  • CI のエントリーポイントは just ci です。
  • 専用の lint コマンドは作成せず、format-check + build --warnings-as-errors で対応します。

必要に応じて、以下を追加してください。

  • exports: gleam run -m cleam
  • snapshot-review: gleam run -m birdie
  • test-watch: gleam test -- --glacier
  • bench-http: k6 run bench/http.js
  • serve-dev: gleam run -m olive

CI ワークフローテンプレート

GitHub Actions を使う場合は、まず assets/github-actions/ci.yml を .github/workflows/ci.yml にコピーし、その上にプロジェクト固有のステップのみ追加します。

原則:

  • CI とローカルの両方で、just ci を唯一のエントリーポイントにします。
  • ワークフロー側で Erlang / Gleam のバージョンを固定します。
  • @external、NIF、Wasm、または Elixir の依存関係がある場合に限り、追加のツールチェーンを導入します。
  • ワークフローにシェルスクリプトを詰め込まず、ロジックは justfile に移します。

テスト

まず純粋関数を固め、次にアクターをテストし、最後に HTTP をテストします。

test/my_app_test.gleam

import gleam/http
import gleeunit
import my_app/app
import my_app/web
import wisp/simulate

pub fn main() -> Nil {
  gleeunit.main()
}

pub fn healthz_test() {
  let assert Ok(app_state) = app.start()
  let response = web.app(app_state)(simulate.request(http.Get, "/healthz"))

  assert response.status == 200
  assert simulate.read_body(response) == "ok\n"
}

テスト方針:

  • 純粋モジュールの状態遷移を直接テストします。
  • アクターモジュールでは、公開 API のみをテストします。
  • HTTP テストには wisp/simulate を優先します。
  • 外部 API にはフェイクサービスを注入します。
  • 不安定なスリープの代わりに、ポーリング/再試行ヘルパーを使用します。
  • スナップショットが適している出力には birdie を使用します。
  • プロパティベーステストが適している純粋なロジックには qcheck を使用します。
  • ローカルでの反復を高速化したい場合は glacier を使用します。

基本的な birdie のワークフロー:

  1. gleam test
  2. 失敗したスナップショット/新しいスナップショットを確認します。
  3. gleam run -m birdie
  4. スナップショットを確認してコミットします。

基本的な qcheck のワークフロー:

  1. 純粋関数のプロパティを 1 つ選びます。
  2. qcheck.given(...) を通じてジェネレーターを渡します。
  3. 反例が見つかったら、縮小された結果を使ってユニットテストを作成します。
  4. 長時間実行されるテストや並列実行には、qcheck_gleeunit_utils を使用します。

qcheck_gleeunit_utils に関する注意事項:

  • Erlang ターゲット専用です。
  • すべてのテストをまとめて並列化するには、run.run_gleeunit を使用します。
  • 1 つの長時間テストのみをラップするには、test_spec.make を使用します。
  • カスタムタイムアウトを設定するには、test_spec.make_with_timeout を使用します。
  • テストグループを明示するには、test_spec.run_in_parallel/run_in_order を使用します。

テスト構成

機能が増えてきたら、責務ごとにテストファイルを分割します。

  • domain_test.gleam:純粋なドメインロジック
  • runtime_test.gleam/app_test.gleam:スーパービジョンとアクターの統合
  • web_app_test.gleam:HTTP コントラクト
  • agent_test.gleam:外部 API のオーケストレーション
  • workspace_session_server_test.gleam:サーバー/状態ヘルパー
  • workspace_bit_runtime_test.gleam:FFI/git/wasm の統合
  • workspace_session_http_test.gleam:セッション API のコントラクト

サポートモジュールも分割します。

  • *_app_test_support.gleam:アプリの起動、ハンドラーの構築、ランタイムヘルパー
  • *_workspace_test_support.gleam:フィクスチャディレクトリ、ワークスペースヘルパー

すべてを 1 つの巨大なテストファイルに詰め込まないでください。リファクタリングの進行に合わせて、テストも分割します。

リントと品質

Gleam では、独立した公式リントツールよりも、フォーマッターとコンパイラーの警告を厳格に適用するほうが適しています。

最低限、次を実行します。

  • gleam format --check .
  • gleam check
  • gleam build --warnings-as-errors
  • gleam test

補助的な確認:

  • 非推奨 API を追跡します:gleam fix
  • ドキュメントを確認します:gleam docs build
  • 未使用のエクスポートを検出します:gleam run -m cleam

パフォーマンス測定

3 つの段階で測定します。

  1. 純粋関数のマイクロベンチマーク。
  2. アクター/ワークスペースの統合ベンチマーク。
  3. HTTP エンドポイントに対する負荷テスト。

原則:

  • ベンチマーク対象では、純粋関数と I/O を分けます。
  • ウォームアップと測定を分けます。
  • デバッグログを無効にします。
  • ベンチマークの前に gleam clean を実行し、依存関係を事前にダウンロードします。
  • マイクロベンチマークの数値を HTTP 負荷テストの数値と同等に比較しないでください。
  • FFI/Wasm/Ports では、コールドスタートとホットパスを分けて測定します。

ツール:

  • マイクロベンチマーク:bench/ モジュールを作成し、gleam run -m bench/main で実行します。
  • ライブラリを使いたい場合は、glychee を使用します。
  • HTTP 負荷テスト:k6 または wrk。
  • BEAM のホットスポット:erl/gleam shell から、:eprof、:fprof、:erlang.statistics(:reductions) を使用します。

最小限の HTTP 測定例:

just run
k6 run bench/http.js

まず assets/bench/http.js を bench/http.js にコピーします。

FFI とネイティブ統合

@external を使用する場合は、次のルールを守ります。

  • アダプターモジュールを 1 つだけ作成します。
  • pub opaque type Handle のみを公開します。
  • Erlang/Elixir の型や PID を過剰に漏らさないでください。
  • パス、ETS、NIF、Port、Wasm ランタイムの詳細は、内部モジュール内に留めます。
  • 正常系テストと失敗系テストの両方を作成します。

コンパイラーは @external の実装を検証しないため、Gleam 側のインターフェースを最小限にします。

追加ツール

よく使う外部ツールは、次のように考えてください。

  • gleeunit:デフォルトのユニットテストランナーです。まずはこちらから始めてください。
  • birdie:スナップショットテスト用です。HTTP レスポンスや生成テキストの固定に適しています。
  • qcheck + qcheck_gleeunit_utils:プロパティベーステスト用です。純粋なドメインロジックに適しています。
  • glacier:対話的/インクリメンタルなテストループです。gleeunit の代わりにそのまま使えます。
  • cleam:未使用のエクスポートを検出します。公開 API の整理に適しています。
  • glychee:マイクロベンチマーク用です。純粋関数や小規模な統合の比較に適しています。
  • k6:HTTP/WebSocket の負荷テスト用です。外部 CLI であり、Gleam パッケージではありません。
  • logging:Erlang のロガーの設定用です。運用重視のプロジェクトに追加するとよいでしょう。
  • olive:ライブリロード対応の開発用プロキシです。Wisp/Mist の開発体験を改善したい場合にのみ検討してください。

選定:

  • デフォルト:gleam format/check/build/test。
  • 最初に検討する価値のある追加ツール:birdie、qcheck、cleam、glychee。
  • glacier と olive:ローカルの開発体験を改善したい場合に使用します。
  • logging:長時間稼働するサービスや本番アプリ向けです。

ドキュメント

README では、最低限次の内容を説明してください。

  • 実装されている内容と、明示的に対象外としている内容。
  • 主要なモジュールのプレフィックスと、それぞれの役割。
  • 起動方法、just ci、主要なエンドポイント。
  • テストファイルの読み方。

中規模以上のプロジェクトでは、docs/ ディレクトリを追加し、読者が内容を順に理解できるよう案内してください。

  • docs/reference-ja.md:日本語の実装ガイド。
  • docs/reference-en.md:英語の実装ガイド。

README の冒頭で、このプロジェクトが「入門向け」か「実用的なリファレンス実装」かを明確に示してください。

リファレンスプロジェクトの位置付け

Gleam プロジェクトをリファレンス実装として紹介する場合は、位置付けを曖昧にしないでください。

  • 初心者向けサンプル:OTP を最小限に抑えた小規模な API。
  • 中規模のリファレンス:wisp + mist + gleam_otp + just + CI + docs。
  • 高度な統合:FFI、Wasm、外部 LLM、ワークスペース/セッションのオーケストレーション。

高度な内容を含める場合は、各要素が「Gleam の標準的なスタイル」なのか、「このプロジェクト固有の複雑さ」なのかを分けて説明してください。

完了チェックリスト

変更を「完了」と判断するためのよいチェックポイント:

  • 公開/内部の境界が整理されている。
  • just ci が成功する。
  • テストファイルが機能ごとに分割されている。
  • サポートモジュールが役割ごとに分割されている。
  • README で実装状況とテスト構成を説明している。
  • 必要に応じて、docs/ にリファレンスガイドがある。

レビューチェックリスト

  • 公開コンストラクターは本当に必要ですか?
  • pub type を pub opaque type にできますか?
  • internal_modules に隠すべきモジュールはありますか?
  • Web レイヤーがデコード/呼び出し/エンコード以上のことをしていませんか?
  • 純粋なロジックとアクターの状態が混在していませんか?
  • ローカルと CI で共通のエントリーポイントとして just ci を使っていますか?
  • gleam build --warnings-as-errors が成功していますか?
  • 絶対パスやマシン固有の設定を埋め込んでいませんか?
  • 外部/FFI 境界が広すぎませんか?

参考資料

コメント