Scala如何将案例类转换为函数?

时间:2019-10-07 07:17:18

标签: scala function currying

我试图了解如何将案例类作为参数传递给接受函数作为参数的函数。下面是一个示例:

考虑以下功能

def !![B](h: Out[B] => A): In[B] = { ... }

如果我正确理解,这是一种多态方法,其类型参数为B,并且接受函数h作为参数。 OutIn是先前定义的另外两个类。

然后按如下所示使用此功能:

case class Q(p: boolean)(val cont: Out[R])
case class R(p: Int)

def g(c: Out[Q]) = {
  val rin = c !! Q(true)_
  ...
}

我知道使用currying是为了避免编写类型注释,而只是编写_。但是,我无法理解为什么将案例类Q转换为类型Out[B] => A的函数(h)。

编辑1 已更新!以上以及InOut的定义:

abstract class In[+A] {
  def future: Future[A]
  def receive(implicit d: Duration): A = {
    Await.result[A](future, d)
  }
  def ?[B](f: A => B)(implicit d: Duration): B = {
    f(receive)
  }
}
abstract class Out[-A]{
  def promise[B <: A]: Promise[B]
  def send(msg: A): Unit = promise.success(msg)
  def !(msg: A) = send(msg)
  def create[B](): (In[B], Out[B])
}

这些代码示例摘自以下论文:http://drops.dagstuhl.de/opus/volltexte/2016/6115/

2 个答案:

答案 0 :(得分:5)

TLDR;

使用带有多个参数列表的case类并部分应用它会产生部分应用的apply调用+ eta扩展会将方法转换为函数值:

val res: Out[Q] => Q = Q.apply(true) _

更长的解释

要了解它在Scala中的工作方式,我们必须了解案例类背后的一些基础知识以及方法与函数之间的区别。

Scala中的案例类是一种表示数据的紧凑方式。定义案例类时,将获得编译器为您创建的一堆便捷方法,例如hashCodeequals

此外,编译器还会生成一个名为apply的方法,该方法使您无需使用new关键字即可创建案例类实例:

case class X(a: Int)

val x = X(1)

编译器会将这个调用扩展到

val x = X.apply(1)

您的案例类也会发生同样的事情,只是您的案例类具有多个参数列表:

case class Q(p: boolean)(val cont: Out[R])

val q: Q = Q(true)(new Out[Int] { })

将被翻译为

val q: Q = Q.apply(true)(new Out[Int] { })

最重要的是,Scala可以将non value type的方法转换为具有FunctionX类型的函数类型,其中X是函数的缩写。为了将一个方法转换为函数值,我们使用了一个叫eta expansion的技巧,在此方法中,我们使用带下划线的方法。

def foo(i: Int): Int = i

val f: Int => Int = foo _

这会将方法foo转换为类型Function1[Int, Int]的函数值。

现在我们已经掌握了这些知识,让我们回到您的示例:

val rin = c !! Q(true) _

如果我们仅在此处隔离Q,则此调用将转换为:

val rin = Q.apply(true) _

由于apply方法使用多个参数列表,因此我们将获得一个给定Out[Q]的函数,该函数将创建一个Q

val rin: Out[R] => Q = Q.apply(true) _

答案 1 :(得分:2)

  

我无法理解为什么将案例类Q转换为类型Out[B] => A的函数(h)的原因和方式。

不是。实际上,case class Q与此绝对无关!这与object Q有关,这是case class Q陪伴模块

每个case class都有一个自动生成的companion module,其中包含(除其他外)一个apply方法,该方法的签名与同伴类的主要构造函数相匹配,并构造该方法的一个实例。伴侣班。

即当你写

case class Foo(bar: Baz)(quux: Corge)

您不仅获得了自动定义的案例类便捷方法,例如所有 elements toStringhashCodecopy和{{ 1}},但您还会获得一个自动定义的配套模块,该模块既可用作模式匹配的提取器,又用作对象构造的工厂:

equals

在Scala中,object Foo { def apply(bar: Baz)(quux: Corge) = new Foo(bar)(quux) def unapply(that: Foo): Option[Baz] = ??? } 是允许您创建“类似于函数”的对象的方法:如果apply是对象(而不是方法),则foo(bar, baz) is translated to foo.apply(bar, baz)。 / p>

难题的最后一步是η-expansion,它将方法(不是对象)提升为功能(其中对象,因此可以将其作为参数传递,存储在变量中,等等。)η-扩展有两种形式:显式使用foo的η-扩展运算符:

_

隐式η-扩展:如果Scala知道100%的意思是一个函数,但您给它指定了方法的名称,则Scala将为您执行η-扩展:

val printFunction = println _

您已经知道有关curring。

因此,如果我们将它们放在一起:

Seq(1, 2, 3) foreach println

首先,我们知道Q(true)_ 在这里不可能是类Q。我们怎么知道呢?因为此处的Q用作,但是类型,并且像大多数编程语言一样,Scala具有严格的分隔在类型和值之间。因此,Q必须是一个值。特别是,由于我们知道类Q是一个案例类,所以对象Q是类Q的伴随模块。

第二,我们知道对于值Q

Q

的语法糖
Q(true)

第三,我们知道对于案例类,伴随模块具有一个自动生成的Q.apply(true) 方法,该方法与主构造函数匹配,因此我们知道apply具有两个参数列表。

所以,最后,我们有

Q.apply

将第一个参数列表传递到Q.apply(true) _ ,然后将Q.apply提升为接受第二个参数列表的函数。

请注意,带有多个参数列表的案例类是不寻常的,因为只有第一个参数列表中的参数才被视为案例类的 elements ,只有元素才受益于“案例类魔术”,也就是说,只有元素才自动实现访问器,Q.apply方法的签名中仅使用元素,自动生成的copyequalshashCode中仅使用元素方法,等等。