暗黙の型変換
Last Updated: 2026-09-28 02:00
結論
melangでは、暗黙の型変換(暗黙的なキャスト)を一切許可しない。
型が異なる値を扱う場合、たとえ変換先に安全に収まることが保証できる場合でも、必ず明示的な型変換を記述する。int → longのような、値が壊れないことが分かりきった変換であっても例外を設けない。
int x = 10;
long y = x; // NG
long y = x.toLong(); // OK
代入だけでなく、引数渡しや演算など、暗黙変換が発生し得るあらゆる場面で同じ方針を適用する。
long a = 10;
int b = 20;
a + b; // NG
a + b.toLong(); // OK
melangが重視するのは、変換が安全かどうかだけではなく、「型を変換する」という処理そのものをコード上に明示することである。
- 予測可能性: 値の型が勝手に変化しない。
- 安全性: 意図しない変換によるバグを防ぐ。
- 明示性: 変換という意味のある処理をコードに残す。
冗長になることは許容する。簡潔さよりも、コードから型の変化を追跡できることを優先する。
なお、暗黙の型変換を禁止することと、型変換そのものを禁止することは別である。melangでは型変換自体は認めるが、必ずx.toLong()のような明示的なメソッドとして記述する。失敗し得る変換についてはtryToInt()のような命名とResult<T, E>を用いる(詳細は「型変換」のnoteで扱う)。
他言語比較
暗黙の型変換の有無は、言語によって大きく異なる。特にintからlongのような数値型間の変換は、言語ごとに扱いが分かれる。C系の言語は算術型の変換を広く暗黙に行う一方、Go・Rust・Kotlin・Swift・Zigのように数値型間の変換を一切暗黙に行わない言語も増えている。
| 言語 | 暗黙の数値変換 | 説明 |
|---|---|---|
| Rust | なし | 数値型間の暗黙変換を一切持たない。 i32からi64のような拡大変換であっても暗黙には行われず、`as`や`From`/`Into`による明示が必須。`as`は情報を失う変換も許すが、`From`は失敗しない変換にのみ実装される、というように変換の安全性を型で区別している。 |
| MoonBit | なし | Rust/Kotlinに近く、数値型間の暗黙変換を持たない。 Rust/Kotlinに近い設計思想で、数値型間の暗黙変換を持たない。`to_double()`のような明示的な変換メソッドを呼ぶ必要がある。 |
| Swift | なし | 数値型間の暗黙変換を持たず、明示的な変換を要求する。 数値型間の暗黙変換を持たず、`Int`と`Double`を混在させる演算もコンパイルエラーになる。`Double(x)`のようにイニシャライザを介した明示的な変換が必要。 |
| Kotlin | なし | 拡大変換であっても数値型間の暗黙変換はない。 Javaと異なり、Kotlinは拡大変換であっても数値型間の暗黙変換を行わない。`toLong()`, `toDouble()`のような明示的な変換メソッドを必ず呼ぶ設計で、melangが目指す方針に最も近い言語の一つ。 |
| Java | あり(拡大のみ) | 拡大変換は暗黙、縮小変換は明示が必要。 intからlong、floatからdoubleのような「情報を失わない」拡大変換(widening)は暗黙に行われる。逆に縮小変換(narrowing)は明示的なキャストを要求する。二項演算子のオペランドも、大きい方の型に暗黙に合わせられる(binary numeric promotion)。 |
| Scala | あり(拡大のみ) | 拡大変換は暗黙で、ユーザー定義の暗黙変換も追加できる。 Javaと同様、IntからLong、FloatからDoubleのような拡大変換は暗黙に行われる。さらに`implicit def`(Scala 3では`given`によるconversion)でユーザー定義の暗黙変換を追加できるが、`scala.language.implicitConversions`の明示的な有効化を要求するなど、濫用への注意喚起がされている。 |
| C# | あり(拡大のみ) | 拡大変換は暗黙、縮小変換は明示的なキャストが必要。 intからlong、floatからdoubleのような拡大変換は暗黙に行われる。ユーザー定義型でも`implicit operator`を定義すれば独自の暗黙変換を組み込める。縮小変換は明示的なキャストが必要。 |
| C++ | あり | 算術型の標準変換が広く暗黙に行われる。 算術型間の標準変換(整数昇格・浮動小数点変換など)が広く暗黙に行われる。縮小変換(intからshortなど)も多くの場合は警告止まりでコンパイルが通り、意図しない情報欠落が起きやすい。ユーザー定義型でも変換コンストラクタやconversion operatorにより暗黙変換を定義できる(`explicit`で抑制可能)。 |
| C | あり(暗黙の整数昇格) | 整数昇格・usual arithmetic conversionsが広く働き、符号なし変換で事故が起きやすい。 算術型間の暗黙変換(integer promotion / usual arithmetic conversions)が広く働き、C++はこれをそのまま受け継いでいる。特にsignedとunsignedを混在させる比較・演算では、signedの値が暗黙にunsignedへ変換されるため`-1 < 3u`が偽になるといった直感に反する挙動が起き、実務上のバグの温床として知られる。この危険な暗黙変換の存在が、KotlinやSwiftのように数値型間の暗黙変換を一切禁止する設計への反面教師になっている。 |
| Go | なし | 異なる数値型間の変換は常に明示が必要。 int32からint64のような安全な拡大変換であっても暗黙には行われず、必ず`T(x)`という形の明示的な変換を書く。 |
| Zig | なし(comptime値のcoercionは別) | 実行時値の数値変換は暗黙にはできないが、comptime値は自動でcoerceされる。 実行時に決まる値同士の数値型間ではRust/Goと同様に暗黙変換を持たず、i32からi64のような拡大変換であっても`@intCast`などのbuiltinで明示する必要がある。一方、リテラルのようなcomptime_int/comptime_float型のcomptime-known値は、収まる範囲であれば変換先の型へ自動的にcoerceされる。さらにif/elseの各分岐や配列・スライスの型を揃えるpeer type resolutionという別の仕組みもあり、「暗黙変換が一切ない」と単純化はできない。 |
| OCaml | なし | 整数と浮動小数点の演算子すら分けるほど暗黙変換を持たない。 数値型間の暗黙変換を一切持たない徹底した設計で、intとfloatの加算にも別の演算子(`+`と`+.`)を使い分ける必要があるほど厳格。変換には`float_of_int`, `int_of_float`のような専用関数を使う。 |
| F# | なし | OCamlと同様、数値型間の暗黙変換を一切持たない。 OCamlを受け継ぎ、intからfloatのような一方向の変換であっても暗黙には行われない。ただし`+.`のような別演算子は持たず、`+`は数値の型を静的に確定できれば`int`・`float`のどちらにも使える(演算子オーバーロードによる静的多相)ため、演算子自体を使い分ける必要はOCamlより薄い。変換には`float x`, `int64 x`のような組み込みの変換演算子を使う。 |
| Haskell | なし | 数値型間の暗黙変換はなく、変換関数で明示する。 数値型間の暗黙変換はなく、`Int`から`Integer`、`Int`から`Double`のような変換にも`fromIntegral`や`realToFrac`のような明示的な変換関数を要求する。数値リテラル自体は`Num`型クラスにより多相的に扱われるが、これは既存の値を変換する話とは別の仕組み。 |
| Erlang | 一部あり(数値のみ) | integer/float間は暗黙変換されるが、他の型とは変換されない。 integerとfloatを混在させる算術演算では暗黙に型が揃えられるが、それ以外の型(文字列・アトムなど)との暗黙変換は行われない。ガード節(`is_integer`など)で型を明示的に確認するスタイルが基本で、Elixirもこの挙動をそのまま受け継いでいる。 |
| Elixir | 一部あり(数値のみ) | integer/float間は暗黙変換されるが、他の型とは変換されない。 integerとfloatを混在させる算術演算では暗黙に型が揃えられるが、それ以外の型(文字列など)との暗黙変換は行われない。パターンマッチやガード節で型を明示的に扱うスタイルが基本。 |
| TypeScript | なし(型検査上) | 型検査上は暗黙変換を許さないが、実行時はJS由来の変換が残る。 静的な型検査上は数値と文字列などの間の暗黙変換を許さない。ただし型検査を通過してJavaScriptとして実行される段階では、`+`演算子など実行時の暗黙変換自体は残っており、「型システム上の暗黙変換」と「実行時の暗黙変換」が分離しているのが特徴。 |
| JavaScript | あり | 演算子ごとに異なる規則で型を暗黙変換する。 演算子ごとに異なる規則でオペランドの型を暗黙に変換する。`+`は片方が文字列なら文字列結合、`-`などは数値変換を試みるといったように規則が演算子ごとに異なり、意図しない挙動の温床になりやすい。 |
| PHP | あり | 数値と数値形式の文字列間で暗黙変換が広く行われる。 数値と数値形式の文字列との間でも暗黙変換が広く行われ、`==`のような比較演算子でも型が揃えられてから比較される。`declare(strict_types=1)`や`===`を使うことで暗黙変換を抑制できるが、デフォルトでは緩い。 |
| Ruby | 一部あり(数値のみ) | 数値型同士は暗黙変換されるが、文字列との混在はエラーになる。 Integer/Floatのような数値型同士の演算では暗黙に型を揃えるが、数値と文字列のような異なる種類の型を混ぜるとTypeErrorになる。ユーザー定義クラスでも`coerce`メソッドを実装すれば数値との暗黙変換に参加できる。 |
| Python | 一部あり(数値のみ) | 数値の型階層内では暗黙変換されるが、他の型とは変換されない。 数値の型階層(int -> float -> complex)内では暗黙変換が行われるが、数値と文字列のような異なる種類の型の間では暗黙変換は起きず、実行時にエラーになる。 |
比較の観点は次の通りである。
- 数値型間の暗黙変換があるか。
- 代入時と演算時で変換規則が異なるか。
- 情報を失う可能性のある変換(縮小変換)に明示指定が必要か。
- ユーザー定義型の暗黙変換を定義できるか。
- 型変換をメソッドとして表現するか、キャスト構文として表現するか。
C・C++・Java・C#・Scalaは、拡大変換(情報を失わない変換)を中心に暗黙変換を許容する。一方Kotlin・Swift・Go・Rust・MoonBit・Zigは、拡大変換であっても暗黙には行わず、変換メソッドやキャスト構文による明示を要求する。ただしZigは例外的に、リテラルのようなcomptime-known値(comptime_int/comptime_float型)に限り、収まる範囲であれば変換先の型へ自動的にcoerceされる仕組みを持つ。Python・Ruby・Elixirのような動的型付け言語は、数値の型階層内でのみ暗黙変換を行い、数値と文字列のような異なる種類の型は変換しない。JavaScript・PHPはさらに緩く、数値と文字列の間でも暗黙変換が働く。
トレードオフ
暗黙変換を許容する設計(C, C++, Java, C#, Scalaなど)
- 利点: 拡大変換のような「安全だと分かっている」変換を書かずに済み、コードが簡潔になる。数値型を跨いだ演算を自然に書ける。
- 欠点: どの時点でどの型に変換されたかがコード上に残らず、追跡が難しくなる。C++のように縮小変換まで警告止まりで通ってしまう言語では、意図しない情報欠落がコンパイルを通過してしまう。
暗黙変換を許容しない設計(Rust, Kotlin, Swift, Go, MoonBit, Zigなど)
- 利点: 型が勝手に変化しないため、コードの挙動を予測しやすい。変換処理の意図がコードから明確に分かり、数値型のサイズ・符号・精度を意識せずに自動変換されることを防げる。「安全な変換だけ許可する」といった例外ルールを設ける必要もない。
- 欠点: 単純な変換でも明示的な記述が必要になり、冗長になる。異なる型を組み合わせる処理では変換メソッドが頻繁に登場し、他言語のコードと比較すると記述量が多くなる場合がある。
動的型付け言語の中間的な設計(Python, Ruby, Elixirなど)
- 特徴: 数値の型階層内(int/float間など)でのみ暗黙変換を許し、数値と文字列のような種類の異なる型は変換しない。「安全に振る舞える範囲」を数値の枠内に限定することで、JavaScriptやPHPほど広範な暗黙変換は避けている。
- melangとの違い: melangはこの「数値の型階層内なら安全」という判断自体を型システムに持ち込まない。int/float間の演算であっても、変換処理をコードに明示することを優先する。
以上を踏まえ、melangはKotlin・Swift・Go・Rust・MoonBit・Zigと同じ立場を取り、拡大変換を含むあらゆる数値型間の暗黙変換を禁止する。冗長さは許容し、予測可能性・安全性・明示性を優先する。