motoko-general-style-guidelines について
Motoko 開発者向けのスタイルガイドです。ソースコードの読みやすさと一貫性を高めるため、書式、句読点、コメント、命名規則を扱います。
主な機能
- 01演算子の空白と改行
- 02タブを使わない2スペースの字下げ
- 03定義のグループ化とコメント規則
- 04セミコロンと波括弧の使い方
- 05識別子の命名規則
活用シーン
- 01Motoko コードの書式統一
- 02命名や句読点のスタイル確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ research-ag/ motoko-skills --skill motoko-general-style-guidelinesインストール後、Claude Code で/motoko-general-style-guidelinesを入力して読み込まれたか確認します。
詳細
Motoko の一般的なスタイルガイドライン
このガイドについて
Motoko ソースコードの可読性と一貫性を高めるため、このスタイルガイドでは Motoko ソースの書式設定やその他の基本的な規約について提案します。
レイアウト
スペース
-
結合の強い演算子の部分式を視覚的にまとめる場合を除き、算術演算子の前後にスペースを入れます。
let z = - 2*x + 3*y + 4*(x*x + y*y); -
比較演算子、ブール演算子、代入演算子の前後にスペースを入れます。
4 + 5 <= 5 + 4; not (a or b and not c); v := 0; v += 1; -
=の前後にスペースを入れます。var v = 0; let r = { a = 1; b = 2 }; -
同様に、
:の前後にスペースを入れます。var v : Nat = 0; func foo(x : Nat, y : Nat) : Nat { x + y } func bar((x, y) : (Nat, Nat)) : Nat { x + y } let w = 1 ^ 0xff : Nat16;理由:
:は宣言における=に相当します。また、型注釈の左辺には、一般に任意の複雑な式やパターンを指定できます。 -
カンマまたはセミコロンの後にスペースを入れ、その前には入れません。
let tuple = (1, 2, 3); let record = { a = 1; b = 2; c = 3 }; -
単純なバリアントやレコードの場合を除き、波括弧の内側にスペースを入れます。
func f() { 0 }; f({ a = 1; b = 2; c = 3 }); f({a = 1; b = 2}); // こちらも可 type Vec3D = { x : Float; y : Float; y : Float }; type Order = { #less; #equal; #more }; type Order = {#less; #equal; #more}; // こちらも可 type Proc = {h : Nat; w : Nat} -> {#ok; #fail}; -
複数行にまたがる場合は、括弧の内側にスペースを入れます。
foo( firstArgument, ( longTupleComponent, anotherLongExpression, moreLongExpression ), [ 1, 2, 3, 4, 5, 6, ], { field1 = 4; field2 = 5; field3 = 6; } ); -
文のキーワードとそのオペランドの間にスペースを入れます。
if (f()) A else B; for (x in xs.values()) { ... }; switch (compare(x, y)) { case (#less) { A }; case (_) { B }; } assert (x < 100); await (async (0)); -
関数またはバリアントタグと、その引数タプルの間にはスペースを入れません。また、ジェネリック型パラメーターのリストの前後にもスペースを入れません。
type Pair<X> = (X, X); type Id = <X>(X) -> X; let ok = #ok(5); func id<X>(x : X) : X { x }; id<Nat>(5); -
タプル、括弧で囲まれた式(括弧を参照)、または名前付きパラメーターリストとして使われるレコード(型の選択を参照)でない場合は、関数とその引数の間にスペースを入れます。
sin 0.0; g [1, 2, 3]; f{arg1 = 0; arg2 = 0};
理由:特に g[1] は、インデックス参照として誤って解析されます。
-
.、?、!などのアクセス演算子やインデックス用の角括弧の前後にはスペースを入れません。foo(bar).baz[5]().boo; foom(?(bam()! + 1));
改行
-
行の右端に固定のマージンを設定し、定義や式を改行します。多くの人が、80文字を適切な上限と考えています。
let sum = a + b + 2*c + d + e + f + g + h + i + k + l + m + n + o + p; // Or: let sum = a + b + 2*c + d + e + f + g + h + i + k + l + m + n + o + p;理由:この形式には、ほかにも次のような利点があります。
-
ウィンドウの右側にコードが隠れるのを防ぎます。
-
左右に並べた差分で、無作為に改行されるのを防ぎます。たとえば、GitHub などのコードレビュー用ツールで表示する場合です。
-
紙、ウェブサイト、その他の媒体で、より見やすく表示できます。
-
-
演算子の後で改行します。
a + b + c + d + f; foo(bar, baz). boo(); -
関数定義や、引数リストが長い関数呼び出しを改行する場合は、引数ごとに別の行にします。
また、長いパラメーターリストにはレコードを使うことも検討してください。型の選択を参照してください。
func someFunction( arg1 : FirstType, arg2 : SecondType, anotherArg : Nat, yetAnother : [Type], func : Nat -> Nat, ) : Nat { ... }; someFunction( veryLongArgumentExpression, anotherVeryLongArgumentExpression, 3, aNestedFunctionCall( alsoWithLongArguments, andMoreSuchArguments, ), moreLongishArgument, );理由:コードを読む際に引数を見落としにくくなり、式の変更時に改行し直す必要もなくなります。
インデント
-
インデントの各レベルは半角スペース2つにします。
actor A { public func f() { return; } }理由:ネストが深くなることがあります。半角スペース2つにすることで、画面のスペースを無駄にせずに済みます。
-
インデントは、前の行の字句内容に左右されないようにします。
特に、前の行の内側の文字に合わせてインデントを縦にそろえないでください。
let x = someFunction( arg1, arg2, arg3, arg4, arg5); // Do this. let x = someFunction(arg1, arg2, arg3, arg4, arg5); // Or this. let x = someFunction(arg1, arg2, arg3, arg4, arg5); // Or this. let x = someFunction( // Or this. longArg1, longArg2, longArg3, longArg4, longArg5, ); // COUNTER EXAMPLE! let x = someFunction(arg1, arg2, arg3, arg4, arg5); // DO NOT DO THIS!理由:縦にそろえる方法には、次のような多くの問題があります。
-
水平方向のスペースを大量に無駄にします。
-
インデントのレベルが大きく不揃いになり、コードの構造が分かりにくくなります。
-
行を変更すると再調整が必要になり、エディターで自動化した場合でも差分が膨らんで分かりにくくなることがあります。
-
可変幅フォントでは完全に崩れてしまいます。
目安:インデントは必ず2の倍数にします。
-
-
タブは使わないでください。
理由:タブの解釈はツールによって大きく異なり、ウェブページや差分など、多くの状況で失われたり、正しく表示されなかったりします。
グループ化
-
複雑な複数行の定義は、空行で区切ります。1行で書けるものは、連続した行に記述できます。
func foo() { // この関数は、興味深い処理をたくさん行います。 // 定義が複数行にわたります。 } func boo() { // これは、もうひとつの複雑な関数です。 // この定義も複数行にわたります。 } func add(x : Nat, y : Nat) { return x + y }; func mul(x : Nat, y : Nat) { return x * y }; -
定義を論理的なグループに分ける場合は、2つの空行で区切ります。各グループには「セクション見出し」として1行コメントを追加します。
// とても大きなクラス class MuffleMiff(n : Nat) { // アクセサー public func miffMuff() : Text { ... } public func sniffMiff() : Nat { ... } // ミューテーター public func clearMurk() { ... } public func addMuff(name : Text) { ... } // 処理 public func murkMuffle(param : List<Gnobble>) { ... } public func transformSneezler() { ... } // 内部状態 var miffCount = 0; var mabbleMap = Map<Nat, Text>(); }
コメント
-
行コメント(
//…)を使用します。ブロックコメント(/* … */)は、行の途中にコメントを記述する場合や、開発中にコードの一部をコメントアウトする場合にのみ使用します。// 次の関数は、指定したスニブルに対して // 現在のパラブームを実行します。可能であれば、 // 適切なプレクサスを返します。 func paBoom(s : Snibble) : Handle<Plexus> { let puglet = initPugs(s.crick, 0 /* size */, #local); /* 奇妙な処理はまだ行いません... ... ... */ return polyfillNexus(puglet); // 今のところは }理由:行コメントを使うと、個々の行を挿入、削除、入れ替えしやすくなります。
-
1行のコードを説明する短いコメントは、行末に記述し、コードとの間を少なくとも2つのスペースで区切ります。
paBoom(getSnibble())); // 新しいスニブルを作成 -
複数行のコメントは、対象のコードと同じインデントで、そのコードの前に記述します。
func f() { // 前のスニブルを使って、現在のパラブームの // 呼び出しを試みます。成功すれば新しい // プレクサスが得られ、失敗すればエラーになります。 let plexusHandle = paBoom(getSnibble())); } -
別の行に記述するコメントは、文頭を大文字にします。文には適切なピリオドを付けます。
句読点
セミコロン
-
Motokoでは、ブロック内の式やローカル宣言を区切るために、直前の宣言が
}で終わるかどうかにかかわらず、常にセミコロンが必要です。理由:これは、規則がやや場当たり的になりがちな他のC系言語とは異なります。
-
ブロック内の最後の式の後には、ブロック全体が1行で書かれている場合を除き、セミコロンを付けます。
型についても同様です。
// 同じ行にある閉じ } の前には ; は不要 type Vec3D = {x : Float; y : Float; z : Float}; type Result<A> = {#ok : A; #error : Text}; func add(x : Nat, y : Nat) : Nat { return x + y }; // 最後のケースの末尾にも ; を付ける type Address = { first : Text; last : Text; street : Text; nr : Nat; zip : Nat; city : Text; }; type Expr = { #const : Float; #add : (Expr, Expr); #mul : (Expr, Expr); }; func eval(e : Expr) : Float { switch (e) { case (#const(x)) { x }; case (#add(e1, e2)) { eval(e1) + eval(e2) }; case (#mul(e1, e2)) { eval(e1) * eval(e2) }; }; }理由:行末に一貫してセミコロンを付けると、行の追加、削除、入れ替えが簡単になります。
波括弧
-
関数本体、
ifまたはcaseの分岐、ループ本体は、式としてネストされ、その中に式が1つだけ含まれる場合を除き、波括弧で囲みます。func f(x) { f1(x); f2(x) }; let abs = if (v >= 0) v else -v; let val = switch (f()) { case (#ok(x)) x; case (_) 0 }; func succ(x : Nat) : Nat = x + 1; -
複数行にわたる波括弧付きの部分式には、「C スタイル」のレイアウトを使用します。
func f() { return; }; if (cond) { foo(); } else { bar(); }; switch (opt) { case (?x) { f(x); }; case (null) {}; };
丸括弧
-
Motoko では「丸括弧なし」のスタイルがサポートされています。関数のパラメーターリストや文のオペランドなど、多くの箇所で丸括弧を省略できます。タプル、オブジェクト、配列など、式がすでに括弧で囲まれている場合や、単純な定数または識別子の場合に省略できます。たとえば次のとおりです。
type Op = Nat -> Nat; let a2 = Array.map<Nat, Nat>(func x { x + 1 }, a); let y = f x; let z = f {}; let choice = if flag { f1() } else { f2() }; switch opt { case null { tryAgain() }; case _ { proceed() }; }; -
丸括弧なしのスタイルを使いすぎないでください。
特に、文で丸括弧と波括弧を同時に省略しないでください。
// 反例! let choice = if flag x + y else z; // これは避けてください! switch val { case 0 f(); // これは避けてください! case n n + 1; // これも避けてください! };理由:両方を同時に省略すると、グループ化の手がかりが視覚的に減り、コードが読みづらくなります。
-
同様に、関数に型パラメーターもある場合は、関数パラメーターを囲む丸括弧を省略しないでください。
// 反例! foo<Nat> 0; // これは避けてください! -
引数が1つで型パラメーターがない関数型では、引数の型を囲む丸括弧を省略します。
ただし、関数またはクラスに型パラメーターもある場合は、丸括弧を省略しないでください。
type Inv = Nat -> Nat; type Id = <T>(T) -> T; type Get = <X>(C<X>) -> X; // 反例! type Get = <X>C<X> -> X; // これは避けてください!
その他
-
数値の桁を区切るには
_を使用します。10進数では3桁ごと、16進数表記では4桁ごとに区切ります。
let billion = 1_000_000_000; let pi = 3.141_592_653_589_793_12; let mask : Nat32 = 0xff00_ff0f;
命名
スタイル
-
型名(クラスや型パラメーターを含む)、モジュール名、アクター名には
UpperCamelCaseを使用します。 -
定数やバリアントのフィールドを含む、その他すべての名前には
lowerCamelCaseを使用します。module MoreMuff { type FileSize = Nat; type Weekday = {#monday; #tuesday; #wednesday}; type Pair<X> = (X, X); class Container<X, Y>() { ... }; func getValue<Name>(name : Name) : Pair<Name> { ... }; let zero = 0; let pair = getValue<Text>("opus"); var nifty : Nat = 0; object obj { ... }; actor ServerProxy { ... }; };理由:一般的な規則では、型のような「静的」な要素には大文字を、値のような「動的」な要素には小文字を使用します。モジュールとアクターは比較的静的で、型をエクスポートできます。オブジェクトは通常、型をエクスポートせず、主に動的な値として使われます。
-
頭字語は通常の単語として表記します。
type HttpHeader = ...; func getUrl() { ... }; let urlDigest = ...; -
パターン内の変数が意図的に未使用であることを示す場合を除き、アンダースコア
_で始まる識別子名は使用しないでください。let (width, _color, name) = rumpler(); ... // _color はここでは使用されません func foo(x : Nat, _futureFlag : Bool) { ... };理由:型チェッカーは未使用の識別子について警告を出すことがあります。意図を示すために名前の先頭に
_を明示的に付けると、この警告を抑制できます。これは、パターンのワイルドカードにキーワード
_を使用することにも合致します。
命名規則
-
値を返す関数の名前は、その値を表すものにします。
冗長な
get接頭辞は避けます。dict.size(); list.first(); sum(array); -
副作用を伴う操作や複雑な操作を実行する関数の名前は、その操作を表すものにします。
dict.clear(); dict.set(key, value); let result = traverse(graph); -
Boolを返す述語関数の名前には、isまたはhasの接頭辞、あるいはテストするプロパティを表す同様の表現を使います。class Set<X>() { public func size() : Nat { ... }; public func add(x : X) { ... }; public func remove(x : X) { ... }; public func isEmpty() : Bool { ... }; public func contains(x : X) : Bool { ... }; }; -
型
Xへの変換、または型Xからの変換を行う関数は、変換元(または変換先)が関数のメソッドであるオブジェクト、あるいは関数が属するモジュールの主要な型である場合、それぞれtoXおよびfromXという名前にします。 -
クラスまたはオブジェクトでは、ゲッターと区別するため、プライベート変数の末尾に
_を付けます。class Cart(length_ : Nat) { var width_ = 0; public func length() : Nat { return length_ }; public func width() : Nat { return width_ }; }理由:Motoko では関数は第一級の値であるため、関数とその他の値識別子は同じ名前空間を共有します。
先頭に
_が付いた識別子は、未使用の名前を示すため、プライベート状態には使用しないでください(スタイルを参照)。 -
グローバルまたはパブリックな識別子や、スコープの広い識別子には、より長く説明的な名前を使い、スコープの狭いローカルな識別子には短い名前を使います。
特に一貫した命名規則を使用している場合など、特に説明することがないときは、1 文字の識別子を使ってもかまいません。
func map(x : Nat, y : Nat) : Nat { x + y }; func eval(e : Expr) : Nat { let n = switch (e) { case (#neg(e1)) { - eval(e1) }; case (#add(e1, e2)) { eval(e1) + eval(e2) }; case (#mul(e1, e2)) { eval(e1) * eval(e2) }; }; Debug.print(n); return n; };理由:一般に信じられていることとは逆に、ローカルな名前が過度に冗長だと、情報量が増えてノイズが多くなり、読みやすさが低下することがあります。
-
適切な場合は、リストや配列などの要素の集まりを表すために複数形を使います。
短い名前にも同様に適用できます。
func foreach<X>(xs : [X], f : X -> ()) { for (x in xs.values()) { f(x) } }
型
型注釈
-
固定幅の数値型を含む定義には型注釈を付け、オーバーロードされた算術演算子や定数の型を明確にします。
let mask : Nat32 = 0xfc03_ff00; let pivot : Nat32 = (size + 1)/2; let vec : [Int16] = [1, 3, -4, 0];:::note
浮動小数点数の定数を使うと、追加の注釈なしで型を
Floatにできます。同様に、必要に応じて明示的に+符号を付けると、NatではなくInt型の正の値を生成できます。:::
let zero = 1.0; // type Float let offset = +1; // type Int -
同様に、
Nat型やInt型以外の型を持つ算術式には、インラインで型注釈を付けます。if (x & mask == (1 : Nat32)) { ... };:::note
このようなケースで定数に注釈が必要になるのは Motoko の型システムの欠点であり、早期に解決できることを期待しています。
:::
関数の引数には注釈は不要です。通常、その型は関数から推論されるためです。唯一の例外は、引数がジェネリック型で、型引数が省略されている場合です。
func foo(len : Nat32, vec : [Nat16]) { ... }; func bar<X>(x : X) { ... }; foo(3, [0, 1, 2]); bar<Nat16>(0); bar(0 : Nat16); -
型が明らかな場合を除き、可変変数には型注釈を付けます。
var name = "Motoko"; var balance = 0; func f(i : Int) { var j = i; }; var balance : Int = 0; var obj : Class = foo();理由:サブタイピングのため、初期化から型を推論しても、意図した型になるとは限りません。たとえば、注釈がない場合、
balanceの型はNatとなり、整数の代入ができなくなります。 -
クラスのすべての公開フィールドに型注釈を付けます。
class C(init_ : Nat) { public let init : Nat = init_; public var count : Nat = 0; } -
関数の戻り値の型が
()の場合、戻り値の型注釈は省略します。func twiceF() { f(); f() }; // no need to write ": ()" -
関数を引数として渡す場合は、関数の型注釈を省略します。
Array.map<Nat, Nat>(func n {n + 1}, a); -
算術演算子と定数のオーバーロードを解決するため、
Nat型やInt型以外の数値型を含む定義には型注釈を付けます。let mask : Nat32 = 0xfc03_ff00; let offset : Nat32 = size + 1;
型の選択
-
負の値にならない整数値には、
Natを使用します。 -
固定幅の
NatNまたはIntNは、多数の値を格納して容量が重要な場合、ビット操作で数値をビットベクターとして低レベルに扱う必要がある場合、または他の canister などの外部要件で指定された型に合わせる場合にのみ使用します。 -
オプション型、ひいては
nullの使用を増やしすぎないようにします。使用範囲は可能な限り狭くします。
nullになるケースを排除し、可能な限りオプション型以外の型を使用します。 -
2 個または 3 個を超える要素がある場合は、タプルの代わりにレコードの使用を検討します。レコードは、名前付きフィールドを持つシンプルなオブジェクトです。
レコード型は宣言せず、その場で使用することもできます。
func nodeInfo(node : Node) : {parent : Node; left : Node; right : Node} { ... } -
二者択一を表す場合は、
Boolの代わりにバリアントの使用を検討します。バリアント型は宣言せず、その場で使用することもできます。
func capitalization(word : Text) : {#upper; #lower} { ... } -
可能であれば、状態の変更やその他の副作用の発生を主な目的とする関数には、戻り値の型
()を使用します。class Set<X>() { public func add(x : X) { ... }; public func remove(x : X) { ... }; ... }; -
パラメーターが多い場合は、引数としてレコード(データだけを持つオブジェクト)を使用することを検討します。
func process({seed : Float; delta : Float; data : [Record]; config : Config}) : Thing { ... }; process{config = Config(); data = read(); delta = 0.01; seed = 1.0};理由:これにより、名前付きパラメーターを表現できます。呼び出し側では引数を自由に並べ替えられ、誤った順序で渡してしまうことを防げます。
-
コールバック引数を受け取る関数などの高階関数では、関数パラメーターを最後に置きます。
理由:関数呼び出しが読みやすくなります。また、カリー化がないため、関数型言語でよくあるように関数を先頭に置く意味はありません。
-
無効な値を表すために、
-1などのセンチネル値を使用しないでください。代わりにオプション型を使用します。
func lookup(x : key) : ?Nat { ... } -
Motoko では、明示的に指定しない限りデータは不変です。
可変性の型や定義(
var)は、必要な箇所でのみ慎重に使用します。理由:可変データはアクター間で通信したり共有したりできません。また、特に並行処理が関わる場合、エラーが発生しやすく、形式的な推論も非常に困難になります。
機能
文
-
数値範囲またはコンテナーを反復処理する場合は、
whileループではなくforループを使用します。for (i in Iter.range(1, 10)) { ... }; for (x in array.values()) { ... };理由:
forループはエラーが起きにくく、読みやすいためです。 -
適切な場合は、式として
ifまたはswitchを使用します。func abs(i : Int) : Int { if (i < 0) -i else i }; let delta = switch mode { case (#up) +1; case (#dn) -1 }; -
Motoko では、結果が誤って破棄されるのを防ぐため、ブロック内のすべての式の型が
()である必要があります。結果を明示的に破棄するには
ignoreを使用します。必要のない場合はignoreを使用しないでください。ignore async f(); // 計算を開始する -
Motoko では、ブロックは最後の式の値を返すため、関数の末尾の
returnを省略できます。関数が短く、「関数型」のスタイルで書かれている場合、つまり複雑な制御フローや副作用がない場合は、この方法を使用します。
関数にほかの
return文が含まれている場合や、命令型の制御フローを使う場合は、末尾に明示的なreturnを記述します。func add(i : Nat, j : Nat) : Nat { i + j }; func foo(a : Float, b : Float) : Float { let c = a*a + b*b; c + 2*c*c; }; func gcd(i : Nat, j : Nat) : Nat { if (j == 0) i else gcd(j, i % j); }; func gcd2(i : Nat, j : Nat) : Nat { var a = i; var b = j; while (b > 0) { let c = a; a := b; b := c % b; }; return a; };
オブジェクトとレコード
-
オブジェクトを単純なレコード、つまりプライベート状態やメソッドを持たないデータ構造として使用する場合は、簡略オブジェクト構文
{x1 = e1; … ; xN = eN}を使用します。 -
シングルトンオブジェクトを作成する場合は、
objectを使用します。 -
可能な場合は、オブジェクトの用途をレコードに限定します。
理由:メッセージのパラメーターや結果として送信したり、安定変数に格納したりできるのはレコードだけです。また、メソッドを持つオブジェクトは、作成やメモリ上での表現にかかるコストも高くなります。
-
完全なオブジェクトは、状態や振る舞いをカプセル化する手段としてのみ使用します。
クラス
-
同じ形状のオブジェクトを複数作成するには、
classを使用します。 -
クラス名には実装ではなく概念上の機能を付けます。ただし、同じ概念に対する複数の異なる実装を区別する必要がある場合は除きます。たとえば、
OrderedMapとHashMapです)。 -
クラスは、型定義であると同時にオブジェクトを作成するファクトリー関数でもあります。
この両方の役割を意図していない限り、クラスを使用しないでください。それ以外の場合は、通常の型エイリアスを使用するか、オブジェクトを返す関数を使用します。
-
クラスを使いすぎないでください。
適切な場合は、通常の型とその型に対する関数を定義するモジュールを使用します。クラスは状態や振る舞いをカプセル化する手段としてのみ使用します。
理由:メソッドを持つオブジェクトには、個別の関数を備えた単純なレコード型と比べてデメリットがあります(前述)。
-
クラスの値を送信可能(共有可能)にする場合、そのクラスには、レコードなどの共有可能な表現との変換を行う
share/unshareメソッドのペアを用意する必要があります。:::note
不変クラスでは、
unshareを静的関数のようにするほうが自然に思えるかもしれません。しかし、不変クラスであっても、順序関数などのコンストラクター引数に依存することがあります。そのため、Map(compareInt).unshare(x)のようなパターンが適切と思われます。:::
-
現時点では、クラスにメソッドを詰め込みすぎないでください。現在はコストが高いためです。
正準的なメソッドを十分に小さな集合に限定し、その上に実装できる重要度の低いメソッドは、外側のモジュール内の関数として定義します。
-
「静的」クラスやメソッドにはモジュールを使用します。
モジュール
-
moduleを使用して型を含む定義をグループ化し、名前空間を作成します。 -
該当する場合、実装する主な型や関数を提供するクラスにちなんでモジュールに名前を付けます。
-
各モジュールは、単一の主要な概念/型/クラス、または密接に関連する概念/型/クラスのグループに限定します。