gleam-practice について
gleam-practiceは、Gleamプロジェクトの新規作成や保守を支援するスキルです。wisp、mist、gleam_otpを使う構成を含め、構成設計、テスト、監督ツリー、CIの指針を提供します。
主な機能
- 01Gleamプロジェクトのセットアップ
- 02ドメインとWeb層の分離
- 03OTPアクターと監督パターン
- 04単体・スナップショット・プロパティテスト
- 05ローカルとCIの共通ワークフロー
活用シーン
- 01ErlangターゲットのGleamプロジェクト作成
- 02wispとmistを使うWebアプリの構成
- 03純粋なロジックとアクターAPIのテスト
- 04一貫したCIチェックの設定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ mizchi/ skills --skill gleam-practiceインストール後、Claude Code で/gleam-practiceを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
README.mdSKILL-ja.mdassets/justfileREADME.mdassets/bench/http.jsassets/github-actions/ci.ymlassets/test/property_test.gleamqcheck_parallel_test.gleamsnapshot_test.gleamtimeout_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.mdassets/justfileassets/github-actions/ci.ymlassets/bench/http.jsassets/test/snapshot_test.gleamassets/test/property_test.gleamassets/test/qcheck_parallel_test.gleamassets/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 つのモジュールに分けています。
| 種類 | モジュール | 使用する場面 |
|---|---|---|
| static | gleam/otp/static_supervisor | 子プロセスが起動時に固定されている場合(DB プール、事前設定済みのアクターなど)。再起動のみを処理します。 |
| factory | gleam/otp/supervisor_factory | ID を指定して、同じ仕様のプロセスを動的に起動する場合(ユーザーごとのセッションなど)。start_child(name, arg) |
| dynamic | gleam/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 cleamsnapshot-review: gleam run -m birdietest-watch: gleam test -- --glacierbench-http: k6 run bench/http.jsserve-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 のワークフロー:
gleam test- 失敗したスナップショット/新しいスナップショットを確認します。
gleam run -m birdie- スナップショットを確認してコミットします。
基本的な qcheck のワークフロー:
- 純粋関数のプロパティを 1 つ選びます。
qcheck.given(...)を通じてジェネレーターを渡します。- 反例が見つかったら、縮小された結果を使ってユニットテストを作成します。
- 長時間実行されるテストや並列実行には、
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 checkgleam build --warnings-as-errorsgleam test
補助的な確認:
- 非推奨 API を追跡します:
gleam fix - ドキュメントを確認します:
gleam docs build - 未使用のエクスポートを検出します:
gleam run -m cleam
パフォーマンス測定
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 境界が広すぎませんか?
参考資料
- Gleam ドキュメント:https://gleam.run/
- Gleam の
gleam.toml:https://gleam.run/writing-gleam/gleam-toml/ - Gleam の外部関数:https://gleam.run/documentation/externals/
- Gleam CLI:https://gleam.run/reference/cli/
- Wisp ドキュメント:https://hexdocs.pm/wisp/
- Mist ドキュメント:https://hexdocs.pm/mist/
- just のマニュアル:https://just.systems/man/en/