型注釈の位置
Last Updated: 2026-09-28 02:00
結論
melangでは変数宣言にpostfix方式(name: Type)を採用し、変数名を先に読ませる設計にする。一方、関数の引数・戻り値は型推論に頼らず常に明示する。
// 変数宣言: 型注釈は省略できる(推論に任せる)
val age: Int = 20
val age = 20
// 関数シグネチャ: 引数・戻り値は常に明示する
fun greet(name: String): String {
return name
}
理由は次の2点である。
- 変数名が先に来ることで、コードを読むとき「何を扱っているか(名前)」を先に認識でき、型は補足情報として後から確認できる。Genericsなどで型が長くなっても、変数名の位置が動かない。
- 関数シグネチャは呼び出し側に公開される契約であり、実装の詳細から推論させるのではなく、宣言だけを読んで引数・戻り値の型が分かることを優先する。ローカル変数の推論とは異なる基準をあえて適用する。
型注釈の位置という構文レベルの話は、型推論の範囲(どこまで省略できるか)や、Generics・Null許容型の表現方法とは独立した論点であり、それらは別のnoteで扱う。
他言語比較
型注釈(型宣言)の位置は、大きく「型が先か(prefix・前置)」「名前が先か(postfix・後置)」で分かれる。さらに、変数宣言・関数引数・戻り値でその扱いが変わる言語も多く、単純な二分法では捉えきれない。主要な言語がそれぞれの場面でどちらの方式を採用し、どこまで型推論に任せているかを比較する。
| 言語 | 変数宣言 | 関数引数 | 戻り値 | 型推論 | 説明 |
|---|---|---|---|---|---|
| Rust | let name: Type | name: Type | -> Type | 変数のみ強い | ローカル変数は型推論が強いが、関数シグネチャは常に明示が必要。 `let`束縛はコンパイラソースコードを解析し、実行可能な形式(機械語や中間表現)に変換するプログラム。静的型付け言語では、実行前にコンパイラが型の不整合などをチェックする役割も担う。が型推論するため型注釈を省略できるが、関数の引数・戻り値は常に`: Type`/`-> Type`という後置構文で明示しなければならない。「ローカルでは推論に任せ、公開インターフェースでは明示する」という境界線を言語仕様として強制している設計。 |
| MoonBit | let name : Type | name : Type | -> Type | 変数のみ強い | Rust/Swiftと同系統のpostfix構文で、シグネチャは明示が基本。 `name : Type`というpostfix構文で、Rust/Swiftと同じ系統に属する。ローカル変数は型推論に任せられるが、関数シグネチャは明示が基本という設計も共通しており、新興言語がpostfix系の慣習を踏襲していることが分かる。 |
| Swift | let name: Type | name: Type | -> Type | 変数のみ強い | Kotlinと同様のpostfix構文で、引数・戻り値は明示が基本。 `name: Type`というpostfix構文をKotlinと共有する。関数の引数・戻り値は基本的に明示が必要で、ローカル変数の強い型推論とは対照的。クロージャの引数に限り、文脈から型を省略できる場合がある。 |
| Kotlin | val name: Type | name: Type | fun f(): Type | 変数は強い/戻り値は式本体のみ | postfix構文で、戻り値は式本体で定義した場合に限り推論できる。 `name: Type`という一貫したpostfix構文。引数の型注釈は必須で推論できないが、戻り値の型は式本体(`= expr`)で定義した関数に限り推論できるという、本体の書き方によって推論範囲が変わる設計。 |
| Java | Type name | Type name | Type f() | varのみ(ローカル) | 型が変数名より前に来るprefix方式。推論はローカル変数の`var`に限定。 `Type name`という前置構文が基本で、フィールド・引数・戻り値すべてに一貫して適用される。`var`はローカル変数宣言にのみ使え、引数や戻り値の型推論は言語仕様として存在しない。 |
| Scala | val name: Type | name: Type | def f(): Type | 変数は強い/戻り値は省略可(非推奨) | postfix構文。戻り値の型注釈は省略できるが、公開APIでは明示が強く推奨される。 `name: Type`というpostfix構文。引数の型は必須だが、メソッドの戻り値型はコンパイラソースコードを解析し、実行可能な形式(機械語や中間表現)に変換するプログラム。静的型付け言語では、実行前にコンパイラが型の不整合などをチェックする役割も担う。が本体から推論でき、文法上は省略できる。ただし公開APIで省略すると意図しない型が推論されるリスクがあるため、スタイルガイドでは明示が推奨される——「書けること」と「書くべきこと」が分かれる例。 |
| C# | Type name | Type name | Type f() | varのみ(ローカル) | Javaと同様prefix方式。`var`による推論はローカル変数に限定。 `Type name`という前置構文が基本で、Javaと同じ設計思想を継承する。`var`はローカル変数の型推論に使えるが、メソッドの引数・戻り値には使えず、公開シグネチャは常に明示的な型を書くことになる。 |
| C++ | Type name | Type name | Type f() / auto f()->Type | autoのみ(ローカル) | 型が変数名の前に来る伝統的なprefix方式。trailing return typeという後置構文も選べる。 基本はCから受け継いだ`Type name`という前置構文。一方C++11で導入されたtrailing return type構文(`auto f() -> Type`)は、テンプレートで戻り値の型を引数から導出する必要から生まれた、後置構文への部分的な移行。同じ言語の中でprefix/postfixが混在する例。 |
| C | Type name | Type name | Type f() | なし | すべてprefix方式。C++・Java・C#が受け継いだ大元の構文。 `Type name`という前置構文の大元であり、C++やJavaの`Type name`はここから直接受け継がれた。型推論の構文がないため、宣言は常に完全な形で書く必要がある。関数ポインタや多次元配列が絡むと、宣言子を内側から外側へ読む複雑な規則(俗に「螺旋規則」とも呼ばれる)が必要になり、この読みにくさがGoのpostfix方式(`var name Type`)やRust/Swiftの`name: Type`のような設計を後押しした一因になっている。 |
| Go | var name Type | name Type | func f() Type | :=のみ(ローカル) | 型は名前の後ろに置くpostfix方式。シグネチャに推論はない。 `var name Type`という一貫したpostfix構文を採用しており、Cの`Type name`とは逆順。`:=`によるローカルの型推論はあるが、関数シグネチャでは常に型を書く必要があり、公開APIの可読性を型推論より優先する設計。 |
| Zig | const name: Type | name: Type | fn f() Type | 変数のみ強い | Rust/Go系と同じpostfix構文。戻り値には矢印を使わず、関数シグネチャは常に明示。 `name: Type`というpostfix構文で、`const`/`var`宣言は初期化式から型推論できる。関数は`fn f(args) ReturnType`という形で、Goのように戻り値の型に矢印を使わず引数リストの直後に置くが、Rust/Goと同様に引数・戻り値は推論できず常に明示が必要。 |
| OCaml | let name : Type | (name : Type) | : Type | 非常に強い(ほぼ不要) | Haskellと同じくpostfix寄りだが、型注釈自体をほぼ省略できる。シグネチャを別ファイルに分離することもできる。 変数・引数・戻り値のいずれも`: Type`という後置構文だが、Hindley-Milner推論により実務上はほとんど省略される。さらに`.ml`(実装)とは別に`.mli`(インターフェース)ファイルへ型シグネチャだけを分離して書くこともでき、Haskellの独立したシグネチャ行をファイル単位に発展させたような設計になっている。 |
| F# | let name : Type | (name : Type) | : Type | 非常に強い(ほぼ不要) | OCamlと同じpostfix構文・強い型推論を持ち、シグネチャを別ファイルに分離することもできる。 変数・引数・戻り値のいずれも`: Type`という後置構文で、OCamlと同じくHindley-Milner推論により実務上はほとんど省略される。`.fs`(実装)とは別に`.fsi`(シグネチャ)ファイルへ型シグネチャだけを分離して書くこともでき、OCamlの`.mli`をそのまま.NET向けに受け継いだ設計になっている。 |
| Haskell | name :: Type(別行) | A -> B の一部 | A -> B の一部 | 非常に強い(ほぼ不要) | 型注釈は本体とは別の行に書く「シグネチャ方式」。引数・戻り値を区別せず矢印でつなぐ。 値そのものの定義行とは別に`name :: Type`という型シグネチャを独立した行として書く。関数はカリー化された1引数関数の連鎖として扱われるため、引数と戻り値を構文上区別せず`A -> B`のように矢印でつなぐ。型注釈はHindley-Milner推論によりほぼ省略できるが、可読性のためシグネチャを明示するのが慣習になっている。 |
| Erlang | 型注釈なし | -specで別行(分離) | -specで別行(分離) | なし(Dialyzerが検査) | 型は関数定義の直前に`-spec`属性として別行に書く。Elixirの`@spec`の元になった構文。 変数には型注釈がないが、関数には`-spec`という属性を関数定義の直前に別行で書ける。Haskellの`name :: Type`という独立したシグネチャ行に近い発想で、Elixirの`@spec`はこの構文をそのまま踏襲している。コンパイラソースコードを解析し、実行可能な形式(機械語や中間表現)に変換するプログラム。静的型付け言語では、実行前にコンパイラが型の不整合などをチェックする役割も担う。自体は検査せず、Dialyzerという外部の静的解析ツールが型の整合性を検査する。 |
| Elixir | 型注釈なし | @specで別行(分離) | @specで別行(分離) | なし(Dialyzerが検査) | 型はdefの直前に`@spec`として別行に書く。Haskellの分離型シグネチャに近い発想。 変数には型注釈がないが、関数には`@spec`という属性を`def`の直前に別行で書ける。構文上はHaskellの`name :: Type`という独立したシグネチャ行に近い発想だが、コンパイラソースコードを解析し、実行可能な形式(機械語や中間表現)に変換するプログラム。静的型付け言語では、実行前にコンパイラが型の不整合などをチェックする役割も担う。自体は検査せずDialyzerという外部の静的解析ツールが検査を担う点が異なる。 |
| TypeScript | let name: Type | name: Type | (): Type | 変数・戻り値は強い/引数は必須 | postfix構文。戻り値は推論できるが、引数はstrictモードで実質必須になる。 `name: Type`というpostfix構文で、変数・戻り値はコンパイラソースコードを解析し、実行可能な形式(機械語や中間表現)に変換するプログラム。静的型付け言語では、実行前にコンパイラが型の不整合などをチェックする役割も担う。が推論できるため省略されることが多い。一方、引数の型は`noImplicitAny`を有効にした標準的な設定では推論できず、省略するとエラーになるため実質的に明示が必須という非対称な設計。 |
| JavaScript | 型注釈なし | 型注釈なし | 型注釈なし | 概念自体がない | 型注釈という構文自体が存在しない。 変数宣言・関数引数・戻り値のいずれにも型注釈を書く構文がない。TypeScriptが型注釈を後付けで追加した土台であり、prefix/postfixという比較軸自体が存在しない基準点として位置づけられる。 |
| PHP | 型注釈なし | Type $name | : Type | なし(明示のみ) | 引数はprefix、戻り値はpostfixという、1つのシグネチャの中で位置が混在する。 変数自体には型注釈の構文がないが、関数の引数は`Type $name`という前置、戻り値は`: Type`という後置と、1つの関数シグネチャの中でprefix/postfixが混在する。引数はC系の伝統を継ぎ、戻り値は後発の`: Type`構文(PHP 7以降)を採用した歴史的経緯の違いがそのまま表れている。 |
| Ruby | 型注釈なし | 型注釈なし(RBSで分離) | 型注釈なし(RBSで分離) | なし(動的型付け) | 言語コアに型注釈の構文がなく、型はRBSという別ファイルに分離して書く。 変数・引数・戻り値のいずれにも型注釈の構文がなく、型を付けたい場合はRBSという`.rbs`拡張子の別ファイルにシグネチャだけを書く。OCamlの`.mli`と似た「実装と型定義をファイルごと分離する」発想だが、Rubyでは型注釈が言語コアに一切存在しない点がOCamlとは異なる。 |
| Python | name: Type | name: Type | -> Type | 型ヒントは任意(実行時は無視) | postfix構文の型ヒントを後付けできるが、実行時には無視される。 変数・引数・戻り値のすべてに`: Type`というpostfixの型ヒントを書けるが、これはあくまで静的解析ツール(mypy等)のためのアノテーションで、実行時の型チェックには影響しない。「型注釈の構文を持つこと」と「静的型付けであること」が別問題であることを示す例。 |
トレードオフ
Type-first(prefix)
- 利点: C系の伝統的な書き方で、Java/C++/C#など広く親しまれている。宣言の先頭を見ればすぐに型が分かる。
- 欠点: 複雑なGenerics型(例:
Map<String, List<User>> users)では、型注釈が長くなるほど変数名が埋もれ、読み手は「結局これは何という名前の変数か」を後回しで探すことになる。
Name-first(postfix)
- 利点: 変数名を先に読めるため、宣言の意図(何を表す値か)をまず把握できる。型が長くなっても
name: Map<String, List<User>>のように後ろに伸びるだけで、変数名の位置は変わらない。 - 欠点: C系の書き方に慣れた開発者には見慣れない構文であり、学習コストがかかる。
シグネチャ方式(Haskell/OCamlの独立した型宣言)
- 利点: 値の定義と型情報を完全に分離できるため、実装を変えずに型だけを見比べたり、型を保ったまま実装を差し替えたりしやすい。
- 欠点: 値の定義行と型注釈が離れているため、コードを上から読むだけでは「この値の型は何か」が一目で分からない。
型推論の範囲
ローカル変数のみ推論を許す言語(Rust, Kotlin, Swift, Java, C#, Zigなど)が多数派で、関数シグネチャは公開契約として明示を要求する。Haskell/OCamlのように関数シグネチャまで推論できる言語もあるが、実務上は可読性のためにシグネチャを明示する慣習が根付いている。型注釈の位置(prefix/postfix)と型推論の強さ(どこまで省略できるか)は独立した設計軸であり、postfix系の言語同士でも推論の範囲は言語ごとに異なる。
以上を踏まえ、melangでは変数宣言をpostfix方式かつ型推論を許容し、関数シグネチャは明示必須とすることで、可読性(名前を先に読む)と契約の明確さ(公開APIは常に型が分かる)を両立させる。