PythonとRubyは似ていると言ったな。あれは嘘だ。
def f(x = []):
x.append(0)
return x
print(f())
print(f())
def f(x = [])
x.push(0)
return x
end
p f()
p f()
上のPythonとRubyのコードは一見同じ処理をしているように見えます。しかし、実際に実行するとその結果は異なります。
[0]
[0, 0]
[0]
[0]
一体何が起きたのでしょうか?
Pythonはデフォルト値
少しコードを変えて、関数呼出しにしてみましょう。どのタイミングで呼び出されたかわかるように、適当にprint()
を付けておきます。
def g():
print("default!")
return []
def f(x = g()):
x.append(0)
return x
print("start!")
print(f())
print(f())
print(f([1]))
default!
start!
[0]
[0, 0]
[1, 0]
結果からわかるようにg()
が評価されたのはスタートの手前、def f(...): ...
が読み込まれたところです。そう、Pythonで引数のデフォルトを書いた場合、関数定義の時点で評価されます。式が評価された後に残るのは評価値だけです。つまり、f()
が定義された時点で、デフォルトの値は決定されており、既にもう変わることはありません。そしてこの値をデフォルト値といいます。デフォルトになるのは、あくまで値であり、どのような式が書いてあっても、評価された後の値だけが残ります。評価されるタイミングも、関数呼び出しでは無く、関数定義時です。
Rubyはデフォルト式
続いて、Rubyも同じようなコードを書いて見ましょう。
def g()
puts 'default!'
return []
end
def f(x = g())
x.push(0)
return x
end
puts 'start!'
p f()
p f()
p f([1])
start!
default!
[0]
default!
[0]
[1, 0]
g()
が二回呼び出されています。それも、f()
を呼び出した直後にです。そう、Rubyで引数のデフォルトを書いた場合、(引数が指定されていなければ)関数呼び出しの時点で評価されます1。そして、その評価値がデフォルトの値として使われます。注意してなければならないのは、関数呼び出しが行われる度に、そのデフォルトが使われるのであれば、式が評価されると言うことです。そしてこの式をデフォルト式といいます。デフォルトになるのは、あくまで式であり、前回の結果がどのような値であったとしても、毎回評価された後の値が使われます2。評価されるタイミングは、関数呼び出しの時であり、関数定義時に一切評価されることはありません。また、引数が指定され、デフォルト式が使われない場合は、その式の評価自体がなされません。
デフォルト値 vs デフォルト式
ということで、似ているとか、パクリだとか、そんなことを言われている言語間でも細かい違いはあるものです。どちらが良い悪いと言う話ではなく、この違いは言語設計の思想の違いのようなものだと思います。
他の言語についても、さらっと調べてみました。
- デフォルト値
- Python
- デフォルト式
- C++
- D
- Crystal
- Elixir
- Scala
- Kotlin
- Ruby
- JavaScript (ECMAScript 2015+)
- TypeScript
- CoffeeScript
- デフォルト固定値(定数式または定数式に準ずる値型のみ、後述参照)
- C#
- PHP
- 未検証(保留)
- Common Lisp
- 構文無し
- C
- Go
- Java
- Haskell
- Scheme
- Clojure
引数の個数によるオーバーロードやパターンマッチ、レコードやマップを渡してデフォルト引数もどきにできる場合は除外してます。引数を取るコードに対し、デフォルト値/式を追加するだけで成立する構文が対象です。「構文無し」となっている言語について、引数の省略(オプショナル引数)ができないわけではありません。
デフォルト固定値(この名称は、今、私が名付けたので、正しい呼び方かはわかりません)の部分は定数式または定数式に準ずる式のみが記載され、その評価値が値型となるものです。デフォルト固定値を使う場合は値はコピーされる3ため呼出し毎に同じオブジェクトを使うと言うことはありません。仕様上、評価のタイミングはいつでも良い事になりますが、実装上は、他の定数式と同じくコンパイル時と推測されます。この部分は自信が無いので、誰か補足をしてくれると助かります。
あとで調べて追加するかもしれないもの。(もし、既に調べた方がいましたら、実証コードおよび結果を含めてコメントをくれると助かります。下記以外の言語でもかまいません。)
Rust、Swift、Perl6、F#、VB.NET、Erlang、Lua、D、Perl (5.x)、Objective-C、Delphi、OCaml
-
Rubyでは全てがメソッドであり、関数は存在しません。関数形式のメソッド呼出しはありますが、関数呼び出しという言葉は正確ではありません。ただ、この文章ではPythonや他言語と用語を合わせるために、メソッド呼出しを含めて関数呼び出しという言葉を使っています。関数定義という言葉についても同様です。 ↩
-
一部のイミュータブルなオブジェクトになるリテラルは、内部でキャッシュを持っており、再利用される場合があります。キャッシュを使ったかどうかは言語仕様上は区別されません。 ↩
-
言語仕様上はコピーされることになりますが、PHPのCopy-on-writeの仕組みのように、実際は値の変更が行われるまではメモリ上のデータをコピーしない場合もあります。 ↩