Perché il compilatore Scala non consente metodi sovraccaricati con argomenti predefiniti?


148

Mentre potrebbero esserci casi validi in cui tali sovraccarichi del metodo potrebbero diventare ambigui, perché il compilatore non consente il codice che non è né ambiguo in fase di compilazione né in fase di esecuzione?

Esempio:

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

Ci sono dei motivi per cui queste restrizioni non possono essere allentate un po '?

Soprattutto quando si converte un codice Java pesantemente sovraccaricato in argomenti predefiniti di Scala sono molto importanti e non è bello scoprire dopo aver sostituito molti metodi Java con uno dei metodi Scala che lo spec / compilatore impone restrizioni arbitrarie.


18
"restrizioni arbitrarie" :-)
KajMagnus,

1
Sembra che tu possa aggirare il problema usando gli argomenti di tipo. Questo compila: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 il

@ user1609012: il tuo trucco non ha funzionato per me. L'ho provato usando Scala 2.12.0 e Scala 2.11.8.
Surfista senza sbocco sul mare

4
IMHO questo è uno dei punti di dolore più forti in Scala. Ogni volta che provo a fornire un'API flessibile, mi capita spesso di imbattermi in questo problema, in particolare quando si sovraccarica l'oggetto Apply (). Anche se preferisco leggermente Scala rispetto a Kotlin, a Kotlin puoi fare questo tipo di sovraccarico ...
lattuga cubica

Risposte:


113

Vorrei citare Lukas Rytz (da qui ):

Il motivo è che volevamo uno schema di denominazione deterministico per i metodi generati che restituiscono argomenti predefiniti. Se scrivi

def f(a: Int = 1)

genera il compilatore

def f$default$1 = 1

Se hai due sovraccarichi con valori predefiniti nella stessa posizione del parametro, avremmo bisogno di uno schema di denominazione diverso. Ma vogliamo mantenere stabile il codice byte generato su più esecuzioni del compilatore.

Una soluzione per la futura versione di Scala potrebbe essere quella di incorporare nello schema di denominazione nomi di tipo degli argomenti non predefiniti (quelli all'inizio di un metodo, che chiariscono le versioni sovraccaricate), ad esempio in questo caso:

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

sarebbe qualcosa del tipo:

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

Qualcuno disposto a scrivere una proposta SIP ?


2
Penso che la tua proposta qui abbia molto senso, e non vedo cosa sarebbe così complesso nel specificarla / implementarla. In sostanza, i tipi di parametro fanno parte dell'ID della funzione. Cosa fa attualmente il compilatore con foo (String) e foo (Int) (ovvero metodi sovraccaricati SENZA un valore predefinito)?
Segna il

Ciò non introdurrebbe effettivamente la notazione ungherese obbligatoria quando si accede ai metodi Scala da Java? Sembra che renderebbe le interfacce estremamente fragili, costringendo l'utente a fare attenzione quando cambiano i parametri di tipo per le funzioni.
blast_hardcheese,

Inoltre, per quanto riguarda i tipi complessi? A with B, per esempio?
blast_hardcheese,

66

Sarebbe molto difficile ottenere una specifica leggibile e precisa per le interazioni di sovraccarico della risoluzione con argomenti predefiniti. Certo, per molti casi individuali, come quello presentato qui, è facile dire cosa dovrebbe accadere. Ma questo non è abbastanza. Avremmo bisogno di una specifica che decida tutti i possibili casi angolari. La risoluzione del sovraccarico è già molto difficile da specificare. L'aggiunta di argomenti predefiniti nel mix renderebbe ancora più difficile. Ecco perché abbiamo deciso di separare i due.


4
Grazie per la tua risposta. La cosa che probabilmente mi ha confuso era che praticamente ovunque il compilatore si lamenta solo se in realtà c'è qualche ambiguità. Ma qui il compilatore si lamenta perché potrebbero esserci casi simili in cui potrebbe sorgere ambiguità. Quindi nel primo caso il compilatore si lamenta solo se esiste un problema provato, ma nel secondo caso il comportamento del compilatore è molto meno preciso e genera errori per codice "apparentemente valido". Vedendo questo con il principio del minimo stupore, questo è un po 'sfortunato.
soc

2
"Sarebbe molto difficile ottenere una specifica leggibile e precisa [...]" significa che esiste una reale possibilità che la situazione attuale possa essere migliorata se qualcuno avanza con una buona specifica e / o implementazione? L'imho della situazione attuale limita un po 'l'usabilità dei parametri nominati / predefiniti ...
soc

Esiste un processo per proporre modifiche alle specifiche. scala-lang.org/node/233
James Iry,

2
Ho alcuni commenti (vedi i miei commenti sotto la risposta collegata) sul fatto che Scala si stia aggrottando le sopracciglia e un cittadino di seconda classe. Se continuiamo a indebolire di proposito il sovraccarico in Scala, stiamo sostituendo la digitazione con i nomi, che IMO è una direzione regressiva.
Shelby Moore III,

10
Se Python può farlo, non vedo alcuna buona ragione per cui Scala non sia riuscita. L'argomento per la complessità è buono: l'implementazione di questa funzione renderà la scala meno complessa dal punto di vista dell'utente. Leggi le altre risposte e vedrai le persone inventare cose molto complesse solo per risolvere un problema che non dovrebbe nemmeno esistere dal punto di vista degli utenti.
Richard Gomes,

12

Non riesco a rispondere alla tua domanda, ma ecco una soluzione alternativa:

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
}

Se hai due liste arg molto lunghe che differiscono in una sola arg, potrebbe valere la pena ...


1
Bene, ho provato a usare argomenti predefiniti per rendere il mio codice più conciso e leggibile ... in realtà ho aggiunto una conversione implicita alla classe in un caso che ha semplicemente convertito il tipo alternativo nel tipo accettato. Sembra brutto. E l'approccio con gli arg predefiniti dovrebbe funzionare!
soc

Dovresti stare attento con tali conversioni, poiché si applicano a tutti gli usi Eithere non solo per foo- in questo modo, ogni volta che Either[A, B]viene richiesto un valore, entrambi Ae Bsono accettati. Si dovrebbe invece definire un tipo che è accettato solo da funzioni con argomenti predefiniti (come fooqui), se si vuole andare in questa direzione; ovviamente, diventa ancora meno chiaro se questa sia una soluzione conveniente.
Blaisorblade,

9

Ciò che ha funzionato per me è ridefinire (in stile Java) i metodi di sovraccarico.

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"

Ciò garantisce al compilatore la risoluzione desiderata in base ai parametri attuali.


3

Ecco una generalizzazione della risposta @Landei:

Cosa vuoi veramente:

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

Uno dei possibili scenari è


  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

Il compilatore sarà confuso su quale chiamare. Nella prevenzione di altri possibili pericoli, il compilatore consentirebbe al massimo che un metodo sovraccarico abbia argomenti predefiniti.

Solo la mia ipotesi :-)


0

La mia comprensione è che ci possono essere collisioni di nomi nelle classi compilate con valori di argomento predefiniti. Ho visto qualcosa di simile menzionato in vari thread.

La specifica dell'argomento denominato è qui: http://www.scala-lang.org/sites/default/files/sids/rytz/Mon,%202009-11-09,%2017:29/named-args.pdf

Afferma:

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

Quindi, per il momento, in ogni caso, non funzionerà.

Potresti fare qualcosa di simile a quello che potresti fare in Java, ad esempio:

def foo(a: String)(b: Int) =  a + (if (b > 0) b else 42)
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.