按名称和按值类型的多态类型推断

时间:2018-08-27 16:20:56

标签: scala

我一直被类型推断问题困扰,我不确定是否做错了什么,编译器中是否有错误,或者是语言上的限制

我创建了一个虚拟示例来说明问题,用例没有意义,但是请相信我我有一个有效的用例

假设我有此代码

val function: (Int, String) => String = (_, _) => ""

implicit class Function2Ops[P1, P2, R](f: (P1, P2) => R) {
   def printArgs(p1: P1, p2: P2): Unit = println(p1, p2)
}

function.printArgs(1, "foo")

有效并打印(1,foo) 现在,如果我将代码更改为(请注意by-name参数)

val function: (Int, => String) => String = (_, _) => ""

implicit class Function2Ops[P1, P2, R](f: (P1, P2) => R) {
   def printArgs(p1: P1, p2: P2): Unit = println(p1, p2)
}

function.printArgs(1, "foo")

它将打印(1,MyTest$$Lambda$131/192881625@61d47554)

现在,在这种情况下,我可以尝试进行模式匹配和/或使用TypeTag提取值(如果是按名称命名的参数),但是, 我实际上想要实现的是做这样的事情

trait Formatter[T] {
  def format: String
}

case class ReverseStringFormat(v: String) extends Formatter[String] {
  override def format: String = v.reverse
}

case class PlusFortyOneFormat(v: Int) extends Formatter[Int] {
  override def format: String = (v + 41).toString
}

implicit def noOpFormatter[T](v: T): Formatter[T] = new Formatter[T] {
  override def format: String = v.toString
}

val function: (Int, => String) => String = (_, _) => ""

implicit class Function2Ops[P1, P2, R](f: (P1, P2) => R) {
   def printArgs(p1: Formatter[P1], p2: Formatter[P2]): Unit = println( p1.format, p2.format)
}

function.printArgs(1, ReverseStringFormat("foo"))

请注意,主要目的是我应该能够传递参数的原始类型或格式化程序,这就是为什么此扩展方法的签名使用Formatter[TypeOfOriginalParam]的原因,这也是为什么我拥有{{ 1}},当我不需要任何格式

现在,在这里,我无能为力,因为代码无法针对此错误进行编译

implicit def noOpFormatter[T](v: T): Formatter[T]

如果我按隐式类的第二个类型作为参数名,我可以使其运行

Error:(22, 40) type mismatch;
   found   : ReverseStringFormat
   required: Formatter[=> String]
   function.printArgs(1, ReverseStringFormat("foo"))

这将打印val function: (Int, => String) => String = (_, _) => "" implicit class Function2Ops[P1, P2, R](f: (P1, => P2) => R) { def printArgs(p1: Formatter[P1], p2: Formatter[P2]): Unit = println( p1.format, p2.format) } function.printArgs(1, ReverseStringFormat("foo"))

现在,主要问题是我想对任何函数都执行此操作,而不管其任何参数是按值还是按名称。 这就是我要坚持的地方,我可以为是否存在按名称的参数的情况的每种可能的组合创建不同的隐式类,但这是不切实际的,因为我需要对从Function1到Function10的每个函数执行此操作,并且按名称和按值参数之间可能的组合数量将很大。

有什么想法吗?如果我只对类型感兴趣,是否真的需要关心参数的惰性?我是在尝试执行设计不支持的操作,还是在编译器中出现错误?

顺便说一句,这就是我要避免的事情

(1,oof)

它有效(请注意,我随机使用了格式化程序或原始值,原始参数是按名称还是按值都没关系)

val function: (Int, => String) => String     = (_, _) => ""
val function2: (Int, String) => String       = (_, _) => ""
val function3: (=> Int, String) => String    = (_, _) => ""
val function4: (=> Int, => String) => String = (_, _) => ""

implicit class Function2Ops[P1, P2, R](f: (P1, => P2) => R) {
  def printArgs(p1: Formatter[P1], p2: Formatter[P2]): Unit = println("f1", p1.format, p2.format)
}

implicit class Function2Opss[P1, P2, R](f: (P1, P2) => R) {
  def printArgs(p1: Formatter[P1], p2: Formatter[P2]): Unit = println("f2", p1.format, p2.format)
}

implicit class Function2Opsss[P1, P2, R](f: (=> P1, P2) => R) {
  def printArgs(p1: Formatter[P1], p2: Formatter[P2]): Unit = println("f3", p1.format, p2.format)
}

implicit class Function2Opssss[P1, P2, R](f: (=> P1, => P2) => R) {
  def printArgs(p1: Formatter[P1], p2: Formatter[P2]): Unit = println("f4", p1.format, p2.format)
}

function.printArgs(1, "foo")
function2.printArgs(1, ReverseStringFormat("foo"))
function3.printArgs(1, "foo")
function4.printArgs(PlusFortyOneFormat(1), "foo")

但是似乎不得不将所有这些都写给我

2 个答案:

答案 0 :(得分:0)

我建议使用类型类,这是实现它们的方法。

首先,我将创建一个用于打印单个参数的类型类。

trait PrintArg[A] { def printArg(a: A): String }

然后,我将实现用于处理按名称和按值参数类型的实例:

object PrintArg extends PrintArgImplicits {
  def apply[A](implicit pa: PrintArg[A]): PrintArg[A] = pa
}
trait PrintArgImplicits extends PrintArgLowLevelImplicits {
  implicit def printByName[A] = new PrintArg[=> A] { def printArg(a: => A) = a.toString }
}
trait PrintArgLowLevelImplicits {
  implicit def printByValue[A] = new PrintArg[A] { def printArg(a: A) = a.toString }
}

除了Scala禁止我们在函数声明语法之外的其他地方声明按名称类型。

error: no by-name parameter type allowed here

这就是我们要解决的原因:我们将在函数声明中声明按名称的类型,并将该函数提升为我们的类型类:

def instance[A](fun: A => String): PrintArg[A] = new PrintArg[A] { def printArg(a: A) = fun(a) }

def printByName[A] = {
  val fun: (=> A) => String = _.toString
  PrintArg.instance(fun)
}
def printByValue[A] = {
  val fun: A => String = _.toString
  PrintArg.instance(fun)
}

现在,让我们将所有内容放在一起:

trait PrintArg[A] { def printArg(a: A): String }
object PrintArg extends PrintArgImplicits {
  def apply[A](implicit pa: PrintArg[A]): PrintArg[A] = pa
  def instance[A](fun: A => String): PrintArg[A] = new PrintArg[A] { def printArg(a: A) = fun(a) }
}
trait PrintArgImplicits extends PrintArgLowLevelImplicits {
  implicit def printByName[A] = {
    val fun: (=> A) => String = _.toString
    PrintArg.instance(fun)
  }
}
trait PrintArgLowLevelImplicits {
  implicit def printByValue[A] = {
    val fun: A => String = _.toString
    PrintArg.instance(fun)
  }
}

最后,我们可以使用打印机中的type类来一次处理所有情况:

implicit class Function2Ops[P1: PrintArg, P2: PrintArg, R](f: (P1, P2) => R) {
  def printArgs(p1: P1, p2: P2): Unit =
    println(PrintArg[P1].printArg(p1), PrintArg[P2].printArg(p2))
}

val function1: (Int, String) => String = (_, _) => ""
val function2: (Int, => String) => String = (_, _) => ""

function1.printArgs(1, "x")
function2.printArgs(2, "y")

将打印

(1,x)
(2,y)

红利1:如果您的类型toString不能显示任何有用的内容,则可以提供另一个隐式def / val并将其支持范围扩展到所有3x3情况。

红利2:对于这个特定示例,我基本上展示了如何实现和使用Show类型类,因此您可能需要在此处做的只是简单地重用现有的实现(也许仅提供按名称提供的隐式def)。但我会将其留给读者练习。

答案 1 :(得分:0)

我不确定我是否完全了解您的最终目标是什么,但是我可以向您解释为什么您看到自己看到的东西。

关于Scala语言的两个事实共同导致您所遇到的问题:

  1. 按名称=> A不是类型;和
  2. 按名称必须是方法签名的一部分,因为调用代码需要构造一个惰性值。

要查看(1)是否为真,请注意您不能这样做:

val a: => Int = 3

换句话说,尽管(=> Int) => Int不是类型,但是=> Int不是类型。您的类型不能为=> Int

要查看(2)是否为真,请考虑调用带有by-name参数的方法时会发生什么:

def foo(a: => Unit) {
  println("1")
  a
}

foo(println("2"))

调用代码需要知道不传递()作为参数,而是传递承诺。因此,调用代码必须知道foo带有一个by-name参数,因此,它是一个by-name参数这一事实必须是方法签名的一部分。在幕后,调用代码必须构造() => Unit类型的no-arg方法并将其传递。

现在,如何将它们结合起来在您的代码中产生问题?

写时:

implicit class Function2Ops[P1, P2, R](f: (P1, P2) => R) {
   def printArgs(p1: P1, p2: P2): Unit = println(p1, p2)
}

并传递类型为(Int, => String) => String的参数,事实(2)要求P2不能绑定到String。如果P2绑定到String,则f的类型为(Int, String) => String,这与必须在方法签名中反映出按需命名的规则相矛盾。尽管您的printArgs不会调用f,但它可以 ,因此f的签名必须保留按名称。

但是,事实(1)要求P2不能绑定到=> String,因为=> String不是类型。因此,剩下的唯一选择是将P2绑定到() => String。这与作为无参数函数的按名称参数的实现兼容,并确保printArgs将从调用方接收正确的类型,并将正确的类型传递给f(如果它选择调用) f

作为最后的评论,请注意不可能抽象出同名参数,因为使用同名参数必须生成适当的字节码。请注意,以下两个函数必须生成不同的字节码:

def foo(a: => Unit) {
  println(a)
}

def foo(a: Unit) {
  println(a)
}

由于Scala中的类型参数在编译时被删除,因此永远不可能以会改变函数行为的方式使用类型参数。因此,不可能仅使用类型参数来抽象名称参数。

最后的观察结果表明,最终可以解决问题的方法:必须放弃使用纯类型参数,而引入值参数。正如Mateusz Kubuszok观察到的那样,引入值参数的一种方法是使用类型类。还有其他方法,例如类型标记或显式值参数。但是,为了改变函数的行为,您必须仅依靠类型参数以外的其他东西。