関数の引数
Last Updated: 2026-09-28 02:00
結論
melangでは、関数の引数の渡し方(値渡し・参照渡し)をsyntaxとして表現しない。呼び出し側にも定義側にも、渡し方を書かない。
copy/ref/moveのような、渡し方を指定するキーワードは設けない。inoutや&mutのような、変更可能な参照も設けない。- 所有権値の持ち主を一つに定め、持ち主がいなくなったときに値を破棄する仕組み。Rustが代表例で、値を別の変数や関数へ渡すと持ち主が移り(move)、元の変数は使えなくなる。は扱わない。
- 値(オブジェクト)はすべて不変とし、代入そのものを禁止する。
- 引数は常に共有して渡す。値のコピーは行わない。
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) のように、呼び出し側にも定義側にも渡し方を書く設計を検討した。次の理由で採用しなかった。
- 所有権を扱わないので、
moveは不要になる。 - 変更可能なデータが存在しないので、変更可能な参照は不要になる。
- 残った
copyとrefは、結果が変わらず、複製するか共有するかというコストの違いだけになる。常に共有と決めれば、選ぶ必要自体がなくなる。
このnoteで決めないこと
- 値を不変とする範囲や、更新を表現する方法。値が不変であることは、「値の不変性」のnoteで扱う。
- 参照の同一性(同じ値かどうか)を言語として公開するか。公開すると、共有していることが結果に現れる。
- メモリ管理(GCなど)の方式。
- 外部関数(FFI)との受け渡しで、渡し方を表現する必要があるか。
- 不変な値を更新するときに、新しい値を作るコストをどう抑えるか。
他言語比較
引数の渡し方は、言語によって大きく4つに分かれる。
- 値渡しのみ: 引数はすべて値渡しで、参照的に扱うにはポインタなどを明示的に渡す(Go・Zig)。
- オブジェクトは参照の値: 引数は値渡しだが、オブジェクトは参照の値が渡され、中身は共有される(Java・Kotlin・Scala・TypeScript・JavaScript・Python・Ruby)。
- 渡し方を選べる: 値・参照・所有権の移動などを、キーワードや型で選べる(Rust・C++・Swift・C#・PHP)。
- 値が不変で、渡し方を書かない: 値がすべて不変なので、渡し方はプログラムの結果に影響せず、コード上に現れない(Haskell・Elixir。OCaml・MoonBitは
ref型など可変な型で書き換えを表す)。
| 言語 | 通常の引数 | 参照・借用の書き方 | 呼び出し側の明示 | 説明 |
|---|---|---|---|---|
| Rust | move(Copy型はコピー) | &T / &mut T(借用) | &x / &mut x を付ける。moveは印なし | 所有権値の持ち主を一つに定め、持ち主がいなくなったときに値を破棄する仕組み。Rustが代表例で、値を別の変数や関数へ渡すと持ち主が移り(move)、元の変数は使えなくなる。を型で管理し、move・借用所有権を移さずに、値を一時的に貸し出して使わせること。Rustでは`&T`(読み取り専用)と`&mut T`(変更可能)で表し、借りている間の書き換えや破棄をコンパイラが検査する。・可変借用所有権を移さずに、値を一時的に貸し出して使わせること。Rustでは`&T`(読み取り専用)と`&mut T`(変更可能)で表し、借りている間の書き換えや破棄をコンパイラが検査する。を使い分ける。 引数の渡し方が「借用所有権を移さずに、値を一時的に貸し出して使わせること。Rustでは`&T`(読み取り専用)と`&mut T`(変更可能)で表し、借りている間の書き換えや破棄をコンパイラが検査する。するか、所有権値の持ち主を一つに定め、持ち主がいなくなったときに値を破棄する仕組み。Rustが代表例で、値を別の変数や関数へ渡すと持ち主が移り(move)、元の変数は使えなくなる。を移すか、変更を許すか」を型として表す。借用所有権を移さずに、値を一時的に貸し出して使わせること。Rustでは`&T`(読み取り専用)と`&mut T`(変更可能)で表し、借りている間の書き換えや破棄をコンパイラが検査する。は呼び出し側にも`&` / `&mut`が付くが、moveは印がなく、変数が消費されることは関数の定義を見ないと分からない。所有権値の持ち主を一つに定め、持ち主がいなくなったときに値を破棄する仕組み。Rustが代表例で、値を別の変数や関数へ渡すと持ち主が移り(move)、元の変数は使えなくなる。と可変性を安全に扱うための仕組みであり、どちらも扱わないmelangでは採用しない。 |
| MoonBit | 値渡し(引数は不変) | Ref[T]・可変な構造体など | 印なし | 引数は不変。可変な値は型(Ref・Array)で表される。 引数は不変で、書き換えたい場合は`Ref[T]`や可変な`Array`など、可変な型の値を渡す。渡し方を選ぶ構文はなく、可変性は型で表される。OCamlと同じ方針で、呼び出し側には印が付かない。 |
| Swift | 値渡し(値型はコピー) | inout / borrowing / consuming | inout は &x を付ける | 引数は不変。書き換えるinoutは呼び出し側にも&を付けて示す。 引数は不変で、書き換えを許す`inout`は呼び出し側にも`&`を付けて示す。呼び出しを読むだけで、書き換えられる可能性が分かる。melangは、書き換え自体をなくすことで同じ読みやすさを得る。Swift 5.9以降は`borrowing` / `consuming`で所有権値の持ち主を一つに定め、持ち主がいなくなったときに値を破棄する仕組み。Rustが代表例で、値を別の変数や関数へ渡すと持ち主が移り(move)、元の変数は使えなくなる。に近い渡し方も表せるが、こちらは呼び出し側に印が要らない。 |
| Kotlin | 値渡し(オブジェクトは参照値) | 専用構文なし | 印なし | 引数は再代入できない。渡し方を選ぶ構文はない。 引数は常に`val`相当で再代入できないため、引数自体を書き換えて呼び出し側に影響を与える事故は防げる。一方で、可変なコレクションを渡せば中身は変更できる。melangの「代入そのものを禁止する」方向と近く、引数を共有して渡す点も同じ。ただしmelangは、オブジェクトの中身も不変にする。 |
| Java | 値渡し(オブジェクトは参照値) | 専用構文なし | 印なし | すべて値渡し。オブジェクトは参照の値が渡され、中身は共有される。 引数はすべて値渡しだが、オブジェクトの場合は参照の値がコピーされるため、中身を書き換えると呼び出し側から見える。「値渡しなのに参照のように見える」という誤解を招きやすい。渡し方を選ぶ構文はなく、呼び出し側にも印はない。melangの渡し方はこれに近いが、オブジェクトが不変なので、共有された中身を書き換えられることはない。 |
| Scala | 値渡し(オブジェクトは参照値) | 専用構文なし(名前渡しは=>) | 印なし | 引数は不変。値渡しに加えて、名前渡し(=>)を選べる。 引数は不変で、参照渡しは無い。`=>`による名前渡しは、コピーか参照かではなく「いつ評価するか」を選ぶ仕組みで、melangが扱う「コピーか共有か」とは別の軸。可変なコレクションを渡せば中身を変更できる点はJava・Kotlinと同じ。 |
| C# | 値渡し(参照型は参照値) | ref / out / in | ref・out は呼び出し側にも必須 | ref・out・inを、定義側と呼び出し側の両方に書く先行例。 `ref` / `out`は、定義側と呼び出し側の両方にキーワードを書かないとコンパイルエラーになる。呼び出しを読むだけで、変数が書き換えられる可能性が分かる。書き換えの可能性を呼び出し側に明示する先行例。melangは書き換え自体をなくすため、この印が要らない。ただし`in`は呼び出し側で省略でき、通常の値渡しには印がない。 |
| C++ | 値渡し(コピー) | T& / const T& / T&& | 参照は印なし。moveは std::move | 値・参照・右辺値参照を選べるが、参照渡しは呼び出し側から見えない。 渡し方を最も細かく選べる一方、`a(s)`と`c(s)`は呼び出し側では同じ見た目になる。変数が書き換えられるかは宣言を確認するまで分からず、書き換えの可能性が呼び出し側から見えないという問題は、値を不変にして代入を禁止するmelangでは、そもそも起きない。 |
| C | 値渡し(コピー) | ポインタ(T*) | ポインタは &x を渡すか、既にポインタの値を渡す | すべて値渡し。参照的に扱うにはポインタを明示的に渡す、Go・C++の直接の起源。 引数はすべて値渡しで、参照的に扱いたければアドレスを明示的にポインタとして渡す。Goの「値渡し+明示的なポインタ」はこの設計をほぼそのまま受け継ぎ、C++の参照(`T&`)はポインタを安全に隠す糖衣構文として追加されたものである。配列を関数に渡すと先頭要素へのポインタに「減衰(decay)」するため、関数側では元の配列のサイズを知る手段がなく、別途サイズを渡す必要がある。このサイズ情報を失うポインタ渡しはバッファオーバーランの主要な原因の一つであり、後発言語がスライスや境界チェック付き配列を導入する動機になった。 |
| Go | 値渡し(コピー) | ポインタ(*T) | ポインタは &x を付ける | すべて値渡し。参照的に扱うにはポインタを明示的に渡す。 引数はすべて値渡しで、参照的に扱いたければポインタを渡す。ポインタの受け渡しは呼び出し側にも`&`が付くため、書き換えられる可能性が読み取れる。ただしスライスやmapは値渡しでも中身を共有するため、渡し方が型によって分かりにくくなる。 |
| Zig | 値渡し(構造体等は参照渡しへの最適化あり) | ポインタ(*T)・スライス([]T) | ポインタは &x を付ける | 引数は値渡しで関数内では不変。大きな構造体の受け渡しはABI最適化の対象。 引数は既定で値渡しかつ関数内では不変(const相当)で、書き換えたい場合はローカルの`var`へ明示的にコピーする必要がある。ポインタ・スライスで受け取れば呼び出し側の値を書き換えられ、呼び出し側にも`&`が付く点はGo/Cと同じ。struct/arrayのような大きな集約型は、意味論上は値渡し(コピー)として扱われるが、コンパイラソースコードを解析し、実行可能な形式(機械語や中間表現)に変換するプログラム。静的型付け言語では、実行前にコンパイラが型の不整合などをチェックする役割も担う。がABIレベルの最適化として実体を参照渡しに置き換えることがある——これは言語仕様ではなく実装上の最適化であり、呼び出し側から見える意味論(値が共有されないこと)には影響しない。allocationが必要な標準ライブラリ関数はallocatorを明示的な引数として受け取る慣習があり、隠れたメモリ確保を避ける設計思想の一部になっている。 |
| OCaml | 値渡し(値は基本的に不変) | ref型(可変セル)を渡す | 印なし(型で分かる) | 値は基本的に不変。書き換えたいときはref型の値を渡す。 引数の渡し方を選ぶ構文はなく、書き換えたいときは`ref`型(可変セル)の値を渡す。渡し方ではなく、値の型で可変性を表す設計。呼び出し側は普通の値渡しと見た目が変わらないが、`ref`型の変数を渡していることは型を見れば分かる。可変なデータを持つ設計であり、melangでは採用しない。 |
| F# | 値渡し(値は基本的に不変) | ref型(可変セル)・byrefを渡す | 印なし(byrefのみ&が必要) | OCamlと同じくref型で可変性を表すが、.NET由来のbyref引数も併用できる。 OCamlの`ref`型をそのまま受け継ぎ、書き換えたいときはref型(可変セル)の値を渡す設計が基本。加えて.NETとの相互運用のため`byref<T>`引数も使え、こちらはC#の`ref`同様、呼び出し側にも`&`を書く必要がある。同じ言語の中に「印がない可変性(ref型)」と「印がある可変性(byref)」が両方存在する点が特徴。 |
| Haskell | 遅延評価(値は不変) | なし(処理系が決める) | 印なし | 値がすべて不変なので、渡し方はプログラマが選ばない。 値がすべて不変なので、コピーか参照かはプログラムの結果に影響せず、処理系が決める。melangの「変更可能なデータが存在しない」前提に最も近い言語。melangはこの前提に、Java・Kotlinと同じ「常に共有して渡す」仕様を組み合わせ、コストを予測しやすくする。 |
| Erlang | 値渡し(値は不変) | 同一プロセス内は共有。プロセス間送信はコピー | 印なし | 値がすべて不変。プロセス境界を越えるときだけメッセージとしてコピーされる。 値はすべて不変で、通常の関数呼び出しは同一プロセス内で値を共有して渡すため、渡し方を選ぶ構文はない。一方、プロセス間でメッセージ(`!`)として送るときは、共有メモリを持たないプロセスモデルのため値がコピーされる。「関数呼び出しの引数」と「プロセス間メッセージ」で共有かコピーかが変わる点が、単一プロセスのHaskell・Elixirとの違い。 |
| Elixir | 値渡し(値は不変) | なし(処理系が決める) | 印なし | 値がすべて不変。共有かコピーかは処理系が決める。 値がすべて不変で、変数の再束縛も呼び出し側には影響しない。同一プロセス内では値が共有され、プロセス間では値がコピーされる、といった扱いは処理系が決める。Haskellと同じく、melangの「変更可能なデータが存在しない」前提に近い。 |
| TypeScript | 値渡し(オブジェクトは参照値) | 専用構文なし | 印なし | プリミティブは値、オブジェクトは参照の値が渡される。 JavaScriptと同じ挙動で、渡し方を選ぶ構文はない。`Readonly`型で型検査上は書き換えを禁じられるが、実行時の挙動は変わらない。プリミティブとオブジェクトで扱いが変わるため、型を見ないと渡し方が分からない。 |
| JavaScript | 値渡し(オブジェクトは参照値) | 専用構文なし | 印なし | プリミティブは値、オブジェクトは参照の値が渡される。 プリミティブは値が渡され、オブジェクトは参照の値が渡される。引数の再代入は呼び出し側に影響しないが、オブジェクトの中身の変更は影響する。渡し方を選ぶ構文はなく、値の種類によって暗黙に決まる。 |
| PHP | 値渡し(オブジェクトはハンドル) | &$x(参照渡し) | 印なし(定義側だけ) | 定義側に&を付けると参照渡し。呼び出し側は印が要らない。 定義側に`&`を付けると参照渡しになるが、呼び出し側は印が要らない。`a($x)`と`b($x)`が同じ見た目で、変数が書き換わるかは関数の定義を見ないと分からない。かつては呼び出し側に`&`を書ける仕組みもあったが、現在は廃止されている。 |
| Ruby | オブジェクト参照の値渡し | 専用構文なし | 印なし | オブジェクトへの参照が渡され、破壊的メソッドは呼び出し側に影響する。 Pythonと同様に、オブジェクトへの参照が渡される。再代入は呼び出し側に影響しないが、破壊的メソッド(`<<`, `!`付きのメソッドなど)は影響する。`freeze`で不変にできるが、渡し方としては指定できない。 |
| Python | オブジェクト参照の値渡し | 専用構文なし | 印なし | すべての引数がオブジェクトへの参照。変更可能な型は共有される。 「オブジェクト参照渡し」と呼ばれ、値渡しとも参照渡しとも言い切れない。同じ引数でも、`int`や`str`のような不変な型は影響せず、`list`や`dict`のような可変な型は影響する。可変か不変かを知らないと、呼び出しの結果を予測できない。 |
比較の観点は次の通りである。
- コピーして渡すのか、同じ値を共有して渡すのか。
- 借用所有権を移さずに、値を一時的に貸し出して使わせること。Rustでは`&T`(読み取り専用)と`&mut T`(変更可能)で表し、借りている間の書き換えや破棄をコンパイラが検査する。するのか、所有権を移すのか。
- 渡し方が、関数定義の型・契約に含まれるか。
- 呼び出し側でも、渡し方を明示する必要があるか。
- 変更可能な参照を、別の概念として扱うか。
- 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など)
- 利点: 値の解放や書き換えの競合を、コンパイル時に防げる。GCなしでも安全に扱える。
- 欠点: 所有権・借用・寿命という概念を学ぶ必要があり、設計と記述の負担が大きい。moveは呼び出し側に印がない言語もある。
値がすべて不変で、渡し方を書かない設計(Haskell, Elixirなど)
- 利点: 渡し方を意識せずに書ける。結果が渡し方に依存しないため、コードを読むときの負担も小さい。
- 欠点: コピーか共有かが処理系次第の場合、コストがコードから読み取れず、性能を調整したいときに手が出しにくい。値を更新するときは新しい値を作るため、コストが別の形で現れる。
melangの選択
melangは、Java・Kotlinと同じく引数を常に共有して渡し、そのうえでHaskell・Elixirと同じく値をすべて不変とする。渡し方を指定するsyntaxは設けない。所有権・借用・変更可能な参照は導入しない。
- Pros: 呼び出しも定義も簡潔になる。渡し方を覚える必要がなく、言語の概念が少なく済む。引数が書き換えられないことが言語で保証されるため、関数の定義を見なくても、呼び出しの前後で値が変わらないと読み取れる。常に共有なので、呼び出しのコストが値の大きさに依存せず、見通しやすい。
- Cons: 値のコピーを選べないため、コピーによって独立した領域を持たせるような使い方はできない。値を更新するときは新しい値を作ることになり、コストは更新の側に現れる。参照の同一性を公開する場合は、共有していることが結果に現れるため、扱いを決める必要がある。外部関数との受け渡しなど、渡し方を表現する必要が出る場面が残る可能性がある。
渡し方を選べる自由よりも、渡し方を考えなくてよいことと、値が変わらない保証を優先する。