为什么Scala编译器不允许使用默认参数重载方法?


148

尽管在某些情况下,这种方法重载可能会变得模棱两可,但为什么编译器却不允许在编译时和运行时都模棱两可的代码呢?

例:

// This fails:
def foo(a: String)(b: Int = 42) = a + b
def foo(a: Int)   (b: Int = 42) = a + b

// This fails, too. Even if there is no position in the argument list,
// where the types are the same.
def foo(a: Int)   (b: Int = 42) = a + b
def foo(a: String)(b: String = "Foo") = a + b

// This is OK:
def foo(a: String)(b: Int) = a + b
def foo(a: Int)   (b: Int = 42) = a + b    

// Even this is OK.
def foo(a: Int)(b: Int) = a + b
def foo(a: Int)(b: String = "Foo") = a + b

val bar = foo(42)_ // This complains obviously ...

有什么理由不能放宽这些限制?

特别是在将重载的Java代码转换为Scala时,默认参数非常重要,并且在用规范/编译器施加了任意限制的一个Scala方法替换了大量Java方法后,很难找到默认参数。


18
“任意限制” :-)
KajMagnus

1
看起来您可以使用类型参数解决问题。编译:object Test { def a[A](b: Int, c: Int, d: Int = 7): Unit = {}; def a[A](a:String, b: String = ""): Unit = {}; a(2,3,4); a("a");}
user1609012 '16

@ user1609012:您的把戏对我不起作用。我使用Scala 2.12.0和Scala 2.11.8进行了尝试。
内陆冲浪者

4
恕我直言,这是Scala中最强的痛点之一。每当我尝试提供灵活的API时,都会经常遇到此问题,尤其是在重载伴随对象的apply()时。尽管我稍微喜欢Scala而不是Kotlin,但在Kotlin中,您可以进行这种重载……
莴苣

Answers:


113

我想引用Lukas Rytz(从这里开始):

原因是我们希望为返回默认参数的生成方法确定性的命名方案。如果你写

def f(a: Int = 1)

编译器生成

def f$default$1 = 1

如果您在相同的参数位置上有两个具有默认值的重载,我们将需要不同的命名方案。但是我们希望在多个编译器运行中保持生成的字节码稳定。

将来的Scala版本的解决方案可能是将非默认参数的类型名称(方法开头的名称,消除了重载版本的歧义)合并到命名模式中,例如,在这种情况下:

def foo(a: String)(b: Int = 42) = a + b
def foo(a: Int)   (b: Int = 42) = a + b

就像这样:

def foo$String$default$2 = 42
def foo$Int$default$2 = 42

有人愿意写SIP提案吗?


2
我认为您的建议在这里很有道理,并且我不认为指定/实现它会那么复杂。本质上,参数类型是函数ID的一部分。编译器当前如何处理foo(String)和foo(Int)(即,没有默认值的重载方法)?
2014年

从Java访问Scala方法时,这不会有效地引入强制性匈牙利符号吗?看来这会使接口变得非常脆弱,迫使用户在更改函数的类型参数时要格外小心。
blast_hardcheese 2014年

另外,复杂类型呢?A with B, 例如?
blast_hardcheese 2014年

66

对于重载分辨率与默认参数的交互,很难获得一个可读且精确的规范。当然,对于许多个别情况,例如此处介绍的情况,很容易说出应该发生的情况。但这还不够。我们需要一个确定所有可能的极端情况的规范。重载分辨率已经很难指定。在混合中添加默认参数将使其更加困难。这就是为什么我们选择将两者分开。


4
感谢您的回答。可能让我感到困惑的是,基本上在其他所有地方,编译器只会抱怨确实存在歧义。但是在这里编译器会抱怨,因为在类似情况下可能会产生歧义。因此,在第一种情况下,编译器仅抱怨是否存在已证明的问题,而在第二种情况下,编译器的行为不太精确,并会触发“看似有效”的代码错误。出于最小惊讶的原则看到这一点,这有点不幸。
SOC

2
难道“很难获得一个可读且精确的规范”是否意味着如果有人加强了良好的规范和/或实现,那么实际情况就有可能得到改善吗?目前的情况imho大大限制了命名/默认参数的可用性...
soc

有一个提议对规范进行更改的过程。 scala-lang.org/node/233
James Iry

2
对Scala使超载感到不满,并且对二等公民有一些评论(请参阅链接的答案下面的评论)。如果我们继续有意地减少Scala中的重载,我们将用名称代替键入,这是IMO的回归方向。
谢尔比·摩尔三世

10
如果Python可以做到,我看不出Scala无法做到的任何充分理由。关于复杂性的说法很不错:从用户的角度来看,实现此功能将使Scale的复杂性降低。阅读其他答案,您会发现人们发明了非常复杂的东西只是为了解决从用户的角度来看甚至不应该存在的问题。
理查德·戈麦斯

12

我无法回答您的问题,但这是一种解决方法:

implicit def left2Either[A,B](a:A):Either[A,B] = Left(a)
implicit def right2Either[A,B](b:B):Either[A,B] = Right(b)

def foo(a: Either[Int, String], b: Int = 42) = a match {
  case Left(i) => i + b
  case Right(s) => s + b
}

如果您有两个很长的arg列表,而它们之间只有一个arg,则可能会遇到麻烦...


1
好吧,我尝试使用默认参数使我的代码更简明易读……实际上,在一种情况下,我向类添加了隐式转换,只是将替代类型转换为可接受的类型。感觉很丑。使用默认参数的方法应该可以正常工作!
soc

你应该小心这样的转换,因为它们适用于所有用途Either,而不是只为foo-这样一来,每当Either[A, B]值要求,无论是AB被接受。foo如果您想朝这个方向发展,应该定义一个只有具有默认参数的函数才能接受的类型(例如此处)。当然,这是否是一个方便的解决方案,甚至还不清楚。
Blaisorblade 2012年

9

对我有用的是重新定义(Java风格的)重载方法。

def foo(a: Int, b: Int) = a + b
def foo(a: Int, b: String) = a + b
def foo(a: Int) = a + "42"
def foo(a: String) = a + "42"

这样可以确保编译器根据当前参数所需的分辨率。


3

这是@Landei答案的概括:

您真正想要的是:

def pretty(tree: Tree, showFields: Boolean = false): String = // ...
def pretty(tree: List[Tree], showFields: Boolean = false): String = // ...
def pretty(tree: Option[Tree], showFields: Boolean = false): String = // ...

工作环境

def pretty(input: CanPretty, showFields: Boolean = false): String = {
  input match {
    case TreeCanPretty(tree)       => prettyTree(tree, showFields)
    case ListTreeCanPretty(tree)   => prettyList(tree, showFields)
    case OptionTreeCanPretty(tree) => prettyOption(tree, showFields)
  }
}

sealed trait CanPretty
case class TreeCanPretty(tree: Tree) extends CanPretty
case class ListTreeCanPretty(tree: List[Tree]) extends CanPretty
case class OptionTreeCanPretty(tree: Option[Tree]) extends CanPretty

import scala.language.implicitConversions
implicit def treeCanPretty(tree: Tree): CanPretty = TreeCanPretty(tree)
implicit def listTreeCanPretty(tree: List[Tree]): CanPretty = ListTreeCanPretty(tree)
implicit def optionTreeCanPretty(tree: Option[Tree]): CanPretty = OptionTreeCanPretty(tree)

private def prettyTree(tree: Tree, showFields: Boolean): String = "fun ..."
private def prettyList(tree: List[Tree], showFields: Boolean): String = "fun ..."
private def prettyOption(tree: Option[Tree], showFields: Boolean): String = "fun ..."

1

一种可能的情况是


  def foo(a: Int)(b: Int = 10)(c: String = "10") = a + b + c
  def foo(a: Int)(b: String = "10")(c: Int = 10) = a + b + c

编译器会困惑于要调用哪一个。为了防止其他可能的危险,编译器将最多允许一个重载的方法具有默认参数。

只是我的猜测:-)


0

我的理解是,使用默认参数值的已编译类中可能存在名称冲突。我已经看到了一些主题中提到的类似内容。

命名参数规范位于此处:http : //www.scala-lang.org/sites/default/files/sids/rytz/Mon,%202009-11-09,%2017 :29/named-args.pdf

它指出:

 Overloading If there are multiple overloaded alternatives of a method, at most one is
 allowed to specify default arguments.

因此,目前无论如何,它是行不通的。

您可以执行类似于Java中的操作,例如:

def foo(a: String)(b: Int) =  a + (if (b > 0) b else 42)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.