lambda函数也不是具有Function1特征的对象吗?

时间:2016-12-19 04:18:56

标签: scala

object MyApp {
  def printValues(f: {def apply(x: Int): Int}, from: Int, to: Int): Unit = {
    println(
      (from to to).map(f(_)).mkString(" ")
    )
  }

  def main(args: Array[String]): Unit = {
    val anonfun1 = new Function1[Int, Int] {
      final def apply(x: Int): Int = x * x
    }

    val fun1 = (x:Int)=>x*x
    printValues(fun1, 3, 6)
  }
}

我认为scala中的lambda函数也是扩展Function1特性的对象。但是,此代码对printValues(fun1, 3, 6)而非printlnValues(anonfun1, 3, 6)失败。为什么会这样?

3 个答案:

答案 0 :(得分:8)

这是一个非常有趣的问题。有一种说法,一个人不应该依赖于代码中的实现细节,我认为这是做到这一点的边缘。

让我们尝试分解这里发生的事情。

结构类型:

当您需要结构类型时,例如您在此方法中所做的:

def printValues(f: {def apply(x: Int): Int}, from: Int, to: Int): Unit = {
  println(
    (from to to).map(f(_)).mkString(" ")
  )
}

Scala所做的是使用反射在运行时尝试查找apply方法,并动态调用它。它被翻译成如下所示:

public static Method reflMethod$Method1(final Class x$1) {
    MethodCache methodCache1 = Tests$$anonfun$printValues$1.reflPoly$Cache1.get();
    if (methodCache1 == null) {
        methodCache1 = (MethodCache)new EmptyMethodCache();
        Tests$$anonfun$printValues$1.reflPoly$Cache1 = new SoftReference((T)methodCache1);
    }
    Method method1 = methodCache1.find(x$1);
    if (method1 != null) {
        return method1;
    }
    method1 = ScalaRunTime$.MODULE$.ensureAccessible(x$1.getMethod("apply", (Class[])Tests$$anonfun$printValues$1.reflParams$Cache1));
    Tests$$anonfun$printValues$1.reflPoly$Cache1 = new SoftReference((T)methodCache1.add(x$1, method1));
    return method1;
}

这是发出的反编译Java代码。长话短说,它寻找apply方法。

Scala< = 2.11

中会发生什么

对于2.12之前的任何Scala版本,声明匿名函数会导致编译器生成一个扩展AbstractFunction*的类,其中*是函数的arity。这些抽象函数类依次继承Function*,并使用lambda的实现实现它们的apply方法。

例如,如果我们接受你的表达:

val fun1 = (x:Int) => x * x 

编译器为我们发出:

val fun2: Int => Int = {
    @SerialVersionUID(value = 0) final <synthetic> class $anonfun extends scala.runtime.AbstractFunction1$mcII$sp with Serializable {
      def <init>(): <$anon: Int => Int> = {
        $anonfun.super.<init>();
        ()
      };
      final def apply(x: Int): Int = $anonfun.this.apply$mcII$sp(x);
      <specialized> def apply$mcII$sp(x: Int): Int = x.*(x)
    };
    (new <$anon: Int => Int>(): Int => Int)
  };
  ()

当我们查看字节码级别时,我们会看到生成的匿名类及其apply方法:

Compiled from "Tests.scala"
public final class othertests.Tests$$anonfun$1 extends scala.runtime.AbstractFunction1$mcII$sp implements scala.Serializable {
  public static final long serialVersionUID;

  public final int apply(int);
    Code:
       0: aload_0
       1: iload_1
       2: invokevirtual #21                 // Method apply$mcII$sp:(I)I
       5: ireturn

  public int apply$mcII$sp(int);
    Code:
       0: iload_1
       1: iload_1
       2: imul
       3: ireturn

So what happens when you request the `apply` method at runtime? The run-time will see that theirs a method defined on `$anonfun` called `apply` which takes an `Int` and returns an `Int`, which is exactly what we want and invoke it. All is good and everyone's happy.

Lo和Behold,Scala 2.12

在Scala 2.12中,我们得到了一种称为SAM转换的东西。 SAM类型是Java 8中的一项功能,它允许您缩写实现接口,而是提供lambda表达式。例如:

new Thread(() -> System.out.println("Yay in lambda!")).start();

而不是必须实施Runnable并覆盖public void run。 Scala 2.12设定了一个目标,即在可能的情况下通过SAM转换与SAM类型兼容。

在我们的特定情况下,可以进行SAM转换,这意味着我们会获得Function1[Int, Int]的专用版本而不是Scala scala.runtime.java8.JFunction1$mcII$sp。此JFunction与Java兼容,并具有以下结构:

package scala.runtime.java8;

@FunctionalInterface
public interface JFunction1$mcII$sp extends scala.Function1, java.io.Serializable {
    int apply$mcII$sp(int v1);

    default Object apply(Object t) { return scala.runtime.BoxesRunTime.boxToInteger(apply$mcII$sp(scala.runtime.BoxesRunTime.unboxToInt(t))); }
}

这个JFunction1已被专门化(就像我们在Scala中使用@specialized注释一样)为def apply(i: Int): Int发出特殊方法。请注意这里的一个重要因素,即此方法仅实现apply形式的Object => Object方法,而不是Int => Int。现在我们可以开始了解为什么这可能会有问题。

现在,当我们在Scala 2.12中编译相同的示例时,我们看到:

def main(args: Array[String]): Unit = {
  val fun2: Int => Int = {
    final <artifact> def $anonfun$main(x: Int): Int = x.*(x);
    ((x: Int) => $anonfun$main(x))
  };
  ()

我们不再看到扩展AbstractFunction*的方法,我们只看到名为$anonfun$main的方法。当我们查看生成的字节码时,我们会在内部看到它会调用JFunction1$mcII$sp.apply$mcII$sp(int v1);

public void main(java.lang.String[]);
  Code:
    0: invokedynamic #41,  0  // InvokeDynamic #0:apply$mcII$sp: ()Lscala/runtime/java8/JFunction1$mcII$sp;
    5: astore_2
    6: return

然而,如果我们自己明确地扩展Function1并实现apply,我们会得到与之前的Scala版本类似的行为,但不是完全相同:

def main(args: Array[String]): Unit = {
  val anonfun1: Int => Int = {
    final class $anon extends AnyRef with Int => Int {
      def <init>(): <$anon: Int => Int> = {
        $anon.super.<init>();
        ()
      };
      final def apply(x: Int): Int = x.*(x)
    };
    new $anon()
  };
  {
    ()
  }
}

我们不再扩展AbstractFunction*,但我们确实有一个apply方法,它在运行时满足结构类型条件。在字节码级别,我们看到int apply(int)object apply(object)以及注释Function*的@specialization属性的一堆案例:

public final int apply(int);
  Code:
     0: aload_0
     1: iload_1
     2: invokevirtual #183                // Method apply$mcII$sp:(I)I
     5: ireturn

public int apply$mcII$sp(int);
  Code:
     0: iload_1
     1: iload_1
     2: imul
     3: ireturn

public final java.lang.Object apply(java.lang.Object);
  Code:
     0: aload_0
     1: aload_1
     2: invokestatic  #190                // Method scala/runtime/BoxesRunTime.unboxToInt:(Ljava/lang/Object;)I
     5: invokevirtual #192                // Method apply:(I)I
     8: invokestatic  #196                // Method scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
    11: areturn

结论:

我们可以看到,在某些情况下,Scala编译器如何处理lambda表达式的实现细节发生了变化。这是一个错误吗?我的感觉倾向于没有。在任何地方,Scala规范都不能保证需要一个名为apply的方法与lambda的签名匹配,这就是我们称之为实现细节的原因。虽然这确实是一个有趣的怪癖,但我不会在任何生产环境中依赖这些代码,因为它可能会发生变化。

答案 1 :(得分:2)

我认为我并没有完全弄清楚问题是什么。 您的代码似乎也可以正常工作。

但该行

  

我认为scala中的lambda函数也是扩展Function1

的对象
如果Scala版本是&lt;

是正确的。 2.12

scala中的函数文字只是FunctionN的一个语法糖,它是Function1到Function22。 所以下面的代码完全相同。

val f: Int=> Int = new Function1[Int, Int] {
    def apply(x:Int): Int = x * x
}

val f2: Int => Int = (x: Int) => x * x

如果你想检查它们都是FunctionN,那么你的代码printValues应该是这样的

def printValues(f: (Int) => Int, from: Int, to: Int): Unit = {
     println((from to to).map(f).mkString(" "))
}
//or
def printValues(f: Function1[Int,Int], from: Int, to: Int): Unit = {
     println((from to to).map(f).mkString(" "))
}

再次,由于语法糖,它们是相同的含义。 您对参数类型的定义是AnyRef{def apply(x: Int): Int}所以

object Foo {
  def apply(x: Int): Int = x * x
}

可用于printValues

如果您正在使用Scala 2.12,那就有点不同了。

如果您的函数文字满足某些条件,它将自动转换为SAM类型。 有关详细信息,请查看此sam-conversion

<强> EDITED 您必须使用2.12,因为如果代码在2.11或者预告片上,代码可以正常工作。 就像我在上一次提到的那样,这都是因为sam转换。 将printValues的参数修改为(Int) => IntFunction1[Int,Int]以明确定义以使用FunctionN

答案 2 :(得分:2)

在结构类型的实例上调用方法涉及运行时反射。

对于val f: { def apply(i: Int): Int} = (x: Int) => x * x,来电f(10)(或f.apply(10))已解析为f.getClass.getMethod("apply", classOf[Int]).invoke(f, 10)

并且一个lambda在Scala 2.11.8中有这样的方法(并且Function1在2.12.1中仍然有它,所以你的代码可以与anonfun1一起使用):

scala> val f: { def apply(i: Int): Int } = (x: Int) => x * x
f: AnyRef{def apply(i: Int): Int} = <function1>

scala> f.getClass.getMethods.filter(_.getName == "apply") foreach println
public final int $line11.$read$$iw$$iw$$anonfun$1.apply(int)
public final java.lang.Object $line11.$read$$iw$$iw$$anonfun$1.apply(java.lang.Object)

但是在2.12.1中它只有一个方法需要Object

scala> val f: { def apply(i: Int): Int } = (x: Int) => x * x
f: AnyRef{def apply(i: Int): Int} = $$Lambda$1015/1912769093@364b1061

scala> f.getClass.getMethods.filter(_.getName == "apply") foreach println
public default java.lang.Object scala.runtime.java8.JFunction1$mcII$sp.apply(java.lang.Object)

当然可以调用它:

scala> f.getClass.getMethod("apply", classOf[AnyRef]).invoke(f, Int.box(10)).asInstanceOf[Int]
res2: Int = 100

但不适用于标准结构类型方法调用。

我相信这是Scala标准库中的一个错误。 2.12.1中的Lambdas由scala.runtime.java8.JFunction*类的实例表示。例如:

@FunctionalInterface
public interface JFunction1$mcDF$sp extends scala.Function1, java.io.Serializable {
    double apply$mcDF$sp(float v1);

    default Object apply(Object t) { return scala.runtime.BoxesRunTime.boxToDouble(apply$mcDF$sp(scala.runtime.BoxesRunTime.unboxToFloat(t))); }
}

我没有看到为什么那些没有专门方法的原因,类似于为scala.Function*特征生成的方法:

@FunctionalInterface
public interface JFunction1$mcDF$sp extends scala.Function1, java.io.Serializable {
    double apply$mcDF$sp(float v1);

    default Object apply(Object t) { return scala.runtime.BoxesRunTime.boxToDouble(apply$mcDF$sp(scala.runtime.BoxesRunTime.unboxToFloat(t))); }

    default double apply(float t) { return apply$mcDF$sp(t); }
}