Scalaでは名前で呼び出すか値で呼び出すか、説明が必要


239

私が理解しているように、Scalaでは、関数は次のいずれかで呼び出されます。

  • 値渡しまたは
  • 名前で

たとえば、次の宣言を前提として、関数がどのように呼び出されるかを知っていますか?

宣言:

def  f (x:Int, y:Int) = x;

コール

f (1,2)
f (23+55,5)
f (12+3, 44*11)

ルールは何ですか?

回答:


540

あなたが与えた例では、値による呼び出しのみを使用しているので、違いを示す新しい、より簡単な例を示します。

まず、副作用のある関数があるとしましょう。この関数は何かを出力してからを返しますInt

def something() = {
  println("calling something")
  1 // return value
}

次にInt、1つが値による呼び出しスタイル(x: Int)で引数を取り、もう1つが名前による呼び出しスタイル()で引数を取ることを除いて、まったく同じ引数を受け入れる2つの関数を定義しますx: => Int

def callByValue(x: Int) = {
  println("x1=" + x)
  println("x2=" + x)
}

def callByName(x: => Int) = {
  println("x1=" + x)
  println("x2=" + x)
}

次に、副作用関数でそれらを呼び出すとどうなりますか?

scala> callByValue(something())
calling something
x1=1
x2=1

scala> callByName(something())
calling something
x1=1
calling something
x2=1

つまり、値渡しバージョンでは、渡された関数呼び出しの副作用(something())のが1回だけ発生した。ただし、名前による呼び出しバージョンでは、副作用が2回発生しました。

これは、値渡し関数は、関数を呼び出す前に、渡された式の値を計算するため、毎回同じ値にアクセスするためです。代わりに、名前による呼び出し関数は、アクセスされるたびに、渡された式の値を再計算します。


296
私はいつもこの用語が不必要に混乱していると思っていました。関数は、名前による呼び出しと値による呼び出しのステータスが異なる複数のパラメーターを持つことができます。それがあることではありませんので、関数がコールバイ名またはコールバイ値であり、それは、そのそれぞれのことだパラメータができるパス -by-名をか値渡し。さらに、「コール・バイ・ネーム」とは何の関係もありません名前を=> Int異なるタイプからInt。それは、Int「生成する引数のない関数」対単なるIntです。ファーストクラスの関数を取得したら、これを説明するために名前による呼び出しの用語を考案する必要ありません。
ベン

2
@Ben、それはいくつかの質問に答えるのに役立ちます、ありがとう。もっと多くの記事で、名前渡しの意味を明確に説明してほしいと思います。
Christopher Poile 2013年

3
@SelimOberテキストf(2)がtypeの式としてコンパイルされる場合、Int生成されたコードはf引数付きで呼び出され2、結果は式の値になります。同じテキストがタイプの式としてコンパイルされる=> Int場合、生成されたコードは、ある種の「コードブロック」への参照を式の値として使用します。どちらの方法でも、その型の値は、その型のパラメーターを必要とする関数に渡すことができます。私はあなたが変数の割り当てでこれを行うことができると確信しています。では、名前や呼び出しはそれと何の関係があるのでしょうか?
ベン

4
@Benでは、=> Int「Intを生成する引数のない関数」である場合、どのように異なるの() => Intでしょうか。Scalaはこれらを異なる方法で処理するように見えます。たとえば、=> Intどうやらval、パラメーターの型としてだけではなく、の型として機能しないようです。
ティムグッドマン

5
@TimGoodmanそうですね、私が考えたよりも少し複雑です。=> Intは便利であり、関数オブジェクトとまったく同じようには実装されていません(おそらく、型の変数を使用できない理由ですが=> Int、これが機能しなかった根本的な理由はありません)。() => Int明示的に返され、引数なしの関数でInt明示的に呼び出される必要があり、機能として渡すことができ、。=> Intは一種の「プロキシInt」であり、それを使用して実行できる唯一のことは、それを(暗黙的に)呼び出すことIntです。
ベン

51

Martin Oderskyの例を次に示します。

def test (x:Int, y: Int)= x*x

これらの条件では、評価戦略を調べて、どちらが速いか(ステップが少ない)を判断します。

test (2,3)

値による呼び出し:test(2,3)-> 2 * 2-> 4
名前による呼び出し:test(2,3)-> 2 * 2-> 4
ここでは、同じステップ数で結果が得られます。

test (3+4,8)

値による呼び出し:test(7,8)-> 7 * 7-> 49
名前による呼び出し:(3 + 4)(3 + 4)-> 7(3 + 4)-> 7 * 7-> 49
ここで呼び出し値ではより高速です。

test (7,2*4)

値による呼び出し:test(7,8)-> 7 * 7-> 49
名前による呼び出し:7 * 7-> 49
ここでの名前による呼び出しの方が高速です

test (3+4, 2*4) 

値による呼び出し:test(7,2 * 4)-> test(7、8)-> 7 * 7-> 49
名前による呼び出し:(3 + 4)(3 + 4)-> 7(3 + 4) -> 7 * 7-> 49
同じステップで結果が得られます。


1
CBVの3番目の例では、test(7,14)ではなくtest(7,8)を意味していたと思います
talonx

1
例は、Scalaプログラミングの原則であるCourseraから取得しています。講義1.2。名前による呼び出しdef test (x:Int, y: => Int) = x * xでは、パラメータyは決して使用されないことに注意してください。
ジェリー博士2016年

1
良い例え!Coursera MOOCから
取得

これは違いの良い説明ですが、質問されている質問、つまり2つのうちどちらがScala呼び出しであるかについては
触れて

16

この例の場合、値によってのみ定義しいるため、すべてのパラメーターは、関数で呼び出されるに評価されます。パラメータを名前で定義する場合は、コードブロックを渡す必要があります。

def f(x: => Int, y:Int) = x

この方法では、パラメータxは評価されるまでに、関数で呼び出される。

ここのこの小さな投稿もこれをうまく説明しています。


10

上記のコメントでの@Benのポイントを繰り返すために、「名前による呼び出し」を単なる構文上の砂糖として考えるのが最善だと思います。パーサーは式を無名関数でラップするだけなので、後で使用されたときにそれらを呼び出すことができます。

実際には、定義する代わりに

def callByName(x: => Int) = {
  println("x1=" + x)
  println("x2=" + x)
}

そして実行中:

scala> callByName(something())
calling something
x1=1
calling something
x2=1

次のように書くこともできます:

def callAlsoByName(x: () => Int) = {
  println("x1=" + x())
  println("x2=" + x())
}

同じ効果を得るには、次のように実行します。

callAlsoByName(() => {something()})

calling something
x1=1
calling something
x2=1

私はあなたが意味したと思います:<!-言語:lang-scala-> def callAlsoByName(x:()=> Int)= {println( "x1 =" + x())println( "x2 =" + x( ))}次に:<!-言語:lang-js-> callAlsoByName(()=> something())この最後の呼び出しでは、something()を中括弧で囲む必要はないと思います。注:私はあなたの回答を編集しようとしましたが、編集者はコメントまたは別の回答である必要があるとレビュアーが拒否しました。
lambdista 2015

どうやらコメントで構文の強調表示を使用できないので、「<!-language:lang-scala->」の部分は無視してください。私は自分のコメントを編集したはずですが、5分以内にしか編集できません。:)
lambdista 2015

1
私も最近これに遭遇しました。このように考えることは概念的には問題ありませんが、Scalaはとを区別=> T() => Tます。最初のタイプをパラメーターとして取る関数は2番目を受け入れません。scalaは@ScalaSignature、これに対してコンパイル時エラーをスローするのに十分な情報をアノテーションに格納します。両方のためのバイトコード=> T() => Tいえ同じであるとされますFunction0。詳細については、この質問を参照してください。
vsnyc 2016年

6

例を示すだけでなく、簡単な使用例で説明しようと思います

あなたが最後にうんざりして以来、毎回あなたをナグする「ナガーアプリ」を構築したいと想像してください。

次の実装を調べます。

object main  {

    def main(args: Array[String]) {

        def onTime(time: Long) {
            while(time != time) println("Time to Nag!")
            println("no nags for you!")
        }

        def onRealtime(time: => Long) {
            while(time != time) println("Realtime Nagging executed!")
        }

        onTime(System.nanoTime())
        onRealtime(System.nanoTime())
    }
}

上記の実装では、名前による受け渡しの場合にのみnaggerが機能します。理由は、値による受け渡しの場合は再利用されるため、値による再評価は行われず、名前による受け渡しの場合は値がすべて再評価されるためです。変数がアクセスされた時間


4

通常、関数のパラメーターは値渡しパラメーターです。つまり、パラメーターの値は、関数に渡される前に決定されます。しかし、関数内で呼び出されるまで評価したくない式をパラメーターとして受け入れる関数を作成する必要がある場合はどうでしょうか。このような場合のために、Scalaは名前による呼び出しパラメーターを提供します。

名前による呼び出しメカニズムがコードブロックを呼び出し先に渡し、呼び出し先がパラメーターにアクセスするたびに、コードブロックが実行され、値が計算されます。

object Test {
def main(args: Array[String]) {
    delayed(time());
}

def time() = {
  println("Getting time in nano seconds")
  System.nanoTime
}
def delayed( t: => Long ) = {
  println("In delayed method")
  println("Param: " + t)
  t
}
}
 1. C:/> scalac Test.scala 
 2. scalaテスト
 3.遅延法で
 4.ナノ秒単位で時間を取得する
 5.パラメータ:81303808765843
 6.時間をナノ秒で取得する

2

私が想定しているように、上記のcall-by-value関数は値だけを関数に渡します。Martin Oderskyそれによると、関数の評価で重要な役割を果たすScalaが後に続く評価戦略です。しかし、それを簡単にするcall-by-name。これは、としても知られているメソッドへの引数として関数を渡すようなものHigher-Order-Functionsです。メソッドが渡されたパラメーターの値にアクセスすると、渡された関数の実装が呼び出されます。以下のように:

@dhgの例によると、まず次のようにメソッドを作成します。

def something() = {
 println("calling something")
 1 // return value
}  

この関数には1つのprintlnステートメントが含まれ、整数値を返します。引数をaとして持つ関数を作成しますcall-by-name

def callByName(x: => Int) = {
 println("x1=" + x)
 println("x2=" + x)
}

この関数パラメーターは、1つの整数値を返す無名関数を定義します。これにxは、0引数を渡したが戻りint値を持つ関数の定義が含まれ、something関数には同じシグネチャが含まれています。関数を呼び出すときに、関数を引数としてに渡しますcallByName。しかし、call-by-valueそれだけの場合、整数値を関数に渡します。以下のように関数を呼び出します。

scala> callByName(something())
 calling something
 x1=1
 calling something
 x2=1 

この場合、somethingメソッドは2回呼び出されました。xcallByName方法のdefintionへの呼び出し、something方法。


2

ここでの多くの回答で説明されているように、値による呼び出しは一般的な使用例です。

名前による呼び出しは、コードブロックを呼び出し元に渡し、呼び出し元がパラメーターにアクセスするたびに、コードブロックが実行され、値が計算されます。

以下のユースケースを使用して、名前による呼び出しをより簡単な方法で示すようにします

例1:

名前による呼び出しの簡単な例/使用例は関数の下にあり、関数をパラメーターとして取り、経過時間を示します。

 /**
   * Executes some code block and prints to stdout the 
time taken to execute   the block 
for interactive testing and debugging.
   */
  def time[T](f: => T): T = {
    val start = System.nanoTime()
    val ret = f
    val end = System.nanoTime()

    println(s"Time taken: ${(end - start) / 1000 / 1000} ms")

    ret
  }

例2:

apache spark(scalaを使用)は、名前による呼び出しを使用したロギングを使用しますLogging。以下のメソッドからかどうかを遅延評価する特性参照してくださいlog.isInfoEnabled

protected def logInfo(msg: => String) {
     if (log.isInfoEnabled) log.info(msg)
 }

2

値によってコール、式の値は、関数呼び出し時に予め計算され、その特定の値は、対応する関数へのパラメータとして渡されます。関数全体で同じ値が使用されます。

一方、名前による呼び出しでは、式自体がパラメーターとして関数に渡され、その特定のパラメーターが呼び出されるたびに、関数内でのみ計算されます。

Scalaでの名前による呼び出しと値による呼び出しの違いは、以下の例でよりよく理解できます。

コードスニペット

object CallbyExample extends App {

  // function definition of call by value
  def CallbyValue(x: Long): Unit = {
    println("The current system time via CBV: " + x);
    println("The current system time via CBV " + x);
  }

  // function definition of call by name
  def CallbyName(x: => Long): Unit = {
    println("The current system time via CBN: " + x);
    println("The current system time via CBN: " + x);
  }

  // function call
  CallbyValue(System.nanoTime());
  println("\n")
  CallbyName(System.nanoTime());
}

出力

The current system time via CBV: 1153969332591521
The current system time via CBV 1153969332591521


The current system time via CBN: 1153969336749571
The current system time via CBN: 1153969336856589

上記のコードスニペットでは、関数呼び出しCallbyValue(System.nanoTime())の場合、システムのナノ時間は事前に計算され、その事前計算された値は関数呼び出しにパラメーターが渡されています。

しかし、CallbyName(System.nanoTime())では関数呼び出しでは、式 "System.nanoTime())"自体がパラメーターとして関数呼び出しに渡され、そのパラメーターが関数内で使用されると、その式の値が計算されます。 。

CallbyName関数の関数定義に注目してください。ここでは、パラメーターxとそのデータ型を分離する=>記号があります。その特定の記号は、関数が名前タイプによる呼び出しであることを示しています。

つまり、値による呼び出し関数の引数は関数に入る前に一度評価されますが、名前による呼び出し関数の引数は、必要な場合にのみ関数内で評価されます。

お役に立てれば!


2

これは、現在Scalaコースを受講している同僚を助けるためにコーディングした簡単な例です。私が興味深いと思ったのは、マーティンが講演の前半で紹介した&&質問の回答を例として使用しなかったことです。とにかくこれがお役に立てば幸いです。

val start = Instant.now().toEpochMilli

val calc = (x: Boolean) => {
    Thread.sleep(3000)
    x
}


def callByValue(x: Boolean, y: Boolean): Boolean = {
    if (!x) x else y
}

def callByName(x: Boolean, y: => Boolean): Boolean = {
    if (!x) x else y
}

new Thread(() => {
    println("========================")
    println("Call by Value " + callByValue(false, calc(true)))
    println("Time " + (Instant.now().toEpochMilli - start) + "ms")
    println("========================")
}).start()


new Thread(() => {
    println("========================")
    println("Call by Name " + callByName(false, calc(true)))
    println("Time " + (Instant.now().toEpochMilli - start) + "ms")
    println("========================")
}).start()


Thread.sleep(5000)

コードの出力は次のようになります。

========================
Call by Name false
Time 64ms
========================
Call by Value false
Time 3068ms
========================

1

パラメータは通常値渡しです。つまり、関数本体で置換される前に評価されます。

関数を定義するときに二重矢印を使用すると、パラメータを名前で強制的に呼び出すことができます。

// first parameter will be call by value, second call by name, using `=>`
def returnOne(x: Int, y: => Int): Int = 1

// to demonstrate the benefits of call by name, create an infinite recursion
def loop(x: Int): Int = loop(x)

// will return one, since `loop(2)` is passed by name so no evaluated
returnOne(2, loop(2))

// will not terminate, since loop(2) will evaluate. 
returnOne(loop(2), 2) // -> returnOne(loop(2), 2) -> returnOne(loop(2), 2) -> ... 

1

インターネットには、この質問に対する素晴らしい答えがすでにたくさんあります。誰かが役立つと思う場合に備えて、トピックについて収集したいくつかの説明と例をまとめて書きます

前書き

値による呼び出し(CBV)

通常、関数のパラメーターは値渡しパラメーターです。つまり、関数自体が評価される前に、パラメーターが左から右に評価されて値が決定されます。

def first(a: Int, b: Int): Int = a
first(3 + 4, 5 + 6) // will be reduced to first(7, 5 + 6), then first(7, 11), and then 7

名前による呼び出し(CBN)

しかし、関数内で呼び出されるまで評価しない式をパラメーターとして受け入れる関数を作成する必要がある場合はどうでしょうか。このような状況のために、Scalaは名前による呼び出しパラメーターを提供します。パラメータがそのまま関数に渡され、代入後に評価が行われることを意味します

def first1(a: Int, b: => Int): Int = a
first1(3 + 4, 5 + 6) // will be reduced to (3 + 4) and then to 7

名前による呼び出しメカニズムがコードブロックを呼び出しに渡し、呼び出しがパラメーターにアクセスするたびに、コードブロックが実行され、値が計算されます。次の例では、delayedは、メソッドが入力されたことを示すメッセージを出力します。次に、delayedはその値とともにメッセージを出力します。最後に、delayedは 't'を返します。

 object Demo {
       def main(args: Array[String]) {
            delayed(time());
       }
    def time() = {
          println("Getting time in nano seconds")
          System.nanoTime
       }
       def delayed( t: => Long ) = {
          println("In delayed method")
          println("Param: " + t)
       }
    }

遅延法
ではナノ秒で時間を取得し
ますパラメータ:2027245119786400

各ケースの長所と短所

CBN: +頻繁に終了します*終了より上でチェック* +関数本体の評価で対応するパラメーターが使用されていない場合、関数の引数が評価されないという利点があります-遅いため、より多くのクラスが作成されます(プログラムがロードに時間がかかります)、より多くのメモリを消費します。

CBV: + CBNよりも指数関数的に効率的であることがよくあります。これは、名前によって呼び出される引数式のこの繰り返しの再計算を回避するためです。すべての関数の引数を1回だけ評価します。+式が評価されるタイミングがよくわかるので、命令型の効果と副作用の方がはるかに優れています。-パラメータ評価中にループが発生する可能性があります*上記の終了を確認してください*

終了が保証されない場合はどうなりますか?

-式eのCBV評価が終了すると、eのCBN評価も終了します-他の方向は真ではありません

非終了の例

def first(x:Int, y:Int)=x

式first(1、loop)を考えます。

CBN:first(1、loop)→1 CBV:first(1、loop)→この式の引数を減らします。1つはループであるため、引数を無限に減らします。終わらない

各ケースの動作の違い

になるメソッドテストを定義しましょう

Def test(x:Int, y:Int) = x * x  //for call-by-value
Def test(x: => Int, y: => Int) = x * x  //for call-by-name

Case1テスト(2、3)

test(2,3)2*24

評価済みの引数から始めるので、値による呼び出しと名前による呼び出しのステップ数は同じになります

Case2テスト(3 + 4,8)

call-by-value: test(3+4,8) → test(7,8)7 * 749
call-by-name: (3+4)*(3+4)7 * (3+4)7 * 749

この場合、値渡しはより少ないステップを実行します

Case3テスト(7、2 * 4)

call-by-value: test(7, 2*4) → test(7,8)7 * 749
call-by-name: (7)*(7)49

2番目の引数の不要な計算を回避します

Case4テスト(3 + 4、2 * 4)

call-by-value: test(7, 2*4) → test(7,8)7 * 749
call-by-name: (3+4)*(3+4)7*(3+4)7*749

異なるアプローチ

まず、副作用のある関数があるとしましょう。この関数は何かを出力してからIntを返します。

def something() = {
  println("calling something")
  1 // return value
}

次に、1つが値による呼び出しスタイル(x:Int)で引数を取り、もう1つが名前による呼び出しスタイル(x: => Int)。

def callByValue(x: Int) = {
  println("x1=" + x)
  println("x2=" + x)
}
def callByName(x: => Int) = {
  println("x1=" + x)
  println("x2=" + x)
}

次に、副作用関数でそれらを呼び出すとどうなりますか?

scala> callByValue(something())
calling something
x1=1
x2=1
scala> callByName(something())
calling something
x1=1
calling something
x2=1

したがって、値渡しバージョンでは、渡された関数呼び出し(something())の副作用が1回だけ発生したことがわかります。ただし、名前による呼び出しバージョンでは、副作用が2回発生しました。

これは、値渡し関数は、関数を呼び出す前に、渡された式の値を計算するため、毎回同じ値にアクセスするためです。ただし、名前による呼び出し関数は、アクセスされるたびに、渡された式の値を再計算します。

CALL-BY-NAMEを使用した方が良い例

送信元:https : //stackoverflow.com/a/19036068/1773841

単純なパフォーマンスの例:ロギング。

次のようなインターフェースを想像してみましょう:

trait Logger {
  def info(msg: => String)
  def warn(msg: => String)
  def error(msg: => String)
}

そして、このように使用されます:

logger.info("Time spent on X: " + computeTimeSpent)

infoメソッドが何もしない場合(たとえば、ロギングレベルがそれより高く設定されているため)、computeTimeSpentが呼び出されることはなく、時間を節約できます。これはロガーでよく起こります。ロガーでは、文字列操作が頻繁に見られます。これは、ログに記録されているタスクに比べて高くつく可能性があります。

正当性の例:論理演算子。

おそらく次のようなコードを見たことがあるでしょう。

if (ref != null && ref.isSomething)

次のように&&メソッドを宣言するとします。

trait Boolean {
  def &&(other: Boolean): Boolean
}

次に、refがnullの場合は常に、isSomethingが&&に渡される前にnull参照で呼び出されるため、エラーが発生します。このため、実際の宣言は次のとおりです。

trait Boolean {
  def &&(other: => Boolean): Boolean =
    if (this) this else other
}

1

例を見ると、違いをよりよく理解するのに役立つはずです。

現在の時刻を返す簡単な関数を定義しましょう:

def getTime = System.currentTimeMillis

次に、1秒遅れて2回印刷する関数を名前で定義します。

def getTimeByName(f: => Long) = { println(f); Thread.sleep(1000); println(f)}

そしてによるもの

def getTimeByValue(f: Long) = { println(f); Thread.sleep(1000); println(f)}

次に、それぞれを呼び出します。

getTimeByName(getTime)
// prints:
// 1514451008323
// 1514451009325

getTimeByValue(getTime)
// prints:
// 1514451024846
// 1514451024846

結果は違いを説明するはずです。スニペットはこちらから入手できます


0

CallByName使用時にcallByValue呼び出され、ステートメントが検出されるたびに呼び出されます。

例えば:-

私は無限ループを持っています。つまり、この関数を実行すると、scalaプロンプトが表示されることはありません。

scala> def loop(x:Int) :Int = loop(x-1)
loop: (x: Int)Int

callByNameこの関数は、上記取るloop引数としてメソッドと、それはその体内で使用されることはありません。

scala> def callByName(x:Int,y: => Int)=x
callByName: (x: Int, y: => Int)Int

callByNameメソッドの実行ではscala、関数内でループ関数を使用していないため、問題は見つかりません(プロンプトが返されます)callByName

scala> callByName(1,loop(10))
res1: Int = 1
scala> 

callByValue関数は上記のloopメソッドをパラメーターとして受け取り、関数または式の内部で評価loopされた後、再帰的に実行される関数によって外部関数が実行され、scalaプロンプトが返されることはありません。

scala> def callByValue(x:Int,y:Int) = x
callByValue: (x: Int, y: Int)Int

scala> callByValue(1,loop(1))

0

これを見てください:

    object NameVsVal extends App {

  def mul(x: Int, y: => Int) : Int = {
    println("mul")
    x * y
  }
  def add(x: Int, y: Int): Int = {
    println("add")
    x + y
  }
  println(mul(3, add(2, 1)))
}

y:=> Intは名前で呼び出されます。名前による呼び出しとして渡されるものはadd(2、1)です。これは遅延評価されます。したがって、コンソールの出力は「mul」の後に「add」が続きますが、addが最初に呼び出されているようです。名前による呼び出しは、関数ポインタを渡すようなものです。
y:=> Intからy:Intに変更します。コンソールに「追加」と「マルチ」が表示されます。通常の評価方法。


-2

ここでのすべての答えが正しい正当化を行うとは思いません:

値による呼び出しでは、引数は一度だけ計算されます。

def f(x : Int, y :Int) = x

// following the substitution model

f(12 + 3, 4 * 11)
f(15, 4194304)
15

上記のように、必要でないかどうかにかかわらず、すべての引数が評価されることがわかります。通常call-by-valueは高速ですが、この場合のように常にではありません。

評価戦略がcall-by-name次の場合、分解は次のようになります。

f(12 + 3, 4 * 11)
12 + 3
15

上記を見るとわかるように、評価する必要がなかったため、4 * 11場合によっては有益かもしれない計算を少し節約しました。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.