Dlaczego kompilator Scala nie zezwala na przeciążone metody z domyślnymi argumentami?

148

Chociaż mogą istnieć prawidłowe przypadki, w których takie przeciążenia metod mogą stać się niejednoznaczne, dlaczego kompilator nie zezwala na kod, który nie jest niejednoznaczny w czasie kompilacji ani w czasie wykonywania?

Przykład:

// 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 ...

Czy są jakieś powody, dla których nie można nieco poluzować tych ograniczeń?

Zwłaszcza podczas konwersji mocno przeciążonego kodu Javy na domyślne argumenty Scali są bardzo ważne i nie jest przyjemnie dowiedzieć się po zastąpieniu wielu metod Javy jedną z metod Scali, że specyfikacja / kompilator nakłada dowolne ograniczenia.

soc
źródło
18
"arbitralne ograniczenia" :-)
KajMagnus
1
Wygląda na to, że możesz obejść ten problem, używając argumentów typu. To kompiluje: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: Twoja sztuczka nie zadziałała. Wypróbowałem to używając Scala 2.12.0 i Scala 2.11.8.
Landlocked Surfer
4
IMHO to jeden z najsilniejszych punktów bólu w Scali. Za każdym razem, gdy próbuję zapewnić elastyczne API, często napotykam ten problem, w szczególności podczas przeciążania metody apply () obiektu towarzyszącego. Chociaż trochę wolę Scalę od Kotlina, w Kotlinie można zrobić tego typu przeładowanie ...
sałata sześcienna

Odpowiedzi:

113

Chciałbym zacytować Lukasa Rytza ( stąd ):

Powodem jest to, że chcieliśmy mieć deterministyczny schemat nazewnictwa dla wygenerowanych metod, które zwracają domyślne argumenty. Jeśli piszesz

def f(a: Int = 1)

kompilator generuje

def f$default$1 = 1

Jeśli masz dwa przeciążenia z ustawieniami domyślnymi na tej samej pozycji parametru, potrzebowalibyśmy innego schematu nazewnictwa. Ale chcemy, aby wygenerowany kod bajtowy był stabilny podczas wielu uruchomień kompilatora.

Rozwiązaniem dla przyszłej wersji Scali mogłoby być włączenie nazw typów argumentów innych niż domyślne (te na początku metody, które ujednoznaczniają przeciążone wersje) do schematu nazewnictwa, np. W tym przypadku:

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

byłoby to coś takiego:

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

Ktoś chętny do napisania propozycji SIP ?

Eugen Labun
źródło
2
Myślę, że twoja propozycja tutaj ma dużo sensu i nie widzę, co byłoby tak skomplikowane w określaniu / wdrażaniu jej. Zasadniczo typy parametrów są częścią identyfikatora funkcji. Co kompilator obecnie robi z foo (String) i foo (Int) (tj. Przeciążone metody BEZ wartości domyślnej)?
Mark
Czy nie wprowadziłoby to skutecznie obowiązkowej notacji węgierskiej podczas uzyskiwania dostępu do metod Scala z języka Java? Wygląda na to, że uczyniłoby to interfejsy wyjątkowo delikatnymi, zmuszając użytkownika do zwracania uwagi na zmianę parametrów typu funkcji.
blast_hardcheese,
A co z typami złożonymi? A with B, na przykład?
blast_hardcheese,
66

Byłoby bardzo trudno uzyskać czytelną i dokładną specyfikację dla interakcji rozdzielczości przeciążenia z domyślnymi argumentami. Oczywiście w wielu indywidualnych przypadkach, jak ten tutaj przedstawiony, łatwo powiedzieć, co powinno się wydarzyć. Ale to nie jest wystarczające. Potrzebowalibyśmy specyfikacji, która decyduje o wszystkich możliwych przypadkach narożnych. Rozdzielczość przeciążenia jest już bardzo trudna do określenia. Dodanie domyślnych argumentów do miksu jeszcze bardziej utrudniłoby sprawę. Dlatego zdecydowaliśmy się oddzielić te dwie rzeczy.

Martin Odersky
źródło
4
Dziękuję za odpowiedź. Prawdopodobnie zmyliło mnie to, że w zasadzie wszędzie indziej kompilator narzeka tylko wtedy, gdy rzeczywiście jest jakaś niejasność. Ale tutaj kompilator narzeka, ponieważ mogą istnieć podobne przypadki, w których mogą pojawić się niejednoznaczności. Tak więc w pierwszym przypadku kompilator narzeka tylko wtedy, gdy istnieje udowodniony problem, ale w drugim przypadku zachowanie kompilatora jest znacznie mniej precyzyjne i powoduje błędy dla „pozornie prawidłowego” kodu. Patrząc na to z zasadą najmniejszego zdziwienia, jest to trochę niefortunne.
soc
2
Czy „Bardzo trudno byłoby uzyskać czytelną i precyzyjną specyfikację [...]” oznacza, że ​​istnieje rzeczywista szansa, że ​​obecna sytuacja może ulec poprawie, jeśli ktoś podejmie kroki z dobrą specyfikacją i / lub implementacją? Obecna sytuacja imho dość mocno ogranicza użyteczność parametrów nazwanych / domyślnych ...
soc
Istnieje proces proponowania zmian w specyfikacji. scala-lang.org/node/233
James Iry
2
Mam kilka komentarzy (patrz moje komentarze pod połączoną odpowiedzią) na temat Scali, która spogląda z dezaprobatą i jest obywatelem drugiej kategorii. Jeśli nadal celowo osłabiamy przeciążenie w Scali, zastępujemy pisanie nazwami, których IMO jest kierunkiem regresywnym.
Shelby Moore III
10
Jeśli Python to potrafi, nie widzę żadnego powodu, dla którego Scala by nie potrafiła. Argument za złożonością jest dobry: wdrożenie tej funkcji sprawi, że Scale będzie mniej skomplikowana z perspektywy użytkownika. Przeczytaj inne odpowiedzi, a zobaczysz, jak ludzie wymyślają bardzo złożone rzeczy, aby rozwiązać problem, który nawet nie powinien istnieć z perspektywy użytkowników.
Richard Gomes
12

Nie mogę odpowiedzieć na Twoje pytanie, ale oto obejście:

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
}

Jeśli masz dwie bardzo długie listy argumentów, które różnią się tylko jednym argumentem, może to być warte zachodu ...

Landei
źródło
1
Cóż, próbowałem użyć domyślnych argumentów, aby mój kod był bardziej zwięzły i czytelny ... w rzeczywistości dodałem niejawną konwersję do klasy w jednym przypadku, który po prostu przekonwertował typ alternatywny na typ akceptowany. To po prostu brzydkie. A podejście z domyślnymi argumentami powinno po prostu działać!
soc
Należy zachować ostrożność przy takich konwersjach, ponieważ mają one zastosowanie do wszystkich zastosowań, Eithera nie tylko do foo- w ten sposób, gdy Either[A, B]żądana jest wartość, zarówno Ai Bsą akceptowane. Zamiast tego należy zdefiniować typ, który jest akceptowany tylko przez funkcje z domyślnymi argumentami (jak footutaj), jeśli chcesz iść w tym kierunku; oczywiście staje się jeszcze mniej jasne, czy jest to wygodne rozwiązanie.
Blaisorblade
9

Udało mi się przedefiniować (w stylu Java) metody przeciążania.

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"

Zapewnia to kompilatorowi żądaną rozdzielczość zgodnie z obecnymi parametrami.

belka
źródło
3

Oto uogólnienie odpowiedzi @Landei:

Czego naprawdę chcesz:

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 = // ...

Obejście problemu

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 ..."
Guillaume Massé
źródło
1

Jednym z możliwych scenariuszy jest


  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

Kompilator nie będzie wiedział, który wywołać. Aby zapobiec innym możliwym zagrożeniom, kompilator pozwoliłby, aby co najwyżej jedna przeciążona metoda miała domyślne argumenty.

Zgaduję :-)

Shiva Wu
źródło
0

Rozumiem, że mogą wystąpić kolizje nazw w skompilowanych klasach z domyślnymi wartościami argumentów. Widziałem coś podobnego, wspomnianego w kilku wątkach.

Nazwana specyfikacja argumentu jest tutaj: http://www.scala-lang.org/sites/default/files/sids/rytz/Mon,%202009-11-09,%2017:29/named-args.pdf

W Stanach:

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

Tak więc na razie to nie zadziała.

Możesz zrobić coś takiego, co możesz zrobić w Javie, na przykład:

def foo(a: String)(b: Int) =  a + (if (b > 0) b else 42)
Janx
źródło