在 Scala 中按名称调用与按值调用,需要澄清

Posted

技术标签:

【中文标题】在 Scala 中按名称调用与按值调用,需要澄清【英文标题】:Call by name vs call by value in Scala, clarification needed 【发布时间】:2021-09-11 04:34:26 【问题描述】:

据我了解,在 Scala 中,可以调用函数

按值或 按名称

例如,给定以下声明,我们是否知道函数将如何被调用?

声明:

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

打电话

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

请问有什么规则?

【问题讨论】:

【参考方案1】:

您给出的示例仅使用按值调用,因此我将提供一个新的、更简单的示例来显示差异。

首先,假设我们有一个带有副作用的函数。这个函数打印出一些东西然后返回一个Int

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

现在我们要定义两个函数,它们接受 Int 参数,除了一个以值调用样式 (x: Int) 和另一个以调用方式接收参数之外,它们完全相同。名称样式 (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())的副作用只发生了一次。但是,在点名版本中,副作用发生了两次。

这是因为按值调用函数在调用函数之前计算传入表达式的值,因此每次都访问相同的值。相反,按名称调用函数重新计算每次访问传入的表达式的值。

【讨论】:

我一直认为这个术语是不必要的混乱。一个函数可以有多个参数,它们的名称调用和值调用状态各不相同。所以不是函数是按名称调用或按值调用,而是它的每个参数都可能是pass-按名称或按值传递。此外,“按名称调用”与 names 无关。 => Int 是与 Int 不同的类型;它是“没有参数的函数将生成Int”而不是Int。一旦您拥有一流的功能,您就不需要发明名称调用术语来描述这一点。 @Ben,这有助于回答几个问题,谢谢。我希望更多的文章能清楚地解释 pass-by-name 的语义。 @SelimOber 如果文本f(2) 被编译为Int 类型的表达式,则生成的代码调用f 并带有参数2,结果是表达式的值。如果相同的文本被编译为=> Int 类型的表达式,则生成的代码使用对某种“代码块”的引用作为表达式的值。无论哪种方式,该类型的值都可以传递给期望该类型参数的函数。我很确定你可以通过变量赋值来做到这一点,而且看不到参数传递。那么名字或称呼与它有什么关系呢? @Ben 那么如果=> Int 是“生成Int 的无参数函数”,它与() => Int 有何不同? Scala 似乎以不同的方式对待这些,例如 => Int 显然不能作为 val 的类型,只能作为参数的类型。 @TimGoodman 你是对的,它比我想象的要复杂一些。 => Int 是一种便利,它并没有完全按照函数对象的方式实现(大概是为什么你不能拥有=> Int 类型的变量,尽管没有根本原因导致它无法工作)。 () => Int显式 一个没有参数的函数,它将返回一个Int,需要显式调用并且可以作为函数传递。 => Int 有点像“代理 Int”,你唯一能做的就是调用它(隐式)来获取 Int【参考方案2】:

这是 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 在相同的步骤内达到结果。

【讨论】:

在 CBV 的第三个示例中,我认为您的意思是 test(7,8) 而不是 test(7,14) 示例取自 Coursera,Scala 编程原理。讲座 1.2。按名称调用应为 def test (x:Int, y: => Int) = x * x 注意参数 y 从未使用过。 好例子!摘自 Coursera MOOC :) 这是对差异的一个很好的解释,但没有解决所提出的问题,即 Scala 调用两者中的哪一个【参考方案3】:

在您的示例中,所有参数将在 在函数中调用之前进行评估,因为您只是按值定义它们。 如果你想按名称定义你的参数,你应该传递一个代码块:

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

这样,参数x 不会被评估直到它在函数中被调用。

little post 这里也很好地解释了这一点。

【讨论】:

【参考方案4】:

为了在上述 cmets 中重复 @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

【讨论】:

我想你的意思是: def callAlsoByName(x: () => Int) = println("x1=" + x()) println ("x2=" + x()) 然后: callAlsoByName(() => something()) 我认为你不需要在 something() 周围加上花括号在这最后一次通话中。注意:我试图只编辑您的答案,但我的编辑被审阅者拒绝,说它应该是评论或单独的答案。 显然你不能在 cmets 中使用 syntax highlighting 所以忽略 "" 部分!我会编辑我自己的评论,但你只能在 5 分钟内完成! :) 我最近也遇到了这个问题。从概念上来说可以这样想,但 scala 区分 => T() => T。将第一种类型作为参数的函数将不接受第二种,scala 在@ScalaSignature 注释中存储了足够的信息以为此引发编译时错误。 => T() => T 的字节码是相同的,并且是 Function0。有关详细信息,请参阅this question。【参考方案5】:

我将尝试通过一个简单的用例来解释,而不是仅仅提供一个例子

想象一下,您想构建一个“唠叨应用”,它会在您上次被唠叨后每次都唠叨你。

检查以下实现:

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 仅在按名称传递时才会起作用 原因是,当通过值传递时,它将被重新使用,因此不会重新评估该值,而当通过名称传递时,每次访问变量时都会重新评估该值

【讨论】:

【参考方案6】:

通常,函数的参数是按值参数;也就是说,参数的值是在传递给函数之前确定的。但是,如果我们需要编写一个函数,该函数接受一个表达式作为参数,在我们的函数中调用它之前我们不想计算它呢?对于这种情况,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. 斯卡拉测试
 3.延迟法
 4.以纳秒为单位获取时间
 5. 参数:81303808765843
 6.以纳秒为单位获取时间

【讨论】:

【参考方案7】:

正如我所假设的,上面讨论的call-by-value 函数只将值传递给函数。根据Martin Odersky 的说法,这是一个 Scala 遵循的评估策略,在函数评估中起着重要作用。但是,让call-by-name 变得简单。就像将函数作为参数传递给方法一样,也称为Higher-Order-Functions。当方法访问传递参数的值时,它调用传递函数的实现。如下:

根据@dhg的例子,先创建方法为:

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

此函数包含一个println 语句并返回一个整数值。创建函数,其参数为call-by-name

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

这个函数参数,定义一个匿名函数,返回一个整数值。在这个x 中包含一个函数定义,该函数具有0 传递的参数但返回int 值并且我们的something 函数包含相同的签名。当我们调用函数时,我们将函数作为参数传递给callByName。但在call-by-value 的情况下,它只将整数值传递给函数。我们调用函数如下:

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

在此我们的something方法调用了两次,因为当我们在callByName方法中访问x的值时,它调用了something方法的定义。

【讨论】:

【参考方案8】:

按值调用是一般用例,这里有很多答案..

Call-by-name 将代码块传递给调用者,并且每次调用 调用者访问参数,代码块被执行, 计算值。

我将尝试通过下面的用例以更简单的方式演示按名称调用

示例 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 (with scala) uses logging using call by name way see Logging trait 其中它的懒惰地评估是否通过下面的方法log.isInfoEnabled

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

【讨论】:

【参考方案9】:

按值调用中,表达式的值在函数调用时预先计算,并将该特定值作为参数传递给相应的函数。整个函数都将使用相同的值。

而在按名称调用中,表达式本身作为参数传递给函数,并且仅在调用特定参数时在函数内部计算。

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

在上面的代码sn-p中,对于函数调用CallbyValue(System.nanoTime()),系统纳米时间是预先计算的,并且预先计算的值已经传递了一个参数到函数调用。

但在 CallbyName(System.nanoTime()) 函数调用中,表达式“System.nanoTime())”本身作为参数传递给函数调用以及该表达式的值在函数内部使用该参数时计算。

注意 CallbyName 函数的函数定义,其中有一个 => 符号分隔参数 x 及其数据类型。那里的特定符号表示该函数是按名称类型调用的。

换句话说,按值调用函数参数在进入函数之前被评估一次,但按名称调用函数参数仅在需要时才在函数内部进行评估。

希望这会有所帮助!

【讨论】:

【参考方案10】:

这是我编写的一个简单示例,以帮助我目前正在学习 Scala 课程的同事。我认为有趣的是,Martin 并没有使用前面讲授的 && 问题答案作为示例。无论如何,我希望这会有所帮助。

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
========================

【讨论】:

【参考方案11】:

参数通常是按值传递的,这意味着它们将在被替换到函数体之前被评估。

您可以在定义函数时使用双箭头强制按名称调用参数。

// 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) -> ... 

【讨论】:

【参考方案12】:

对于这个问题,互联网上已经有很多奇妙的答案。我会将我收集到的关于该主题的一些解释和示例汇编成一个汇编,以防万一有人觉得它有帮助

简介

价值调用 (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

按名称调用机制将代码块传递给调用,每次调用访问参数时,都会执行代码块并计算值。在以下示例中,延迟打印一条消息,表明已输入该方法。接下来,延迟打印一条带有其值的消息。最后,延迟返回'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 效率高出指数级,因为它避免了这种按名称调用所需要的参数表达式的重复重新计算。它只评估每个函数参数一次 + 它在命令式效果和副作用方面表现得更好,因为您往往会更好地了解何时评估表达式。 - 它可能会在其参数评估期间导致循环 * 在终止上方检查以下 *

如果不能保证终止怎么办?

-如果表达式 e 的 CBV 评估终止,则 e 的 CBN 评估也终止 - 另一个方向不正确

非终止示例

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

首先考虑表达式(1,loop)

CBN: first(1,loop) → 1 CBV: first(1,loop) → 减少这个表达式的参数。由于 one 是一个循环,因此它会无限地减少参数。它不会终止

每个案例行为的差异

让我们定义一个测试方法

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

案例 1 测试(2,3)

test(2,3)   →  2*2 → 4

由于我们从已评估的参数开始,因此按值调用和按名称调用的步骤数量相同

案例 2 测试(3+4,8)

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

在这种情况下,按值调用执行的步骤更少

案例 3 测试(7, 2*4)

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

我们避免对第二个参数进行不必要的计算

Case4 测试(3+4, 2*4)

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

不同的方法

首先,假设我们有一个带有副作用的函数。此函数打印出一些内容,然后返回一个 Int。

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

现在我们将定义两个函数,它们接受完全相同的 Int 参数,除了一个采用按值调用样式 (x: Int) 接收参数,另一个采用按名称调用样式(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())的副作用只发生了一次。但是,在点名版本中,副作用发生了两次。

这是因为值调用函数在调用函数之前计算传入表达式的值,因此每次访问的值都是相同的。但是,按名称调用函数会在每次访问传入的表达式时重新计算它的值。

最好使用 CALL-BY-NAME 的示例

发件人:https://***.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 时,您都会收到错误消息,因为在传递给 && 之前,将在 null 引用上调用 isSomething。因此,实际的声明是:

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

【讨论】:

【参考方案13】:

通过一个示例应该可以帮助您更好地理解差异。

让我们定义一个返回当前时间的简单函数:

def getTime = System.currentTimeMillis

现在我们将通过 name 定义一个函数,以延迟一秒的时间打印两次:

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

结果应该解释差异。 sn-p 可用here。

【讨论】:

【参考方案14】:

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))

【讨论】:

【参考方案15】:

看这个:

    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。控制台将显示“add”后跟“mul”!通常的评估方式。

【讨论】:

【参考方案16】:

Scala 变量评估在这里更好地解释 https://sudarshankasar.medium.com/evaluation-rules-in-scala-1ed988776ae8

def main(args: Array[String]): Unit = 
//valVarDeclaration 2
println("****starting the app***") // ****starting the app***
val defVarDeclarationCall1 = defVarDeclaration // defVarDeclaration 1
val defVarDeclarationCall2 = defVarDeclaration // defVarDeclaration 1

val valVarDeclarationCall1 = valVarDeclaration //
val valVarDeclarationCall2 = valVarDeclaration //

val lazyValVarDeclarationCall1 = lazyValVarDeclaration // lazyValVarDeclaration 3
val lazyValVarDeclarationCall2 = lazyValVarDeclaration //

callByValue(
  println("passing the value "+ 10)
  10
) // passing the value 10
   // call by value example
  // 10

callByName(
  println("passing the value "+ 20)
  20
) // call by name example
  // passing the value 20
  // 20
  

  def defVarDeclaration = 
println("defVarDeclaration " + 1)
1
  

  val valVarDeclaration = 
println("valVarDeclaration " + 2)
2
  

  lazy val lazyValVarDeclaration = 
println("lazyValVarDeclaration " + 3)
3
  

  def callByValue(x: Int): Unit = 
println("call by value example ")
println(x)
  

  def callByName(x: => Int): Unit = 
println("call by name example ")
println(x)
  

【讨论】:

【参考方案17】:

我认为这里的所有答案都不是正确的理由:

在按值调用中,参数只计算一次:

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,因此节省了一些计算,这有时可能是有益的。

【讨论】:

以上是关于在 Scala 中按名称调用与按值调用,需要澄清的主要内容,如果未能解决你的问题,请参考以下文章

在 C 中按值调用

在 lambda 演算中按值调用

在C ++中的继承上下文中按值传递对象[重复]

java学习笔记:java的方法参数传递

在火花对 RDD 中按值排序

C# 按值传递与按引用传递