Scalaの名前渡しについて個人的にしっくりきた説明
ということで、Scalaの名前渡しについて調べていて個人的にしっくりきた説明があったので、まとめてみようと思います。
###参考記事
Scalaの名前渡しについて
初めにScalaの名前渡し(call-by-name)ってなんぞ?ってところから説明します。それは知ってるって人は次の項目にskip
Scalaの名前渡しは公式のドキュメントではこう書いてあります
By-name parameters are evaluated every time they are used. They won’t be evaluated at all if they are unused.
簡単に要約すると
名前渡しの引数はその引数が使われるときに評価される。その引数が使われなければ評価されない。
です。
ほう。
これをきちんと理解するには、まず普通の関数に引数を渡したときにどのような挙動をしているかを知る必要があります。
普通に掛け算を行う関数を作りました。
object workSpace {
var f = 1
var s = 2
def main(args: Array[String]): Unit = {
val res = add(f, s)
println(res) // 2
}
def mul(first: Int, second: Int): Int = {
first * second
}
}
(Scalaではvarは使うべきでないですが、説明のためご了承を、、)
結果は当然 1*2
で2です。では、こうするとどうなるでしょうか。
object workSpace {
var f = 1
var s = 2
def main(args: Array[String]): Unit = {
val res = add(f, s)
println(res) // 2
}
def mul(first: Int, second: Int): Int = {
f = 2
first * second
}
}
first * second
が呼ばれる前にfの値が2になっているから 2*2
で結果は4でしょうか?
結果は 3
です。
これは引数のfirstの値が決まるのが mul関数の呼び出し時
のためです。
なので、結果は3だったというわけです。つまりは値渡しということ。
ここで名前渡しの登場です。関数を次のように書き換えます。
object workSpace {
var f = 1
var s = 2
def main(args: Array[String]): Unit = {
val res = add(f, s)
println(res) // 4
}
def mul(first: => Int, second:Int): Int = {
f = 2
first * second
}
}
first: Int
が first: => Int
に変わりました。これで結果は 4
になります。
これでfirstの値の評価が mul関数の呼び出し時
から firstが使われる時
に変わったということです。なので first * second
でfirstが使われる時の値、つまり f = 2
をした後の値が使われるわけです。
これで初めの定義の意味は分かってもらえたでしょうか??
皆さんも疑問に思ったかも知れませんが、僕はこれをみたときにこれは参照渡しとはどう違うのか、と疑問に思いました。
また、遅延評価とは違うのか?と疑問に思った人もいるかも知れません。
参照渡しとはどう違うのか
まず、参照渡しとの違いです。これは、次の遅延評価との違いにも繋がります。
上の例をみると、僕はfの参照を渡せば行けそうだな、と思いました。
と思ったのですが、そもそもScalaには参照渡しは存在していなかったので、参照渡しなどはできませんでした。(勉強不足でした)
ですが、Scalaという枠を取っ払えば考えられます。
定義的にはこのような違いがあるようです。
名前渡しでは値でも参照でもなく、式がそのまま渡される。
つまり参照渡しでは当然参照が渡される。名前渡しでは式そのままが渡されるということでした。
イメージがつきにくいですね。
参考サイトの二つ目の次の例がわかりやすかったです。
まず、名前渡しを使わなかった例です。
def hello() = {
println("in hello()")
"HELLO"
}
def print(s : String) = {
println("print() start")
println(s)
println("print() end")
}
def greeting() = print(hello())
この結果は以下のようです。
in hello()
print() start
HELLO
print() end
名前渡しではないので、print関数の呼び出し時にhello()が評価されるので、まず in hello()
の出力がされます。
これをこのように書き換えます。
object workSpace {
def main(args: Array[String]): Unit = {
greeting()
}
def hello() = {
println("in hello()")
"HELLO"
}
def print(s : => String) = {
println("print() start")
println(s)
println("print() end")
}
def greeting() = print(hello())
}
結果は以下
print() start
in hello()
HELLO
print() end
sを使うときににhello()が評価されるのでこのようになります。ここの理解としては、関数hello()が呼び出されるときに評価される、でもいいのですが、hello()関数がprint関数内の s
の部分ににコードブロックとして埋め込まれると理解するとわかりやすくなります。
つまりこんな感じです。
object workSpace {
def main(args: Array[String]): Unit = {
print()
}
def print() = {
println("print() start")
println({println("in hello()"); "HELLO"})
println("print() end")
}
}
こう考えると、名前渡しの定義の 式がそのまま渡される
ということも理解しやすくなるのではと思います。
#遅延評価との違い
これも、先ほどのように コードブロックとして埋め込まれる
と理解すると説明ができます。
まず、遅延評価とは次のように考えます。
一旦計算された値はキャッシュをすることが可能であり、遅延プロミスは最大で一度しか計算されないようにすることができる
遅延評価 - Wikipedia
つまり、計算結果がキャッシュされて同じ計算が呼ばれるとキャッシュを使用するというのが遅延評価です。
ここまで来ればもうわかると思いますが、名前渡しの場合コードブロックが埋め込まれるという説明であれば、キャッシュを保持しているわけではなく、呼び出されるたびに演算を行っているため、遅延評価とは異なるということがわかると思います。
最後に
今回はScalaの名前渡しについて自分なりに調べた結果、わからなかった部分の説明をしてみました。まだまだScalaは初心者で間違っているところもあるかも知れないので、もしご指摘があればコメントなどにお願いします。
Scala難しいですね。。