Чому компілятор 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, що специфікація / компілятор накладає довільні обмеження.


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 (). Хоча я трохи віддаю перевагу Скалі над Котліном, але в Котліні ви можете зробити такий вид перевантаження ...
кубічний салат

Відповіді:


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, ми замінюємо введення іменами, які IMO є регресивним напрямком.
Шелбі Мур III,

10
Якщо Python може це зробити, я не бачу жодної вагомої причини, чому Скала не могла. Аргумент складності є хорошим: реалізація цієї функції зробить масштаб менш складним з точки зору користувача. Прочитайте інші відповіді, і ви побачите, що люди вигадують дуже складні речі просто для вирішення проблеми, яка не повинна існувати навіть з точки зору користувачів.
Річард Гомес

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
}

Якщо у вас є два дуже довгі списки аргументів, які відрізняються лише одним аргументом, це може бути варте клопоту ...


1
Ну, я намагався використовувати аргументи за замовчуванням, щоб зробити свій код більш стислим і читабельним ... насправді я додав неявне перетворення до класу в одному випадку, який просто перетворив альтернативний тип у прийнятий тип. Це просто некрасиво. І підхід з аргументами за замовчуванням повинен просто працювати!
соц

Ви повинні бути обережними з такими перетвореннями, оскільки вони застосовуються для всіх цілей використання, 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"

Це гарантує компілятору, яку роздільну здатність ви хочете відповідно до наявних параметрів.


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)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.