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

168

虽然在某些情况下,这样的方法重载可能会导致歧义,但为什么编译器会禁止既不在编译时也不在运行时产生歧义的代码呢?

例子:

// 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方法后发现规范/编译器强加了任意的限制,那么这并不好。


23
任意限制。 - 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
@user1609012:你的技巧对我没用。我已经尝试了在Scala 2.12.0和Scala 2.11.8中使用它。 - Landlocked Surfer
5
在我看来,这是Scala中最大的痛点之一。每当我尝试提供一个灵活的API时,我经常遇到这个问题,特别是在重载伴生对象的apply()方法时。虽然我稍微更喜欢Scala而不是Kotlin,但是在Kotlin中你可以做到这种类型的重载... - cubic lettuce
这个问题的记录票据在 https://github.com/scala/bug/issues/8161。 - Seth Tisue
7个回答

126

我想引用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)(即没有默认值的重载方法)做了什么? - Mark
这么做似乎会在从Java访问Scala方法时引入强制使用的匈牙利命名法,这样做好像会使接口非常脆弱,迫使用户在函数类型参数发生变化时小心谨慎。 - blast_hardcheese
还有,复杂类型呢?例如 A with B - blast_hardcheese
位置不重要。只要在多个重载方法中存在默认值,编译器就会抱怨。 - undefined

71

获取与默认参数相互作用的重载决议的可读且精确规范将非常困难。当然,对于许多单独的情况,例如在此处提供的情况,很容易说出应该发生什么。但这还不够。我们需要一个规范来决定所有可能的边界情况。重载决议已经非常难以指定。将默认参数添加到混合中将使其更加困难。这就是为什么我们选择将两者分开的原因。


5
谢谢你的回答。可能让我感到困惑的是,在其他地方,只有当确实存在一些歧义时编译器才会抱怨。但在这里,编译器会抱怨,因为可能会出现类似的情况导致歧义。因此,在第一种情况下,只有在存在问题的情况下编译器才会抱怨,但在第二种情况下,编译器的行为要不太精确,它会对“看似有效”的代码触发错误。从最小惊奇原则来看,这有点令人遗憾。 - soc
2
“‘It would be very hard to get a readable and precise spec [...]’这句话的意思是,如果有人提供了一个好的规范和/或实现,那么目前的情况是否有可能得到改善?在我看来,目前的情况相当限制命名/默认参数的可用性…” - soc
2
我对Scala不支持重载函数有一些评论(请参见我在链接答案下面的评论)。如果我们继续故意削弱Scala中的重载功能,我们就是用名称替换类型,这在我看来是一个倒退的方向。 - Shelby Moore III
13
如果Python能做到,我看不出为什么Scala不能。关于复杂性的论点是有道理的:实现这个特性将使Scala从用户的角度来看更简单。阅读其他答案,你会发现人们发明非常复杂的东西来解决用户角度下本不应该存在的问题。 - Richard Gomes
这些规范可以从隐式中借用吗?对于这种问题的常见解决方案是引入一些表示参数的特质,并将各种元组的隐式转换为该特质。因此,隐式歧义的规则可以应用于默认参数。 - ayvango
显示剩余3条评论

13

我无法回答你的问题,但这里有一个解决方法:

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
}

如果你有两个非常长的参数列表,它们只在一个参数上有所不同,那么这可能值得一试...


1
嗯,我尝试使用默认参数使我的代码更简洁易读...实际上,在某种情况下,我添加了一个隐式转换到类中,它只是将另一种类型转换为可接受的类型。这感觉很丑陋。而使用默认参数的方法应该可以正常工作! - soc
你应该小心进行这样的转换,因为它们适用于 Either 的所有用法,而不仅仅是 foo - 这样,每当请求一个 Either[A, B] 值时,都会接受 AB。如果你想朝这个方向前进,应该定义一个只被具有默认参数的函数(比如这里的 foo)接受的类型;当然,这是否是一个方便的解决方案甚至更加不清楚。 - Blaisorblade

11

对我有效的方法是重新定义(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)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接