【问题标题】:Polymorphic type inference for by-name and by-value types按名称和按值类型的多态类型推断
【发布时间】:2019-02-02 05:32:49
【问题描述】:

我一直遇到类型推断问题,我不确定我做错了什么,编译器中存在错误,或者是语言限制

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

假设我有这个代码

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] 的原因,这也是我使用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"))

这会打印(1,oof)

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

有什么想法吗?如果我只对类型感兴趣,我真的需要关心参数的惰性吗?我是在尝试做一些设计不支持的事情,还是编译器中的错误?

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

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")

它的工作原理(请注意,我随机使用了格式化程序或原始值,原始参数是按名称还是按值无关紧要)

(f1,1,foo)
(f2,1,oof)
(f3,1,foo)
(f4,42,foo)

但是不得不把所有这些都写给我似乎很奇怪

【问题讨论】:

  • 非常好的问题。
  • val function: (Eval[Int], Eval[String]) => StringEval 怎么样?
  • 问题是我需要能够使用任何功能来做到这一点,我正在编写一个库,所以我无法控制用户将提供哪些功能,而且大多数时候他们将是方法转换为像obj.method _ 这样的函数,所以我不能假设/强加任何关于参数的形状/类型的东西
  • 也许将部分行为提取到类型类中?这样,您将有两种情况:按值和按名称,您可以为每个参数采用一个类型类实例,有效地处理所有 4 种组合。
  • @MateuszKubuszok 对不起,我不明白,类型类在这里如何帮助我? (也许是因为我在考虑真正问题的背景,但如果你能发布一个简单的例子,这将有助于我理解你的建议)

标签: scala


【解决方案1】:

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

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

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)
  }
}

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

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 只是按名称的情况)。但我将把它作为练习留给读者。

【讨论】:

    【解决方案2】:

    我不确定我是否完全理解你的最终目标是什么,但我可以向你解释为什么你会看到你所看到的。

    关于 Scala 语言的两个事实结合起来产生了您遇到的问题:

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

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

    val a: => Int = 3
    

    换句话说,虽然(=> Int) => Int 是一个类型,但=> Int 不是一个类型。不能有 => Int 类型的值。

    要了解 (2) 是否成立,请考虑使用名称参数调用方法时会发生什么:

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

    调用代码需要知道不要将() 作为参数传递,而是传递一个promise。因此,调用代码必须知道foo 采用了一个按名称参数,因此它是一个按名称参数这一事实必须是方法签名的一部分。在幕后,调用代码必须构造一个() => Unit 类型的无参数方法并传递它。

    现在,这些如何结合起来在您的代码中产生问题?

    当你写作时:

    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 所观察到的,引入值参数的一种方法是使用类型类。还有其他方法,例如类型标记或显式值参数。然而,为了改变函数的行为,你必须依赖于类型参数以外的东西。

    【讨论】:

      猜你喜欢
      • 2014-08-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-11-29
      • 2023-04-08
      • 2021-12-21
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多