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

时间:2015-01-01 16:32:51

标签: scala functional-programming

这些代码中这些下划线用法的区别和术语名称有:(参见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)_
                        ^

这是什么意思?

2

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扩展,这是正确的吗?但我也从this Q/A读到,这种下划线是部分应用功能。在同一页面中,有人还说这是占位符语法。那么哪一个是正确的呢?

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?我应该不使用吗? 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 _

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

相同
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 个答案:

答案 0 :(得分:7)

1&amp; 2 - 是相同的,这是eta-expansion,这意味着函数正在从作为语言一部分的函数转换为某些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)是一个返回function-object Byte => T的表达式(因为handler是一个函数对象FileInputStream => Byte => T而你做了部分应用它),因此scala不能对表达式进行eta扩展(仅适用于值和方法)。

部分应用

4 作为scala的curried函数支持的副作用(curried我的意思是逐个获取参数的能力)。

3 只是明确地partially applied

请注意,在所有3个示例中 - 如果您尝试使用多参数列表方法(尚未扩展到咖喱功能) - 您将收到1&amp; 2&amp; 4的相反结果:

handler: FileInputStream => Byte => T

如果需要,部分应用程序也会为您进行eta扩展。您也可以将eta-expansion视为(非精确)函数,使用0个部分应用的参数,因此它几乎是非常接近的术语,因为部分应用的函数总是scala中的对象(在haskell中它是first-class function)因为你总是需要部分应用的函数为first-class-citizen(如object或fc-function),以便在eta扩展后应用它。

<强> 5 即可。 Scala可以对值本身进行eta扩展,因为它们可以被视为具有0个参数的编译时函数(这就是为什么你看到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> )。它可以将任何值扩展到函数对象:

() => ...

在你的例子中 - value只是另一个函数 - 对象本身。另外scala> val k = 5 k: Int = 5 scala> val kk = k _ kk: () => Int = <function0> scala> val kkk = kk _ kkk: () => () => Int = <function0> scala> 不是完全curry函数(它将参数计数为some-count),但scala也可以自动部分应用。为了使它完全成为现实:

(Int, Int) => Int

这个过程实际上叫做currying。

使其成为咖喱的另一种方法 - 使用元组。它不是那么纯粹,因为currying实际上是删除了元组,但scala的Tuple只是一个类而不是参数列表中的元组: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> - 输入不是scala术语中的元组,但在(Int, Int) => Int中,输入是一个元组(无论从FP-perspecive它是第一种情况下对象的元组和第二种情况下一个对象的元组)。伪tuplying的例子:

((Int, Int)) => Int

5 vs 1&amp; 2 正如您之前所见,您无法将eta-expansion应用于表达式,只有方法/值/ vars:

 scala> def f(a: Int, b: Int) = a
 f: (a: Int, b: Int)Int

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

您会在错误消息中看到“方法”,但scala旨在以相同的方式处理方法/值/变量(当它们是类/对象的成员时)(至少部分地)支持{{3} }。

6 这是eta扩展,默认返回Function0:

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

你可能在这里期待function1,但是scala> val a = println _ a: () => Unit = <function0> 被重载并且eta-expansion机制选择最少的签名。如果需要其他类型(例如println中的Function1),则可以选择其他类型:

foreach

正如我所说,你可以将函数对象视为部分应用了0个参数的函数(如果需要,包括eta-expansion),这是与另一个UAP混淆的根源(我会在那里选择更好的例子)

正如我在P.2中所说,这种扩展可以自动应用:

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

关于scala> List(1,2) foreach println 1 2 - 它有效,因为scala中的任何类(包括println _ +"#")都有Function1(这就是implicit def + (s: String)在这里不起作用的原因)在Predef中定义(请参阅answer):

Int

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

scala> println _
res50: () => Unit = <function0>

scala> println _ + "#"
res51: String = <function0>#

你应该指定对象主机来修复它,因为scala期望类似“obj方法参数”(但这是实验性功能,有时你需要粘贴一些空行或“;”才能使它工作):

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

P.S。关于C ++引用/指针。函数没有任何价值,因为它是编译时结构,因此编译器只是为它创建一个值,该过程称为eta-expansion(或纯函数的eta-abstraction)。该值可能是指针(引用它的对象)的一部分,或者只是引用本身 - 无关紧要。重要的是,函数在这里从编译(方法)移动到运行时(f-c函数),因此它“变得活着”。

P.S.2。有时,当部分应用多参数列表方法作为参数显式传递时,scala会自动进行eta扩展(如SI-194)。

P.S.N。您可以在@Daniel C. Sobral here中找到关于scala标点符号的下划线的其他信息。