← melang

型変換

Last Updated: 2026-09-28 02:00

結論

melangでは、型変換(型キャスト)を明示的なメソッド呼び出しとして表現する。

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で扱う。

他言語比較

型変換の書き方は、言語によって大きく3つに分かれる。

もう一つの違いは、失敗し得る変換をどう表すかである。ResultやOptionのような値で返す言語、例外を投げる言語、boolとout引数で返す言語、NaNや0のような値にして失敗を隠す言語がある。

言語変換の書き方失敗し得る変換説明
Rustas / From・IntoTryFrom・TryInto(Result)
MoonBitメソッド(to_int64())エラー型で表す(パース)
Swift型名のイニシャライザ(Int64(x))Optionalを返す(Int(exactly:))
Kotlinメソッド(toLong())nullを返す(toIntOrNull())
Javaキャスト構文((int) x)例外(NumberFormatExceptionなど)
Scalaメソッド(toLong)Optionを返す(toIntOption)
C#キャスト構文・ConvertクラスTryParse(bool + out)・例外
C++キャスト構文(static_castなど)例外・エラーコード(stoi / from_chars)
Cキャスト構文((int)x)基本なし(strtolなどでerrno/ポインタを確認)
Go型名(int64(x))多値返却(value, error)
Zigbuiltin関数(@intCast等)エラーユニオン(!T)・範囲外はpanic
OCaml変換関数(Int64.of_int)optionを返す(int_of_string_opt)
F#変換演算子(int64 x).NETのTryParse(タプルに変換)
Haskell変換関数(fromIntegral)Maybeを返す(readMaybe)
ErlangBIF(integer_to_list)エラーで停止・try/catch
Elixir関数(Integer.to_string)タプルを返す(Integer.parse)・例外
TypeScriptNumber(x) / x.toString()型に現れない(NaN)
JavaScriptNumber(x) / x.toString()型に現れない(NaN・例外)
PHPキャスト構文((int)$x)黙って0・falseを返す
Rubyメソッド(to_i / to_s)例外(Integer())・nil
Python型名(int(x))例外(ValueError)

比較の観点は次の通りである。

  1. 変換を、メソッド・型名・キャスト構文・関数のどれで書くか。
  2. 失敗し得る変換を、名前で区別できるか。
  3. 失敗を、値・例外・エラーコード・特殊な値のどれで表すか。
  4. 失敗の理由を呼び出し側に渡せるか。
  5. 縮小変換(long → intなど)が、範囲外の値を黙って切り捨てないか。
  6. 独自型の変換を、標準の型と同じ形で定義できるか。

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など)

型名・キャスト構文で変換する設計(Go, Swift, Java, C#, C++, Pythonなど)

失敗を例外や特殊な値で表す設計(Java, Python, JavaScript, PHP, Rubyなど)

失敗を値で返す設計(Rust, Kotlin, Swift, Scala, Haskell, OCaml, Zigなど)

melangの選択

melangは、Kotlinのメソッド形式と、Rustの「成功する変換と失敗し得る変換を区別し、失敗をResultで返す」設計を組み合わせる。失敗し得る変換には、C#のTryParseと同じくtryを名前に含める。

冗長さは許容する。簡潔さよりも、変換の有無と失敗可能性を、コードから読み取れることを優先する。