Почему компилятор 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 по умолчанию очень важно, и после замены большого количества методов Java одним методом Scala не очень удобно выяснять, что спецификация / компилятор накладывает произвольные ограничения.

Soc
источник
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
@ user1609012: Ваш трюк у меня не сработал. Я попробовал это, используя Scala 2.12.0 и Scala 2.11.8.
Не
4
ИМХО, это одна из самых сильных болей в Скале. Всякий раз, когда я пытаюсь предоставить гибкий API, я часто сталкиваюсь с этой проблемой, особенно при перегрузке объекта apply (). Хотя я немного предпочитаю Scala, а не Kotlin, в Kotlin вы можете сделать такую ​​перегрузку ...
кубический салат

Ответы:

113

Я хотел бы привести слова Лукаса Ритца ( отсюда ):

Причина в том, что нам нужна была детерминированная схема именования для сгенерированных методов, которые возвращают аргументы по умолчанию. Если ты пишешь

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
Я думаю, что ваше предложение здесь имеет большой смысл, и я не вижу, что было бы так сложно определить / реализовать его. По сути, типы параметров являются частью идентификатора функции. Что компилятор в настоящее время делает с foo (String) и foo (Int) (то есть перегруженными методами БЕЗ значения по умолчанию)?
Марк
Не приведет ли это к эффективному введению обязательной венгерской нотации при доступе к методам Scala из Java? Кажется, что это сделало бы интерфейсы чрезвычайно хрупкими, заставив пользователя позаботиться о том, чтобы изменились параметры типа для функций.
blast_hardcheese
Кроме того, как насчет сложных типов? A with B, например?
blast_hardcheese
66

Было бы очень трудно получить четкую и точную спецификацию для взаимодействия разрешения перегрузки с аргументами по умолчанию. Конечно, для многих отдельных случаев, подобных представленному здесь, легко сказать, что должно произойти. Но этого недостаточно. Нам нужна спецификация, которая решает все возможные варианты. Разрешение перегрузки уже очень сложно определить. Добавление аргументов по умолчанию в смесь усложнит ситуацию. Вот почему мы решили разделить их.

Мартин Одерский
источник
4
Спасибо за Ваш ответ. Что меня, вероятно, смутило, так это то, что компилятор в основном везде жалуется, если на самом деле есть некоторая двусмысленность. Но здесь компилятор жалуется, потому что могут быть подобные случаи, когда может возникнуть неоднозначность. Таким образом, в первом случае компилятор только жалуется, если есть проверенная проблема, но во втором случае поведение компилятора гораздо менее точно и вызывает ошибки для «на первый взгляд правильного» кода. Видя это с принципом наименьшего удивления, это немного неудачно.
Soc
2
Означает ли, что «было бы очень трудно получить читабельную и точную спецификацию [...]», есть ли реальная вероятность того, что текущая ситуация может быть улучшена, если кто-то подойдет с хорошей спецификацией и / или реализацией? Текущая ситуация imho ограничивает удобство использования именованных / стандартных параметров ...
soc
Существует процесс предложения изменений в спецификации. scala-lang.org/node/233
Джеймс Ири
2
У меня есть некоторые комментарии (см. Мои комментарии ниже связанного ответа) о том, что Scala не одобряет перегрузку и о граждане второго сорта. Если мы продолжим намеренно ослаблять перегрузку в Scala, мы заменим типизацию именами, что в IMO является регрессивным направлением.
Шелби Мур III
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
}

Если у вас есть два очень длинных списка аргументов, которые отличаются только одним аргументом, это может стоить проблем ...

Landei
источник
1
Ну, я попытался использовать аргументы по умолчанию, чтобы сделать мой код более кратким и читабельным ... на самом деле я добавил неявное преобразование в класс в одном случае, который просто преобразовал альтернативный тип в принятый тип. Это просто ужасно. И подход с аргументами по умолчанию должен просто работать!
Soc
Вы должны быть осторожны с такими преобразованиями, так как они применяются для всех видов использования, Eitherа не только для fooэтого - всякий раз, когда Either[A, B]запрашивается значение, оба Aи Bпринимаются. Вместо этого следует определить тип, который принимается только функциями, имеющими аргументы по умолчанию (как fooздесь), если вы хотите пойти в этом направлении; конечно, становится еще менее понятно, является ли это удобным решением.
Blaisorblade
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"

Это гарантирует компилятору, какое разрешение вы хотите в соответствии с существующими параметрами.

Belka
источник
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 = // ...

Workarround

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)
Janx
источник