型変換
Last Updated: 2026-09-28 02:00
結論
melangでは、型変換(型キャスト)を明示的なメソッド呼び出しとして表現する。
- 暗黙の型変換は許可しない。
long(x)のような型名を使ったキャスト構文は基本的に採用しない。- 変換は値に対するメソッドとして書く。
- 変換が必ず成功する場合は、通常の値を返す。
- 変換に失敗する可能性がある場合は、
Result<T, E>を返す。 - 失敗し得る変換には
tryを含む名前を付け、失敗可能性をコード上から分かるようにする。
x.toLong() // 必ず成功する。Longを返す
x.toString() // 必ず成功する。Stringを返す
x.tryToInt() // 失敗し得る。Result<Int, E>を返す
s.tryParseInt() // 失敗し得る。Result<Int, E>を返す
ここでいう型変換とは、ある型の値を別の型の値へ変換することである。int → longやstring → intのような組み込み型どうしの変換だけでなく、独自型からstringへの変換や、独自型から別のドメイン型への変換も含めて考える。
成功が保証されるかどうか
型変換で特に重要なのは、変換が成功することを保証できるかどうかである。
変換元のすべての値について変換先の値を作れるなら、その変換は必ず成功する。int → longや、あらゆる値のstringへの変換がそうである。この場合はtoLong()やtoString()のように、通常の値を返す。
値によっては変換できないなら、その変換は失敗し得る。long → intは値がintの範囲外だと変換できない。string → intは文字列の中身が数値でなければ変換できない。この場合はtryToInt()やtryParseInt()のように、Result<T, E>を返す。
成功する変換と失敗し得る変換を、名前と戻り値の型の両方で区別する。呼び出し側は、名前を読めば失敗し得るかどうかが分かり、Resultを受け取ることで失敗の扱いを忘れられなくなる。
変換とinterface
変換処理を、型に組み込まれた特殊機能として増やしていくことはしない。変換はinterfaceで定義できるようにする。
interfaceの実装は、Rustのtraitと同様に明示的に実装を宣言する方式とする。これにより、独自型にも標準の型と同じ形で変換を定義できる。
interfaceの形は、RustのFrom / TryFromのような汎用的な変換interfaceを参考にする。ただし、ToLongのように変換先ごとのinterfaceにするか、Convertible<T>のような汎用interfaceにするかは、interface全体の設計を固める段階で決める。
このnoteでは、interfaceの最終形やResult自体の仕様までは決めない。それぞれのnoteで扱う。
- interface: interfaceのnoteで詳細化する。
Result: エラー処理のnoteで詳細化する。- 暗黙の型変換: 禁止する理由と他言語比較を「暗黙の型変換」のnoteで扱う。
他言語比較
型変換の書き方は、言語によって大きく3つに分かれる。
- メソッド:
x.toLong()のように、値に対するメソッドとして書く(Kotlin・Scala・Ruby・MoonBitなど)。 - 型名・キャスト構文:
int64(x)や(int) xのように、型名を使って書く(Go・Swift・Java・C#・C++・Pythonなど)。Zigの@intCastのようなbuiltin関数は、型名ではなく変換の種類を指定する点は異なるが、専用の記号ではなく呼び出し構文で書く点でこの系統に近い。 - trait・関数:
From/IntoやfromIntegralのように、traitや関数で書く(Rust・Haskell・OCamlなど)。
もう一つの違いは、失敗し得る変換をどう表すかである。ResultやOptionのような値で返す言語、例外を投げる言語、boolとout引数で返す言語、NaNや0のような値にして失敗を隠す言語がある。
| 言語 | 変換の書き方 | 失敗し得る変換 | 説明 |
|---|---|---|---|
| Rust | as / From・Into | TryFrom・TryInto(Result) | 変換をtraitで表し、失敗し得る変換をTryFromとして区別する。 `From`は失敗しない変換、`TryFrom`は失敗し得る変換にのみ実装され、変換の安全性が型で区別される。一方で`as`は縮小変換でも黙って値を切り捨てるため、失敗を見逃す抜け道が残っている。melangの「変換をinterfaceで定義し、失敗し得るものは`Result`で返す」方針の主な参考元。 |
| MoonBit | メソッド(to_int64()) | エラー型で表す(パース) | 変換をメソッドで書く。Kotlin・Rustに近い設計。 `to_int64()`, `to_double()`のような変換メソッドを型ごとに持つ。文字列のパースなど失敗し得る変換は、エラーとして表される。melangの記法に近く、エラーを型で表す設計思想も近い。 |
| Swift | 型名のイニシャライザ(Int64(x)) | Optionalを返す(Int(exactly:)) | 型名で変換し、失敗し得る変換は別のイニシャライザで表す。 変換は`Int64(x)`のように型名のイニシャライザで書く。範囲外の値は実行時にクラッシュさせ、失敗を許容する場合は`exactly:`付きのイニシャライザでOptionalを返す。「クラッシュ / nil / 切り捨て」を引数ラベルで書き分ける点は参考になるが、失敗の理由を持たないOptionalで表す。 |
| Kotlin | メソッド(toLong()) | nullを返す(toIntOrNull()) | 変換をメソッドで書き、失敗し得るものにはOrNullを付ける。 数値変換は`toLong()`, `toInt()`のようなメソッドで書き、melangの記法と最も近い。文字列のパースには例外を投げる`toInt()`と、失敗時にnullを返す`toIntOrNull()`があり、名前で失敗の扱いを区別している。ただしnullは失敗の理由を持たず、`Long.toInt()`のような数値の縮小変換は失敗を検出しない。 |
| Java | キャスト構文((int) x) | 例外(NumberFormatExceptionなど) | キャスト構文と、失敗時に例外を投げるAPIを併用する。 基本はキャスト構文で、縮小変換は黙って値を切り捨てる。範囲外を検出したい場合は`Math.toIntExact`のような別のAPIを選ぶ必要がある。文字列のパースは`parseInt`が検査例外ではない`NumberFormatException`を投げるため、失敗可能性がシグネチャに現れない。 |
| Scala | メソッド(toLong) | Optionを返す(toIntOption) | メソッド形式の変換に加え、Optionを返すパースを持つ。 Kotlinと同様に`toLong`, `toInt`のメソッド形式で数値変換を書く。文字列のパースには例外を投げる`toInt`と`Option`を返す`toIntOption`があり、`Try`や`Either`で包んで失敗を値として扱うこともできる。 |
| C# | キャスト構文・Convertクラス | TryParse(bool + out)・例外 | 失敗し得る変換にTry接頭辞を使う先行例を持つ。 失敗し得る変換に`TryParse`という名前を付け、`bool`と`out`引数で成功・失敗を返す慣習が古くからある。`try`を名前に含めて失敗可能性をコード上に示す点はmelangと同じ発想。ただし`bool`は失敗の理由を持たず、`out`引数のため書き方も冗長になる。数値のキャストは既定で検査されない。 |
| C++ | キャスト構文(static_castなど) | 例外・エラーコード(stoi / from_chars) | キャスト構文で変換する。数値の縮小変換は検出されない。 `static_cast`や`reinterpret_cast`など、変換の性質ごとに分かれたキャスト構文を持つ。数値のキャストは失敗を検出しない。文字列からのパースは`std::stoi`が例外、`std::from_chars`がエラーコードで失敗を表し、方式が混在している。 |
| C | キャスト構文((int)x) | 基本なし(strtolなどでerrno/ポインタを確認) | キャスト構文で変換し、失敗を検出する標準的な仕組みは薄い。 `(Type)`によるキャスト構文がC++の`static_cast`などの直接の起源であり、数値の縮小変換は検出されず黙って値が変わる。文字列のパースは、失敗と「0という正当な結果」を区別できない`atoi`のような関数と、`errno`とポインタ引数の両方を確認しないと失敗を検出できない`strtol`のような関数が混在しており、失敗を安全に扱う統一的な言語機能を欠いたまま、各ライブラリ関数が独自の規約で失敗を表している。 |
| Go | 型名(int64(x)) | 多値返却(value, error) | 型名を関数のように使って変換し、パースは(値, error)で返す。 数値型間の変換は`T(x)`の形で、縮小変換でも失敗を検出せず値を切り捨てる。文字列からのパースは`strconv`パッケージの関数が`(値, error)`を返し、呼び出し側が`err`を確認する。失敗を返り値で表す点はmelangの`Result`に近いが、確認を忘れてもコンパイルは通る。 |
| Zig | builtin関数(@intCast等) | エラーユニオン(!T)・範囲外はpanic | builtinの変換関数で明示し、失敗し得るパースはエラーユニオンで表す。 `@intCast`, `@floatCast`, `@truncate`, `@ptrCast`のようなbuiltin関数で変換を明示する。数値の縮小変換は安全性チェック付きビルド(Debug/ReleaseSafe)では範囲外の値を実行時panicとして検出し(ReleaseFast/ReleaseSmallでは未定義動作)、切り捨てを許容したい場合は`@truncate`を使い分ける。文字列のパースなど失敗し得る変換は`std.fmt.parseInt`のようにerror union(`!T`)を返す関数で表現し、`try`で伝播するか`catch`で処理する。C由来の暗黙の数値変換はほとんどなく、comptime-known値やpeer type resolutionによる限定的なcoercionを除けば変換は明示のbuiltinを介する。 |
| OCaml | 変換関数(Int64.of_int) | optionを返す(int_of_string_opt) | 変換ごとに関数を用意し、失敗し得るものに_optを付ける。 `Int64.of_int`, `float_of_int`のように、変換元と変換先の組ごとに専用の関数を持つ。文字列のパースは例外を投げる`int_of_string`と、`option`を返す`int_of_string_opt`があり、名前の接尾辞で失敗の扱いを区別する。 |
| F# | 変換演算子(int64 x) | .NETのTryParse(タプルに変換) | 組み込みの変換演算子で変換し、縮小変換は検出しない。.NETのTryParseはタプルとして使える。 OCamlの`Int64.of_int`のような個別関数ではなく、`int`, `int64`, `float`のような組み込みの変換演算子を持ち、C言語風のキャストに近い書き味になっている。数値の縮小変換はC++やJavaと同様に黙って値を切り捨てる。.NET標準の`TryParse`(`bool`と`out`引数)は、F#コンパイラソースコードを解析し、実行可能な形式(機械語や中間表現)に変換するプログラム。静的型付け言語では、実行前にコンパイラが型の不整合などをチェックする役割も担う。が`out`引数を戻り値のタプルへ自動変換して見せるため、C#より扱いやすい形でパターンマッチできる。 |
| Haskell | 変換関数(fromIntegral) | Maybeを返す(readMaybe) | 変換関数で明示し、失敗し得る変換はMaybeで返す。 数値型間の変換は`fromIntegral`が担い、縮小変換でも黙って値が溢れる。範囲外を検出したい場合は`toIntegralSized`が`Maybe`を返す。文字列のパースは、例外を投げる`read`より失敗を`Maybe`で表す`readMaybe`が推奨されている。 |
| Erlang | BIF(integer_to_list) | エラーで停止・try/catch | 変換は組み込み関数(BIF)で行い、失敗時はプロセスが異常終了する。 `integer_to_list/1`, `list_to_integer/1`のような組み込み関数(BIF)で変換する。失敗すると`badarg`のようなエラーで即座にプロセスが終了する「let it crash」が基本方針で、必要な箇所だけ`try`/`catch`で捕捉する。Elixirの`{:ok, value}`のような値としての失敗表現は、Erlang標準ライブラリの慣習としては薄い。 |
| Elixir | 関数(Integer.to_string) | タプルを返す(Integer.parse)・例外 | 変換を関数で書き、パースは結果をタプルか:errorで返す。 `Integer.parse/1`は成功時に`{値, 残りの文字列}`、失敗時に`:error`を返し、パターンマッチで扱える。一方`String.to_integer/1`は失敗時に例外を投げる。Elixirでは`{:ok, value}`のようなタプルで結果を表す慣習があり、melangの`Result`に近い考え方。 |
| TypeScript | Number(x) / x.toString() | 型に現れない(NaN) | 型検査は変換しない。実行時の変換はJavaScriptに従う。 `Number(x)`や`x.toString()`で実行時に変換する。失敗は`NaN`になり、型はnumberのままなので失敗可能性が型に現れない。`as`は型検査器への申告にすぎず、実行時には何も変換しない点も、変換と混同しやすい。 |
| JavaScript | Number(x) / x.toString() | 型に現れない(NaN・例外) | 変換関数とメソッドを併用し、失敗はNaNや例外で表す。 `Number("abc")`は`NaN`、`parseInt("42px")`は先頭の数字だけを読んで`42`を返すなど、失敗や部分的な成功が値の中に紛れ込む。`BigInt(1.5)`のように例外で失敗する変換もあり、失敗の表現が統一されていない。 |
| PHP | キャスト構文((int)$x) | 黙って0・falseを返す | キャスト構文で変換し、失敗を黙って別の値にする場合がある。 `(int)`キャストは変換に失敗しても`0`を返すなど、失敗を通常の値として扱う。失敗を検出したい場合は`filter_var`のような別の関数を使い、失敗は`false`で返る。成功した`0`と失敗を区別するには呼び出し側の注意が必要になる。 |
| Ruby | メソッド(to_i / to_s) | 例外(Integer())・nil | 変換をメソッドで書き、厳密な変換は別の関数で分ける。 `to_i`, `to_s`, `to_f`のメソッド形式で変換するが、`to_i`は緩く、失敗しても`0`を返す。厳密に変換したい場合は`Integer()`を使い、失敗は`ArgumentError`になる。`exception: false`を渡すと`nil`を返す。 |
| Python | 型名(int(x)) | 例外(ValueError) | 型名を関数のように呼んで変換し、失敗は例外で表す。 `int(x)`, `float(x)`, `str(x)`のように型名で変換する。失敗は`ValueError`で表され、`try`/`except`で処理する。整数は任意精度なので数値の範囲外による失敗は起きないが、失敗可能性は関数の型からは分からない。 |
比較の観点は次の通りである。
- 変換を、メソッド・型名・キャスト構文・関数のどれで書くか。
- 失敗し得る変換を、名前で区別できるか。
- 失敗を、値・例外・エラーコード・特殊な値のどれで表すか。
- 失敗の理由を呼び出し側に渡せるか。
- 縮小変換(
long → intなど)が、範囲外の値を黙って切り捨てないか。 - 独自型の変換を、標準の型と同じ形で定義できるか。
Kotlinは、数値変換をtoLong()のようなメソッドで書き、暗黙の数値変換もない。文字列のパースには、例外を投げるtoInt()と、nullを返すtoIntOrNull()があり、名前で失敗の扱いを区別する。melangの記法に最も近い。
Rustは、Fromは失敗しない変換、TryFromは失敗し得る変換にだけ実装され、TryFromはResultを返す。失敗可能性を型で表す点と、変換をtraitで定義する点が、melangの直接の参考になる。ただしasは縮小変換で値を黙って切り捨てるため、失敗を見逃す抜け道が残っている。
C#のTryParseは、tryを名前に含めて失敗可能性を示す先行例である。ただし戻り値はboolで、失敗の理由は分からない。SwiftのInt(exactly:)やKotlinのtoIntOrNull()も、失敗を値で返すが、Optionalやnullのため失敗の理由を持たない。
Go・Java・C++は、縮小変換で値を黙って切り捨てる。PHPの(int)"abc"やRubyの"abc".to_iは、変換に失敗しても0を返す。JavaScriptのNumber("abc")はNaNを返し、型はnumberのままである。これらは、失敗が通常の値に紛れ込む例である。
Zigは@intCastのようなbuiltin関数で変換を明示し、縮小変換は安全性チェック付きビルド(Debug/ReleaseSafe)では範囲外の値を実行時panicとして検出する(切り捨てを許容したい場合は@truncateを使い分ける)。文字列のパースなど失敗し得る変換はstd.fmt.parseIntのようにerror union(!T)を返す関数で表現され、RustのResultに近い失敗の扱いをする。
トレードオフ
メソッドで変換する設計(Kotlin, Scala, Ruby, MoonBitなど)
- 利点: 変換がコード上で明示される。
x.toLong()のように、値から変換先へと左から右に読める。メソッドなので、補完で候補を探しやすく、呼び出しを連鎖させやすい。 - 欠点: 単純な変換でも毎回メソッド呼び出しが必要になり、冗長になる。型変換を多用するコードでは記述量が増える。
型名・キャスト構文で変換する設計(Go, Swift, Java, C#, C++, Pythonなど)
- 利点:
int64(x)のように変換先が先頭に来るため、変換後の型が読み取りやすい。多くの言語で慣れた書き方である。 - 欠点: 型名やキャスト構文は言語の特殊な構文になりやすく、独自型の変換を標準の型と同じ形で定義するのが難しい。失敗し得る変換を、名前で区別しにくい。C++・Java・Goのように、縮小変換が失敗を検出しない言語も多い。
失敗を例外や特殊な値で表す設計(Java, Python, JavaScript, PHP, Rubyなど)
- 利点: 成功する前提のコードを簡潔に書ける。
- 欠点: 失敗可能性が関数のシグネチャに現れず、呼び出し側が失敗を見落としやすい。
NaNや0のように失敗が通常の値に紛れ込むと、バグに気づくのが遅れる。
失敗を値で返す設計(Rust, Kotlin, Swift, Scala, Haskell, OCaml, Zigなど)
- 利点: 失敗可能性が型に現れ、呼び出し側は失敗を扱わないとコードを書けない。
Resultなら、失敗の理由も渡せる。 - 欠点: 失敗し得る変換を呼ぶたびに、結果を取り出す処理が必要になる。
Optionやnullで返す設計は簡潔だが、失敗の理由を持てない。
melangの選択
melangは、Kotlinのメソッド形式と、Rustの「成功する変換と失敗し得る変換を区別し、失敗をResultで返す」設計を組み合わせる。失敗し得る変換には、C#のTryParseと同じくtryを名前に含める。
- Pros: 変換がコード上で明示され、暗黙変換による予期しない挙動を防げる。型が勝手に変化しないため、コードを読んだときの予測可能性が高い。
tryとResultによって、失敗可能な変換を、型と名前の両方から認識できる。interfaceによって、変換可能性を明示的に表現できる。 - Cons: 単純な変換でも毎回メソッド呼び出しが必要になり、冗長になる。interfaceを使うことで、単純な変換でも設計要素が増える可能性がある。どの変換を成功保証とし、どの変換を失敗可能とするかの設計が必要になる。
冗長さは許容する。簡潔さよりも、変換の有無と失敗可能性を、コードから読み取れることを優先する。