← melang

関数の引数

Last Updated: 2026-09-28 02:00

結論

melangでは、関数の引数の渡し方(値渡し・参照渡し)をsyntaxとして表現しない。呼び出し側にも定義側にも、渡し方を書かない。

fn total(price: Int, items: List<Item>) -> Int { ... }

total(p, xs)  // 渡し方は書かない

渡し方を書かなくてよい理由

引数の渡し方を明示したくなるのは、渡した先で値が書き換えられたり、所有権が移って元の変数が使えなくなったりして、呼び出し側の結果が変わり得るからである。

melangでは、値が不変で代入も禁止されるため、呼び出された側が引数を書き換えて、呼び出し側に影響することがない。所有権も扱わないので、渡した変数が使えなくなることもない。引数がコピーされても共有されても、プログラムの結果は変わらない。結果が変わらないものを、書き手に選ばせる理由はない。

そこで、言語の仕様として常に共有して渡すと決める。不変なので、コピーして独立させる意味がない。呼び出しのコストは引数の個数だけで決まり、値の大きさに依存しない。

これはJava・Kotlinの渡し方に近い。どちらも引数は値渡しで、オブジェクトは参照の値が渡され、中身は共有される。違いは、melangではオブジェクト自体が不変な点である。Java・Kotlinでは、共有された中身を書き換えると呼び出し側から見えてしまうが、melangでは書き換えられないので、共有していることが結果に現れない。

呼び出し側から見れば、total(p, xs)と書いた時点で、pとxsが変わらないことが言語によって保証される。関数の定義を確認しなくても、呼び出しの前後で値が変化しないと読み取れる。これは、渡し方を明示する設計が目指す「呼び出しを読むだけで値の扱いが分かる」ことを、渡し方を書かずに実現する方法である。

検討した案

当初は、foo(copy x) / foo(ref x) / foo(move x) のように、呼び出し側にも定義側にも渡し方を書く設計を検討した。次の理由で採用しなかった。

このnoteで決めないこと

他言語比較

引数の渡し方は、言語によって大きく4つに分かれる。

言語通常の引数参照・借用の書き方呼び出し側の明示説明
Rustmove(Copy型はコピー)&T / &mut T(借用)&x / &mut x を付ける。moveは印なし
MoonBit値渡し(引数は不変)Ref[T]・可変な構造体など印なし
Swift値渡し(値型はコピー)inout / borrowing / consuminginout は &x を付ける
Kotlin値渡し(オブジェクトは参照値)専用構文なし印なし
Java値渡し(オブジェクトは参照値)専用構文なし印なし
Scala値渡し(オブジェクトは参照値)専用構文なし(名前渡しは=>)印なし
C#値渡し(参照型は参照値)ref / out / inref・out は呼び出し側にも必須
C++値渡し(コピー)T& / const T& / T&&参照は印なし。moveは std::move
C値渡し(コピー)ポインタ(T*)ポインタは &x を渡すか、既にポインタの値を渡す
Go値渡し(コピー)ポインタ(*T)ポインタは &x を付ける
Zig値渡し(構造体等は参照渡しへの最適化あり)ポインタ(*T)・スライス([]T)ポインタは &x を付ける
OCaml値渡し(値は基本的に不変)ref型(可変セル)を渡す印なし(型で分かる)
F#値渡し(値は基本的に不変)ref型(可変セル)・byrefを渡す印なし(byrefのみ&が必要)
Haskell遅延評価(値は不変)なし(処理系が決める)印なし
Erlang値渡し(値は不変)同一プロセス内は共有。プロセス間送信はコピー印なし
Elixir値渡し(値は不変)なし(処理系が決める)印なし
TypeScript値渡し(オブジェクトは参照値)専用構文なし印なし
JavaScript値渡し(オブジェクトは参照値)専用構文なし印なし
PHP値渡し(オブジェクトはハンドル)&$x(参照渡し)印なし(定義側だけ)
Rubyオブジェクト参照の値渡し専用構文なし印なし
Pythonオブジェクト参照の値渡し専用構文なし印なし

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

  1. コピーして渡すのか、同じ値を共有して渡すのか。
  2. 借用所有権を移さずに、値を一時的に貸し出して使わせること。Rustでは`&T`(読み取り専用)と`&mut T`(変更可能)で表し、借りている間の書き換えや破棄をコンパイラが検査する。するのか、所有権を移すのか。
  3. 渡し方が、関数定義の型・契約に含まれるか。
  4. 呼び出し側でも、渡し方を明示する必要があるか。
  5. 変更可能な参照を、別の概念として扱うか。
  6. GCを採用する場合でも、所有権・借用・変更をsyntaxとして明示するか。

C#は、refとoutを定義側と呼び出し側の両方に書かないとコンパイルエラーになる。呼び出しを読むだけで、変数が書き換えられる可能性が分かる。ただし、通常の値渡しには印がなく、inは呼び出し側で省略できる。

Swiftも、書き換えを許すinoutを呼び出し側の&で示す。Rustも、借用は呼び出し側に& / &mutが付く。一方でRustのmoveは、呼び出し側に印がなく、変数が消費されることは関数の定義を見ないと分からない。

C++は、f(s)という同じ見た目の呼び出しが、値渡しにも参照渡しにもなり得る。PHPも、定義側に&を付けると参照渡しになるが、呼び出し側は変わらない。どちらも、変数が書き換わるかを、関数の定義を確認するまで読み取れない。

Zigも、引数はすべて値渡しで、書き換えたい場合はポインタを明示的に渡す点はGoと同じである。ただし、Goでは引数を関数内で自由に再代入できるのに対し、Zigの引数は関数内で既定でconst相当となり、書き換えるにはローカルのvarへ明示的にコピーする必要がある。また、struct/arrayのような大きな集約型は、意味論上は値渡し(コピー)のまま、コンパイラがABIレベルの最適化として参照渡しを選ぶことがあるが、これは呼び出し側から見える意味論には影響しない実装詳細である。

Java・Kotlin・Python・JavaScript・Rubyは、渡し方を選べない。値渡しなのにオブジェクトの中身は共有されるため、再代入は呼び出し側に影響せず、破壊的な変更は影響するという違いが生まれる。可変か不変かを知らないと、呼び出しの結果を予測できない。

これらの言語で渡し方が問題になるのは、値が書き換えられるからである。Rust・C++・Swift・C#のref / inout / &mutは書き換えを表すための仕組みで、Rustのmoveや借用は、書き換えと解放の安全性を守るための仕組みである。

HaskellとElixirは、値がすべて不変なので、渡し方が結果に影響せず、コード上にも現れない。melangの前提に最も近い。melangは、Java・Kotlinの「参照の値を渡し、中身を共有する」渡し方に、Haskell・Elixirの「値がすべて不変」という前提を組み合わせる。

トレードオフ

渡し方を選べず、暗黙に決まる設計(Java, Kotlin, Python, JavaScript, Rubyなど)

渡し方を定義側にだけ書く設計(C++, PHPなど)

渡し方を呼び出し側にも書く設計(C#, Swift, Rustの借用など)

所有権を型で管理する設計(Rust, Swiftのconsumingなど)

値がすべて不変で、渡し方を書かない設計(Haskell, Elixirなど)

melangの選択

melangは、Java・Kotlinと同じく引数を常に共有して渡し、そのうえでHaskell・Elixirと同じく値をすべて不変とする。渡し方を指定するsyntaxは設けない。所有権・借用・変更可能な参照は導入しない。

渡し方を選べる自由よりも、渡し方を考えなくてよいことと、値が変わらない保証を優先する。