規約、パターン、アンチパターン
原文: Conventions, patterns, and anti-patterns ── © Louis Pilfold and the Gleam contributors, Apache-2.0。日本語訳 © 2026 Canonical。
この文書では、Gleam のコードにおける規約、パターン、アンチパターンをまとめています。規約とアンチパターンは常に守るべき決まりですが、パターンは、自分のコードに役立つとプログラマが判断したときに適用するものです。
規約
Gleam では、変数、定数、関数には snake_case を、型とバリアントには PascalCase を使うことが強制されます。
関数と定数の非修飾インポートを避ける
他のモジュールで定義された関数や定数には、常に修飾つきの構文を使います。
// 良い例
import gleam/list
import gleam/string
pub fn reverse(input: String) -> String {
input
|> string.to_graphemes
|> list.reverse
|> string.concat
}
// 悪い例
import gleam/list.{reverse}
import gleam/string.{to_graphemes, concat}
pub fn reverse(input: String) -> String {
input
|> to_graphemes
|> reverse
|> concat
}型とレコードコンストラクタについては、コードが読みにくくならないと判断できる場合に限り、非修飾の構文を使ってもかまいません。
モジュールの関数にはすべて型注釈を付ける
モジュールの関数にはすべて、引数と返り値の型注釈を付けるべきです。
// 良い例
fn calculate_total(amounts: List(Int), service_charge: Int) -> Int {
int.sum(amounts) * service_charge
}
// 悪い例
fn calculate_total(amounts, service_charge) {
int.sum(amounts) * service_charge
}
// 悪い例: 返り値の注釈が無い
fn calculate_total(amounts: List(Int), service_charge: Int) {
int.sum(amounts) * service_charge
}失敗しうる関数には result を使う
Gleam では、成功する可能性も失敗する可能性もある関数は、すべて result を返すことができます。
言語によっては、失敗しうる関数に result 型と option 型の両方を使いますが、Gleam ではそうしません。常に result を使うことでコードの一貫性が保たれ、result と option を相互に変換するための定型コードも不要になります。失敗時に返す追加情報がない場合は、result のエラー型を Nil にできます。
失敗しうる関数で panic を使うことはありません。ライブラリの中では特にそうです。panic が適切な場面があるとすれば、失敗しうる関数が返した result を処理する、アプリケーションコードの最上位です。
// 良い例
pub fn first(list: List(a)) -> Result(a, Nil) {
case list {
[item, ..] -> Ok(item)
_ -> Error(Nil)
}
}
// 悪い例: option を返している
pub fn first(list: List(a)) -> option.Option(a) {
case list {
[item, ..] -> option.Some(item)
_ -> option.None
}
}
// 悪い例: 失敗時に panic している
pub fn first(list: List(a)) -> a {
case list {
[item, ..] -> item
_ -> panic as "cannot get first of empty list"
}
}モジュール名は単数形にする
モジュール名は複数形ではなく単数形にします。
// 良い例
import app/user
// 悪い例
import app/usersこれは最後のセグメントだけでなく、すべてのセグメントに当てはまります。
// 良い例
import app/payment/invoice
// 悪い例
import app/payments/invoice頭字語は 1 語として扱う
頭字語(acronym)は、常に 1 つの単語として扱います。
// 良い例
let json: Json = build_json()
// 悪い例
let j_s_o_n: JSON = build_j_s_o_n()この規約を無視して JSON という名前を使いたくなるかもしれませんが、そうすると Gleam から生成される BEAM のコード内で j_s_o_n という名前になってしまいます。
変換関数の命名の慣例
ある型から別の型へ変換する関数の名前は、x_to_y という慣例に従ってください。
// 良い例
pub fn json_to_string(data: Json) -> String
// 悪い例
pub fn json_into_string(data: Json) -> String
pub fn json_as_string(data: Json) -> String
pub fn string_of_json(data: Json) -> Stringモジュール名と型名が同じ場合は、関数名の先頭で型名を繰り返さないでください。
// src/my_app/identifier.gleam にて
// 良い例
pub fn to_string(id: Identifier) -> String
// 悪い例
pub fn identifier_to_string(id: Identifier) -> String関数はモジュール修飾子を付けて呼び出されるため、入力値の型はモジュール名を見れば明らかです。
import my_app/identifier.{type Identifier}
pub fn run(id: Identifier) -> String {
identifier.to_string(id)
}変換で使われるエンコーディング、フォーマット、変種(variant)に特定の名前がある場合は、それを関数名に含めてください。
// 良い例
pub fn date_to_rfc3339(date: Date) -> String
// 悪い例
pub fn date_to_string(date: Date) -> String変換の操作に、より説明的な名前があるなら、そちらを使います。
// 良い例
pub fn round(data: Float) -> Int
// 悪い例
pub fn float_to_int(data: Float) -> Int失敗しうる関数の命名の慣例
result を返す関数には、対象のドメインと操作内容に合ったふさわしい名前を付けるべきです。
// 良い例
pub fn parse_json(input: String) -> Result(Json, ParseError)
pub fn enqueue(job: BackgroundJob) -> Result(Nil, EnqueueError)その関数が、既存の関数を result に対応させた特別版であり、エラー時に早期リターンするような場合は、より適切なドメイン固有の名前がないかぎり try_ という接頭辞を使えます。デザインパターンや抽象概念に基づく名前は避けるべきです。
pub fn map(list: List(a), f: fn(a) -> b) -> List(b)
// 良い例
pub fn try_map(
list: List(a),
f: fn(a) -> Result(b, e),
) -> Result(List(b), e)
// 悪い例
pub fn monadic_bind(
list: List(a),
f: fn(a) -> Result(b, e),
) -> Result(List(b), e)コアライブラリを使う
Gleam のコアチームは、Gleam のライブラリやアプリケーションの共通の土台となるパッケージをいくつか保守しています。
この共通の土台があるおかげで、関連する Gleam のパッケージ同士が連携しやすくなり、よくある問題もパッケージの設計によって回避できます。
これらのパッケージが提供する機能は、自分で作り直さないでください。たとえば、gleam_time の Timestamp を使わずに独自の時刻型を作るようなことは避けてください。
開発ツールの設定は gleam.toml に置く
gleam コマンドラインプログラムは Gleam 開発に必要な機能の大部分を提供しますが、セキュリティスキャナやライセンスチェッカーなど、追加のツールが必要になることもあります。
そうしたツールをファイルで設定する場合、設定ファイルには gleam.toml を使い、設定値は tools.$TOOL_NAME というキーの下に配置します。
name = "thingy"
version = "1.0.0"
[dependencies]
gleam_stdlib = "<= 1.0.0 and < 2.0.0"
[tools.lustre.dev]
host = "0.0.0.0"
[tools.lustre.build]
minify = true
outdir = "../server/priv/static"my-tool.toml や config/my-tool.yaml のような専用の設定ファイルは使わないでください。
動的な設定は、環境変数から読むか、コマンドライン引数として渡せます。
正しいソースコードのディレクトリを使う
Gleam のビルドツールは、ソースコード用に src、dev、test という 3 つのディレクトリを用意しており、それぞれ目的が異なります。
src は、アプリケーションやライブラリ本体のコードを置く場所です。ここからは dependencies や src/ のモジュールをインポートできますが、dev_dependencies、dev/、test/ のモジュールはインポートできません。
test は、自動化された単体テストや結合テストなど、パッケージをテストするためのコードを置く場所です。ここからは、すべての依存モジュールやディレクトリのモジュールをインポートできます。
dev は、コード生成器や補助スクリプトなど、開発中に使うその他のコードを置く場所です。ここからも、すべての依存モジュールやディレクトリのモジュールをインポートできます。
パターン
説明的なエラーを設計する
エラーの型を作るときは、どんなエラーなのかをビジネスドメインの言葉で説明できるようにバリアントを設計してください。デバッグを容易にしたり、分かりやすいエラーメッセージを組み立てたりできるように、各バリアントにはエラーの実例に関する追加情報を持たせるべきです。
エラーがより低い層のエラーに起因する場合 ── たとえば、ファイルの読み取りに失敗してアプリケーションのデータを読み込めなかった場合など ── は、その低い層のエラーを、高い層のエラーのフィールドに含めることができます。
// 良い例
pub type NoteBookError {
NoteAlreadyExists(path: String)
NoteCouldNotBeCreated(path: String, reason: simplifile.FileError)
NoteCouldNotBeRead(path: String, reason: simplifile.FileError)
NoteInvalidFrontmatter(path: String, reason: tom.ParseError)
}
// 悪い例: 情報が足りない
pub type NotesError {
NoteAlreadyExists
NoteCouldNotBeCreated
NoteCouldNotBeRead
NoteInvalidFrontmatter
}
// 悪い例: ビジネスドメインではなく依存を中心に設計している
pub type NotesError {
FileError(path: String, reason: simplifile.FileError)
TomlError(path: String, reason: tom.ParseError)
}コメントは惜しまず書く
コメントは、コードの理解を助ける非常に効果的な手段です。複数のプログラマが参加するプロジェクトや、長期にわたって読まれたり編集されたりするプロジェクトでは、特に価値があります。
コメントは、コードが何をするかも、なぜそうするのかも説明できます。多くの場合、読み手はコメントの助けが無くても何をするかは分かるでしょうが、不慣れな読み手にとってはそうとは限りませんし、後からそのコードにバグがあると判明した場合 ── つまり、実際の挙動と書き手が意図した挙動が一致していない場合 ── もそうとは限りません。
pub fn classify_file_content(content: String) -> FileOrigin {
let likely_generated =
// 新しい版の squirrel では、これは常にファイルの先頭にあるので、
// ファイルが生成されたものかどうかを判断するには、このコメントを
// 調べるだけで十分なはず……
string.contains(
content,
"> 🐿️ This module was generated automatically using",
)
// ……ところが古い版では、そのモジュールコメントが存在しない! そこで
// squirrel が生成した関数があるかどうかを調べる必要がある。
|| string.contains(
content,
"> 🐿️ This function was generated automatically using",
)
case likely_generated {
True -> LikelyGenerated
False -> NotGenerated
}
}コメントがあるからといって、コード自体を分かりにくく書いてよいわけではありません。逆に、コードがよく書けているからといって、コメントが無価値になるわけでもありません。
不正な状態を作れなくする
Gleam の型システムやカスタム型を使うと、対象のドメインをコード内で正確にモデル化できます。ビジネスルールをしっかりと型定義に落とし込めば、不正なデータを作ること自体が不可能になります。これにより多くのバグが排除され、型定義そのものがビジネスロジックのドキュメントとして機能します。
たとえばウェブサイトで、訪問者がログイン済みのユーザーかゲストのどちらかであるとします。ログイン済みのユーザーは全員、メールアドレスとデータベース上の id を持っています。これは次のように表現できます。
pub type Visitor {
Visitor(id: Option(Int), email: Option(String))
}訪問者がログイン済みなら 2 つの省略可能なフィールドは両方とも設定され、ゲストなら両方とも未設定になります。
let logged_in_user = Visitor(id: Some(123), email: Some("hi@example.com"))
let guest = Visitor(id: None, email: None)しかし、このデータ構造では、メールアドレスだけ、あるいは id だけで構築できてしまいます。どちらの状態も、ビジネスルールに照らし合わせれば不正です!
let invalid = Visitor(id: Some(123), email: None)より良い設計では、id とメールアドレスが必ず同時に存在することを保証し、不正な状態を作れないようにします。
pub type Visitor {
LoggedInUser(id: Int, email: String)
Guest
}このパターンについては Richard Feldman による優れた講演があり、YouTube で見られます。
bool をカスタム型に置き換える
bool 型は 2 つの状態を持つデータを表すのに便利ですが、次のような欠点もあります。
Bool、True、Falseは文脈がないと意味を持たないため、値が何を表しているのか誤解を招きやすくなります。- bool 型は無関係な様々なデータで使われるため、ある bool 値と別の bool 値を取り違えても、それを防ぐための型エラーが出ません。
- 将来 3 つ目の状態が必要になった場合、bool は使えなくなり、大規模なリファクタリングが必要になります。2 つ目の bool を追加したくなるかもしれませんが、それだと状態が 4 つになり、3 つ目を足せば 8 つになってしまいます。bool の組み合わせは非常に分かりにくくなりがちです。
pub type SchoolPerson {
SchoolPerson(name: String, is_student: Bool)
}代わりに、わかりやすいカスタム型を定義して使うことを検討してください。そうすれば、これらの問題を回避できます。
pub type SchoolPerson {
SchoolPerson(name: String, role: Role)
}
pub type Role {
Student
Teacher
}sans-io パターン
sans-io パターンは、API クライアントや SDK などのパッケージを、特定の HTTP クライアントに依存しないように設計する手法です。HTTP リクエストの送信などの責務は、ライブラリの利用者に委ねられます。このパターンでは、利用者が HTTP 送信を完全に制御できるため、どのターゲット、構造、フレームワークでも利用でき、レート制限やリトライなど必要な機能を自由に追加できます。
このパターンを実装するには、API の操作ごとに 2 つの関数が対になるようにコードを構成します。1 つは HTTP リクエストを構築する関数、もう 1 つは HTTP レスポンスを受け取って結果のデータを返す関数です。
import gleam/http/request.{type Request}
import gleam/http/response.{type Response}
/// create-user エンドポイントへのリクエストを構築する。
pub fn create_user_request(name: String) -> Request(String) {
request.new()
|> request.set_method(Post)
|> request.set_host("example.com")
|> request.set_body(json.to_string(json.object([#("name", name)])))
|> request.prepend_header("accept", "application/json")
|> request.prepend_header("content-type", "application/json")
}
/// create-user エンドポイントからのレスポンスを解析する。
pub fn create_user_response(response: Response(String)) -> Result(User, ApiError) {
case response.status {
201 -> Ok(User(name: response.body))
409 -> Error(UserNameAlreadyInUse)
429 -> Error(RateLimitWasHit)
code -> Error(GotUnexpectedResponse(code, response.body))
}
}これは、HTTP を送信する関数を引数として受け取る設計とは異なります。関数を受け取る設計では、その型に適合する HTTP クライアントしか使えず、制約が非常に強くなります。とくに、一方は promise 型を使い、もう一方は使わないという違いがあるため、ライブラリを Erlang と JavaScript の両方のターゲットで使えなくなってしまいます。
ビルダーパターン
ビルダーパターンは、省略可能なフィールドを複数持つレコードを作るための柔軟な方法で、設定によく使われます。
// 使い方
button.new(text: "Continue")
|> button.colour("green")
|> button.large
|> button.to_html必須のフィールドは、ビルダーのパイプラインを開始する関数が引数として受け取り、残りのフィールドには既定値が設定されます。
pub type Button {
Button(text: String, colour: String, classes: Set(String))
}
pub fn new(text text: String) -> Button {
Button(text:, colour: "pink", classes: set.new())
}この例の Button 型は opaque ではないため、レコードを直接構築して操作できます。検証やデータの整合性を保つため、型を opaque にして、ビルダーの関数を強制的に使わせたい場合もあるでしょう。
ビルダーの関数は、ビルダーの値を引数として受け取り、1 つ以上のフィールドを変更して新しい値を返します。
pub fn colour(button: Button, value: String) -> Button {
Button(..button, colour: value)
}ビルダーの関数は、引数の値を単にフィールドに設定するだけでなく、データを構築するために任意のロジックを実行できます。よくある用途に向けて便利な関数を用意しておくと役立ちますし、完全な制御を求める利用者には、ビルダーのレコードを直接構築してもらうことも可能です。
pub fn large(button: Button) -> Button {
let classes = button.classes |> set.delete("small") |> set.insert("large")
Button(..button, classes:)
}アンチパターン
略語
名前を短くすればキーストロークを数回節約できますが、コードの可読性や理解は大きく妨げられます。略語は曖昧であり、読み手は何の略かを推測しなければならないため、しばしば誤解を招きます。背景や文化の異なる人たちと働いている場合は、とくに起こりがちです。
名前は常に省略せずに書いてください。
// 悪い例
let cap = 5
let off = 0
let cnt = proc_dat(ss)
// 良い例
let capacity = 5
let offset = 0
let continuation = process_data(session)モジュールの断片化
早まってモジュールを小さな複数のモジュールに分割しないでください。また、大きなモジュールを問題視しないでください。そうではなく、ビジネスドメインと、コードの利用者にとって最良な API を作ることに集中しましょう。
多くのモジュールに分割された API は、よく設計された 1 つのモジュールに比べて理解しにくく、利用する際にも多くの定型コードが必要になります。また、内部の実装を他のモジュールから使うために公開せざるをえなくなり、詳細を隠すのも難しくなります。
モジュールが複数あると、モジュール間で機能が重複する関数や型も作られやすくなります。API は大きくなるほど理解しづらく、扱いにくくなります。最良の API は小さく、焦点が絞られているものです。
インポートの循環に悩まされていたり、簡単な作業をするだけで複数のモジュールをインポートしなければならなかったりする場合は、本来 1 つであるべきモジュールを分割してしまった兆候かもしれません。
この話題については、Evan Czaplicki の講演「The life of a file」に豊富な情報があります。
// 悪い例
import my_library/client
import my_library/config
import my_library/decode
import my_library/error
import my_library/parser
import my_library/types
// 良い例
import my_libraryこのアンチパターンは、AI が生成したコードでとくによく見られます。コーディングに AI を使っている場合は、この決まりに一層注意してください。
ライブラリの中で panic する
ライブラリ内で panic させてはいけません。したがって、panic や let assert を使うべきではありません。
result を返す代わりに panic させると、ライブラリの利用者から制御を奪い、エラーを処理できなくしてしまいます。ライブラリは自身が使われる文脈を知らないため、作者が panic してよいかを判断することは不可能です。そのため、決して panic させてはいけません。
この決まりの唯一の例外は、BEAM のアプリケーションフレームワークである OTP そのものを扱うライブラリです。OTP には監視ツリーによる非局所的な処理があるため、適切に設計された監視ツリーを前提とすれば、panic させるのが適切な状況もありえます。どんなときにこれが良い選択肢となるのかを見極めるには、ライブラリの作者が OTP のシステムを深く理解している必要があります。
グローバル名前空間の汚染
Gleam のモジュールの名前空間はグローバルです。これは BEAM エコシステムから受け継いだ性質です。2 つのパッケージが同じ名前のモジュールを定義していると、両方を依存に加えたプロジェクトはコンパイルに失敗します。
この問題を避けるため、パッケージは一意な名前のディレクトリ内にモジュールを配置し、自身の名前空間を定義すべきです。このディレクトリ名はパッケージ名と一致させます。たとえば lustre というパッケージでは、モジュールを src/lustre/ に置いています。
# 良い例
src/
├── my_package.gleam
└── my_package/
├── distribution.gleam
└── inventory.gleam
# 悪い例
src/
├── distribution.gleam
├── inventory.gleam
└── my_package.gleam名前空間への侵入
他のパッケージが所有するトップレベルのディレクトリに、自分のモジュールを置くべきではありません。他人の名前空間に侵入すると、モジュールの衝突によるコンパイルエラーを招き、モジュールの出所がわからない紛らわしいコードになってしまいます。
たとえば、lustre パッケージと一緒に使うためのパッケージを作っている場合でも、自分のモジュールを src/lustre/ の中に置いてはいけません。
lustre パッケージの保守者は、自分たちが所有する別の Lustre 関連パッケージで src/lustre/ ディレクトリを再び使うかもしれないためです。
デザインパターンによるグループ分け
コードをモジュールに分割する際は、ビジネスドメインとコードの利用者にとって最良の API は何かを軸にして、常にシステムの境界を設計してください。つまり、デザインパターンや抽象的なコードの構成物を、プロジェクト内の境界の基準として決して使わないということです。
// 悪い例: 種類によるグループ分け
import app/constants
import app/functions
import app/types
import app/utilities
// 悪い例: 圏論によるグループ分け
import app/functors
import app/monads
import app/monoids
import app/semigroups
// 悪い例: デザインパターンによるグループ分け
import app/controllers/user_controller
import app/decorator/user_decorator
import app/model/user_mode
import app/services/user_service
import app/views/user_view
// 良い例: ビジネスドメインによるグループ分け
import app/stock
import app/billingチェックしてから assert する
チェックしてから assert する(check-then-assert)のは手続き型言語でよくあるパターンで、値が望ましい状態かどうかを調べ、その後で望ましい状態である前提で処理を行うというものです。Gleam のような関数型言語ではこれはアンチパターンであり、決して行うべきではありません。
代わりに、パターンマッチや result.try、result.map のような関数を使って、別の状態になりうるデータを扱ってください。これらの手法は型システムを活かし、チェック箇所と使用箇所の食い違いによるミスを防ぐだけでなく、性能面での利点もあります。
// 悪い例: チェックしてから assert する
case result.is_ok(data) {
True -> {
let assert Ok(value) = data
process(value)
}
False -> data
}
// 悪い例: `use` を使って、チェックしてから assert する
use <- bool.guard(when: result.is_error(data), return: data)
let assert Ok(value) = data
process(data)
// 良い例: パターンマッチ
case data {
Ok(value) -> process(value)
Error(e) -> Error(e)
}
// 良い例: コンビネータ
data |> result.try(process)
// 良い例: `use` を使ったコンビネータ
use value <- result.try(value)
process(value)FFI で dynamic を使う
他の言語で書かれたコードを使う場合、Gleam の型システムでは表現できない引数や返り値が出てきます。たとえば、Erlang の関数が引数に int と float の両方を受け取れるといった場合です。
こうした型を表すために、gleam/dynamic モジュールの Dynamic 型を決して使わないでください。
dynamic 型はあらゆる種類のデータを表すため、その関数にどんな値を渡しても正当だということになってしまいます。これは正しくありませんし、ランタイムエラーを引き起こします。代わりに、期待される型を正確に表す新しい型を作成してください。
// 良い例
pub type Buffer
pub fn byte_size(data: Buffer) -> Int
// 悪い例
import gleam/dynamic.{type Dynamic}
pub fn byte_size(data: Dynamic) -> Intすべてのバリアントにマッチさせる
Gleam の case 式による網羅性検査は、データモデルを変更した際に、再びコンパイルが通る前にコード全体が適切に更新されることを保証してくれます。
case 式の最後のパターンが任意の値にマッチするものだと、新しく追加されたバリアントに対するリファクタリングの支援が事実上無効になり、コンパイラがコードの更新を助けられなくなります。結果として、古いロジックが正しくなくなったことによるバグが混入しやすくなるため、可能なかぎり全捕捉(catch-all)のパターンは避けてください。
// 悪い例: 他のバリアントはすべて teacher だと決めつけている
case role {
Student -> handle_student()
_ -> handle_teacher()
}
// 良い例: 黙って不正になることがない
case role {
Student -> handle_student()
Teacher -> handle_teacher()
}圏論の使いすぎ
圏論に基づく複雑な抽象を作るのは避けてください。
Gleam には、こうした抽象を扱いやすくするための機能もなければ、それらがもたらす無視できないランタイムのオーバーヘッドを消し去るためのコンパイラやランタイムの最適化もありません。複雑な抽象はコードの認知的負荷を高めることが多く、単純で、具体的で、親しみやすいという Gleam のプログラミングの流儀に反します。
具体的な問題は、具体的な解法で解いてください。
// 悪い例: 抽象的な書き方
pub fn sum(
data: a,
monoid: Monoid(a),
catamorphism: Catamorphism(a, b),
) -> b {
catamorphism.apply(data, monoid.empty, monoid.append)
}
// 良い例: 具体的な書き方
pub fn total_cost(costs: List(Int)) -> Int {
int.sum(costs)
}<!--
OTP のアンチパターン
状態としてのプロセス
TODO
監視されていないプロセス
TODO
大きなメッセージ
TODO
プロセスでコードを構成する
TODO -->