这些 scala 方法中下划线用法之间的差异


这些

代码的下划线用法之间有什么区别和术语名称:(请参阅handler(resource)部分)

1.

def readFile[T](f: File)(handler: FileInputStream => Byte => T): T = {
    val resource = new java.io.FileInputStream(f)
    try {
        val hh = handler(resource)_
        hh(2)
    } finally {
        resource.close()
    }
}
val bs = new Array[Byte](4)
readFile(new File("scala.txt")) {
    input => b: Byte => println("Read: " + (input.read(bs) + b))
}

我收到编译错误:

Error:(55, 29) _ must follow method; cannot follow Byte => T
            val hh = handler(resource)_
                        ^

什么意思?

阿拉伯数字。

def readFile[T](f: File)(handler: FileInputStream => Byte => T): T = {
    val resource = new java.io.FileInputStream(f)
    try {
        val hh = handler(resource) _
        hh(2)
    } finally {
        resource.close()
    }
}
// Lower parts are same, so removed for brevity...
// ...

结果与 1 号相同,我得到:_ must follow method编译错误。

我读到这是因为下划线用于将方法转换为函数(ETA 扩展),但我也看到相同的下划线用于部分应用函数没有问题,例如:

val sum = (x: Int, y: Int) => x + y
val sum2 = sum _

在这种情况下没有错误。

3.

def readFile[T](f: File)(handler: FileInputStream => Byte => T): T = {
    val resource = new java.io.FileInputStream(f)
    try {
        val hh = handler(resource)(_)
        hh(2)
    } finally {
        resource.close()
    }
}
//...

这个工作正常。如果我没记错的话,在这种情况下,下划线称为ETA扩展,对吗?但我也从这个问答中读到,这种下划线是针对部分应用函数的。在同一页中,有人还说这是一个占位符语法。那么哪一个是正确的呢?

4.

def readFile[T](f: File)(handler: FileInputStream => Byte => T): T = {
    val resource = new java.io.FileInputStream(f)
    try {
        val hh = handler(resource)
        hh(2)
    } finally {
        resource.close()
    }
}
//...

这个不使用下划线,但它也像 3 号一样工作正常。我对这个案例的问题,与3号有什么区别?我应该使用4号而不是3号吗?第 3 号中的下划线是多余的吗?

很抱歉在这里有很多问题,但这个下划线的东西真的很令人困惑。

不知何故,我认为 Scala 中下划线的复杂性与 C/C++ 中指针和引用 (*/&/&&) 的复杂性相匹配。

更新:

5.

我再次发现了关于下划线的有趣之处:

scala> def sum(x: Int, y: Int) = x + y     // sum is a method because of def
sum: (x: Int, y: Int)Int
scala> val sum2 = sum _    // sum2 is explicit ETA expanded function from sum
sum2: (Int, Int) => Int = <function2>
scala> sum2(2,3)      // testing
res0: Int = 5
scala> val sum3 = (x: Int, y: Int) => x + y      // sum3 is a function object
sum3: (Int, Int) => Int = <function2>
scala> val sum4 = sum3 _           // what happpened here?
sum4: () => (Int, Int) => Int = <function0>
scala> sum4()(2,3)
res2: Int = 5

你能告诉我sum4发生了什么事吗?为什么sum3 _的结果具有函数类型:() => (Int, Int) => Int

6.

List(1, 2, 3) foreach println _

根据这个答案,这是部分应用的功能。好的,我可以看到下划线之前的空格有点棘手。它实际上与:

List(1, 2, 3).foreach(println(_))

所以这确实是部分应用的功能。

但是如果我这样做:

scala> List(1, 2, 3).foreach(println _+1)  //#1
<console>:8: error: type mismatch;
 found   : Int(1)
 required: String
              List(1, 2, 3).foreach(println _+1)
                                          ^
scala> List(1, 2, 3).foreach(println _+"#")    //#2 printed out nothing (why?)
scala> List(1, 2, 3).foreach(println 1+_)      //#3
<console>:1: error: ')' expected but integer literal found.
       List(1, 2, 3).foreach(println 1+_)
                                     ^
scala> List(1, 2, 3).foreach(println "#"+_)    //#4
<console>:1: error: ')' expected but string literal found.
       List(1, 2, 3).foreach(println "#"+_)
                                     ^

新手通常会认为在这种情况下下划线是占位符,但我相信事实并非如此,不是吗?

1 和 2 - 是相同的,这是 eta 扩展,这意味着函数正在从作为语言一部分的函数转换为某个FunctionN类的真实对象:

scala> def f(a: Int) = a
f: (a: Int)Int
scala> f.apply(1)
<console>:9: error: missing arguments for method f;
follow this method with `_' if you want to treat it as a partially applied function
              f.apply(1)
              ^
scala> f _
res1: Int => Int = <function1>    
scala> (f _).apply(1)
res2: Int = 1

它在你的例子中不起作用,因为handler(resource)是一个返回函数对象Byte => T的表达式(因为handler是一个函数对象FileInputStream => Byte => T并且你对它进行了部分应用),所以 scala 不能对表达式进行 eta 扩展(仅适用于值和方法)。

4 部分用作 Scala 的柯里德函数支持的副作用(柯里德是指逐个获取参数的能力)。

3 只是显式部分应用。

请注意,在所有 3 个示例中 - 您的 handler: FileInputStream => Byte => T 函数是一个对象(因此它已经 eta 扩展),如果您尝试使用多参数列表方法(尚未扩展到 curried 函数)做同样的事情 - 您将收到 1&2&4 的相反结果:

scala> def f(a: Int)(b: Int) = a //it's not a curried function, as it's just multi-parameter-list method
f: (a: Int)(b: Int)Int
scala> f(2) 
<console>:9: error: missing arguments for method f;
follow this method with `_' if you want to treat it as a partially applied function
              f(2)
           ^
scala> f(2) _ //you need to convert f(2) to object first
res4: Int => Int = <function1>
scala> f(2)(_)
res5: Int => Int = <function1>
scala> f _  //expand method to the function object
res6: Int => (Int => Int) = <function1>

因此,如果需要,部分应用程序也可以为您进行eta扩展。你也可以认为 eta 扩展是(不精确)具有 0 个部分应用参数的函数,所以它几乎是接近的术语,因为部分应用的函数总是 scala 中的对象(在 haskell 中它是一等函数),因为你总是需要部分应用的函数成为一等公民(如对象或 f-c-函数)才能在 eta 展开后应用它。

5. Scala 本身可以对值进行 eta 扩展,因为它们可能被视为参数为 0 的编译时函数(这就是您看到 () => ... 的原因)。它可以将任何值扩展到函数对象:

scala> val k = 5
k: Int = 5
scala> val kk = k _
kk: () => Int = <function0>
scala> val kkk = kk _
kkk: () => () => Int = <function0>
scala> 

在您的示例中 - 值只是另一个函数对象本身。此外(Int, Int) => Int不是完全柯里函数(它逐个计数地获取参数),但 scala 也可以自动部分应用此类参数。要使其完全咖喱:

scala> def f(a: Int, b: Int) = a
f: (a: Int, b: Int)Int
scala> (f _).curried
res23: Int => (Int => Int) = <function1>
scala> def f(a: Int, b: Int)(z: Int) = a
f: (a: Int, b: Int)(z: Int)Int
scala> (f _).curried
res22: Int => (Int => (Int => Int)) = <function1>

这个过程实际上称为咖喱。

另一种使其柯里化的方法 - 是使用元组。它不像currying实际上删除元组那么纯粹,但是scala的元组只是一个类,而不是参数列表中的元组:(Int, Int) => Int - 输入不是Scala术语中的元组,但在((Int, Int)) => Int中,输入是一个元组(不管从FPperspecive来看,它是第一个情况下对象的元组,第二个情况下是一个对象的元组)。伪图普利的例子:

 scala> def f(a: Int, b: Int) = a
 f: (a: Int, b: Int)Int
 scala> (f _).tupled
 res24: ((Int, Int)) => Int = <function1>

5 vs 1&2 正如您之前看到的,您不能将 eta 扩展应用于表达式,只能将方法/值/变量应用于表达式:

 scala> 5 _
 <console>:8: error: _ must follow method; cannot follow Int(5)
          5 _
          ^
 scala> val (a, b) = (5, 5)
 scala> (a + b) _
 <console>:10: error: _ must follow method; cannot follow Int
              (a + b) _
                 ^

你在错误消息中看到所需的"方法",但 scala 旨在以相同的方式处理方法/值/变量(当它们是类/对象的成员时)以(至少部分)支持 UAP。

6 它是 eta 扩展,默认情况下返回 Function0:

scala> val a = println _
a: () => Unit = <function0>

您可能期望在这里使用 function1,但println是重载的,并且 eta 扩展机制选择了最少的签名。当需要其他类型时(如Function1 foreach) - 它可能会选择另一种:

scala> val a: String => Unit = println _
a: String => Unit = <function1>

正如我所说,您可以将函数对象视为部分应用0参数的函数(如果需要,包括eta扩展),因此这是与另一个答案混淆的根源(我会在那里选择更好的示例)。

正如我在 P.S.2 中所说,此扩展可能会自动应用:

scala> List(1,2) foreach println
1
2

关于println _ +"#" - 它之所以有效,是因为 scala 中的任何类(包括 Function1 )在 Predef 中都定义了implicit def + (s: String)(这就是为什么Int在这里不起作用的原因)(请参阅 SI-194):

scala> println _
res50: () => Unit = <function0>
scala> println _ + "#"
res51: String = <function0>#

由于 5 vs 1&2,所有其他选项都不起作用,实际上 scala 甚至无法在单参数函数之后解析字符串:

scala> println "#"
<console>:1: error: ';' expected but string literal found.
   println "#"
           ^

您应该指定对象主机来修复它,因为 scala 需要类似"obj 方法 param"的东西(但这是实验性功能,有时您需要粘贴一些空行或";"才能使其工作):

scala> Predef println "aaa"
aaa

附言关于C++参考/指针。函数没有价值,因为它是编译时结构,所以编译器只是为它创建一个值,这个过程被称为 eta 扩展(或纯函数的 eta 抽象)。这个值可以是指针的一部分(引用它的对象)或只是引用它本身 - 没关系。重要的是函数在这里从编译(方法)移动到运行时(f-c-function),所以它"变得活跃"。

附言2.有时,当部分应用的多参数列表方法作为参数显式传递时,scala 会自动执行 eta 扩展(就像这里一样)。

附言您可以在 C. Sobral 关于 scala 标点符号的答案中找到有关下划线@Daniel其他信息。

最新更新