Scala中的ByName参数用法

5
我正在阅读"Scala函数式编程这本书,遇到了一个我不太理解的例子。
在严格/惰性章节中,作者描述了流的构造,并有如下代码:
sealed trait Stream[+A]
case object Empty extends Stream[Nothing]
case class Cons[+A](h: () => A, t: () => Stream[A]) extends Stream[A]

object Stream {
    def cons[A](hd: => A, tl: => Stream[A]) : Stream[A] = {
        lazy val head = hd
        lazy val tail = tl
        Cons(() => head, () => tail)
    }
    ...
}

我有一个问题是关于智能构造函数(cons),它调用Cons案例类的构造函数。用于传递headtail值的具体语法对我来说没有意义。为什么不直接像这样调用构造函数:
Cons(head, tail)

据我理解所使用的语法是强制创建两个Function0对象,它们简单地返回headtail值。那么这与仅传递headtail(不带() =>前缀)有何不同,因为Cons case类已经定义了按名称获取这些参数?这不是多余的吗?还是我漏掉了什么?
4个回答

9

区别在于=> A不等于() => A

前者是按名称传递的,而后者是一个不带参数且返回A的函数。

您可以在Scala REPL中测试此功能。

scala> def test(x: => Int): () => Int = x
<console>:9: error: type mismatch;
 found   : Int
 required: () => Int
       def test(x: => Int): () => Int = x
                                        ^

在我的示例中,简单地引用x会导致该参数被调用。在你的示例中,它构建了一个方法,推迟了对x的调用。


是的。对此我感到很抱歉。正如我向@Jesper提到的那样,我应该注意到这两个构造函数之间的差异,但我没有。因此,Cons需要一个显式的Function0,而cons会为您构建一个(在幕后)。传递给Cons的内容会立即被评估,而传递给cons的内容则稍后评估。这样说对吗? - melston
没错。当你按值传递时,每次读取它时都会重新评估。 - Nate
小问题:() => A 是一个只有单一、空参数列表的函数类型。(Unit) => A 是接收 unit 作为参数的函数类型。() 不是一个有效的类型。 - Feuermurmel
@Feuermurmel 我已经修改了;你是正确的,措辞不正确。我试图强调 ()Unit 值。因此,() => IntUnit => Int 是相同的类型,但稍微更短一些。 - Nate
@Nate () => IntUnit => Int 不是相同的类型:https://scalafiddle.io/sf/7V2DdXv/0。不过无论如何,感谢你修复了答案! :) - Feuermurmel
1
@Feuermurmel,你又对了!感谢你教给我一些东西! - Nate

9
首先,你认为=> A() => A是相同的。然而,它们并不相同。例如,=> A只能在按名称传递参数的上下文中使用 - 不可能声明类型为=> Aval。由于case class参数始终是val(除非明确声明为var),因此很明显为什么case class Cons[+A](h: => A, t: => Stream[A])不起作用。
其次,仅仅将按名称传递的参数包装到一个带有空参数列表的函数中,并不等同于上面的代码所实现的功能:通过使用lazy val,可以确保hdtl最多被评估一次。如果代码如下所示:
Cons(() => hd, () => tl)

原始的 hd 会在每次调用一个 Cons 对象的 h 方法(字段)时都被计算。使用 lazy valhd 仅在第一次调用此 Cons 对象的 h 方法时被计算,并且在随后的每次调用中返回相同的值。

简单地在 REPL 中演示区别:

> def foo = { println("evaluating foo"); "foo" }
> val direct : () => String = () => foo
> direct()
evaluating foo
res6: String = foo
> direct()
evaluating foo
res7: String = foo
> val lzy : () => String = { lazy val v = foo; () => v }
> lzy()
evaluating foo
res8: String = foo
> lzy()
res9: String = foo

请注意,在第二次调用lzy()时,“评估foo”的输出已经消失,而在第二次调用direct()时没有消失。

我不确定为什么val不能是类型=> A。我在细节中迷失了方向,没有意识到Conscons定义之间的区别。不过,我确实理解了lazy val对象的用法。谢谢。 - melston
沿着同样的线路,hdtl不是val参数吗?而且它们是按名称传递的。 - melston
我不知道你所说的 val 参数是什么意思。对我来说,val 参数这个术语只有在类的上下文中才有意义(例如:class A(x: String)class B(val x: String))。正如你指出的,cons 是一个普通方法。那么为什么 hdtl 要成为 val 参数呢? - misberner
抱歉,我不知道函数参数不能是 var。在阅读了您的评论并找到一些相关讨论后,我现在意识到函数参数不能使用 var。我错误地将 class B(var x: String) 的结构扩展到了普通函数中。 - melston

1
请注意,方法cons的参数是按名传递的(hdtl)。这意味着如果你调用cons,在调用cons之前不会评估参数;它们将在稍后,在你在cons内部使用它们的时候评估。
请注意,Cons构造函数接受两个类型为Unit => A的函数,但不作为按名传递的参数。因此,在调用构造函数之前,这些函数将被评估。
如果你执行Cons(head, tail),那么headtail将被评估,这意味着hdtl将被评估。
但整个重点在于避免在必要时调用hdtl(当某人访问Cons对象中的ht时)。因此,你向Cons构造函数传递了两个匿名函数;这些函数直到有人访问ht时才会被调用。

我在代码中迷失了方向,本应该注意到 case class 构造函数和智能构造函数声明之间的区别。谢谢。 - melston

0

def cons[A](hd: => A, tl: => Stream[A]) : Stream[A] 中,

hd 的类型为 Atl 的类型为 Stream[A]

然而,在 case class Cons[+A](h: () => A, t: () => Stream[A]) extends Stream[A] 中,

h 的类型为 Function0[A]t 的类型为 Function0[Stream[A]]

给定 hd 的类型为 A,智能构造函数调用该 case 类如下:

 lazy val head = hd
 lazy val tail = tl
 Cons(() => head, () => tail) //it creates a function closure so that head is accessible within Cons for lazy evaluation

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接