😽

Nimは値渡し?参照渡し?

2021/04/30に公開

はじめに

Nim は値渡しなのでしょうか。
それとも参照渡しなのでしょうか。

その真意を調べるため、 ganariya はダンジョンの奥地に向かいました。

しかし、完全なる実力不足であり、浅瀬をチャプチャプしました。
今回は、そのチャプチャプの結果をまとめます。
(ぜひ有識者のご意見がいただければと思います...)

値渡し・参照渡し

ここでいう値渡しと参照渡しについて Python を参考に自分の考えをまとめます。

まず、 Python はすべて値渡しです。
Python の変数は、メモリ番地を表す id を保存します。(同一性を保証します。)

x = 1000
y = 1000
z = x

"""
4494415248 4494415248 4494415248
"""
print(id(x), id(y), id(z))


a = [10, 20]
b = a
b.append(20)
"""
[10, 20, 20] [10, 20, 20]
4495773568 4495773568
"""
print(a, b)
print(id(a), id(b))

上記のように、整数・配列それぞれに id が設定されており、変数はこの id を保存しています。
そして id の番地に格納されるオブジェクトが、変更可能である場合があり、それが配列や辞書・クラスのインスタンスになっています。
そのため、配列や辞書はある意味 参照渡し としてまるで他の変数に参照を渡している可能ように見えます。
その実態は、メモリ番地である id を値コピーして渡しているのみです。

よって、値渡しは値をコピーすることと考えています。
そして、参照渡しは、「変更可能であるオブジェクトのメモリ番地」を値コピーして渡していると考えています。

Nim

基本

Nim では、var, let, const すべて python でいう deep copy になっています。
mutable immutable かかわらず、すべてのネストまで潜って各をそれぞれコピーして渡します。
そのため、非常に大きい配列などを参照渡しの気分で渡すと、おそらくかなりの時間を食います。(すべてコピーしているため)

Python の気分で書くとだいぶ挙動が異なります。
すべてコピーなのは最初びっくりしました。(seq などは参照だと思っていたため。)

var x = 10
var y = x
x = 20

# 2010
echo x, y

var a = "hello"
var b = a
a[0] = 'x'

# xellohello
echo a, b

関数

関数では、何もつけないで型だけを書くと immutable な変数となり、すべて値がコピーされて渡されます。
そのため、以下の例のように a[0] を書き換えようとするとエラーとなります。

有志の方に、関数における変数渡しの挙動について教えていただきました。

関数では、varref をつけないで型だけを書くと immutable な変数となり書き換えることができません
ただし値をすべてコピーする値渡しになるのか、変数のメモリをポインタのように渡す参照渡しになるのかは、実行時に決まるそうです。
渡す変数のサイズが大きければ参照渡しになり、サイズが小さければ値渡しになります。
このような挙動を取ることによって、動作がより高速になると考えられます。
どちらにしろ、型のみを書いて関数を呼ぶ場合は、安全のために関数内部で変数の値を変更できません。

import sequtils, strutils, algorithm

proc f(a: seq[int]) =
  # a[0] = 100
  # this is error.
  discard

var a = @[1, 2, 3]

一方 var をつけて渡すと、変更が可能となります。
関数内で変更を加えると、それが呼び出し側にも反映されます

proc f1(x: var int) =
  x = 20

proc f2(x: var seq[int]) =
  x[0] = 100

var x = 10
f1(x)
# 20
echo x

var y = @[1, 2, 3]
f2(y)
# @[100, 2, 3]
echo y

ポインタ

ここまでの内容では、 proc 以外で参照(メモリ)を渡すことが難しそうです。
そこで、 Nim にはポインタがあります。

同じ参照先を持ちたい場合は ref もしくは ptr を利用します。
ref はガベージコレクションが自動で行われますが、 ptr は自前で行う必要があります。
ここはおとなしく ref を使うことにしましょう。

ref T は T 型のポインタを作ります。
ただし、初期段階ではポインタの向く先のメモリがないため、 new T でメモリの実体を作成します。
そして、x[] のようにしてアドレスの番地のオブジェクトを取得します。

# int *x = (int *)malloc(sizeof(int))
var x: ref int = new int

# int *y = x
var y: ref int = x

# *x = 100
x[] = 100

# *x *y
# 100100
echo x[], y[]

C 言語でいう &x のようなアドレスは x.addr で取得できます。
ただし、そのアドレスを保存できるのは ptr のみです。
ref には保存できないみたいです。

var a = 10
# ptr 0x10ec110c0 --> 10
echo a.addr.repr

# error
# var b: ref int = a.addr

var c: ptr int = a.addr
c[] = 1000
# 1000 1000
echo a, " ", c[]

まとめ

ここまで見て Nim は以下のように見えています。

  • 基本的に deep copy で、すべて値をコピーして渡す
    • そのため、重い配列やオブジェクトを Python の気持ちで書くと痛い目を見る
  • 関数で var をつけると C++ の参照型や Python の参照渡しに近い挙動をする
  • ref ptr はポインタ型
    • new T で T 型のオブジェクトを作ってメモリを取得する
    • x[] でポインタの指すアドレスのオブジェクトを取得する
    • x.addr でアドレスが取得でき、 ptr 型にのみ保存できる

初心者が書いているので、ぜひ間違っているところやより正しい知識を教えていただけるとうれしいです。
お読みいただき、ありがとうございました。

リンク

GitHubで編集を提案

Discussion