Pourquoi le compilateur Scala interdit-il les méthodes surchargées avec des arguments par défaut?

148

Bien qu'il puisse y avoir des cas valides où de telles surcharges de méthodes pourraient devenir ambiguës, pourquoi le compilateur interdit-il le code qui n'est ni ambigu au moment de la compilation ni au moment de l'exécution?

Exemple:

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

Y a-t-il des raisons pour lesquelles ces restrictions ne peuvent pas être un peu assouplies?

Surtout lors de la conversion de code Java fortement surchargé en arguments par défaut Scala est très important et il n'est pas agréable de savoir après avoir remplacé de nombreuses méthodes Java par une méthode Scala que la spécification / compilateur impose des restrictions arbitraires.

soc
la source
18
"restrictions arbitraires" :-)
KajMagnus
1
Il semble que vous puissiez contourner le problème en utilisant des arguments de type. Cela compile: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: Votre astuce n'a pas fonctionné pour moi. Je l'ai essayé avec Scala 2.12.0 et Scala 2.11.8.
Landlocked Surfer le
4
À mon humble avis, c'est l'un des points de douleur les plus forts de Scala. Chaque fois que j'essaie de fournir une API flexible, je rencontre souvent ce problème, en particulier lors de la surcharge de apply () de l'objet compagnon. Bien que je préfère légèrement Scala à Kotlin, à Kotlin, vous pouvez faire ce genre de surcharge ...
laitue cubique

Réponses:

113

Je voudrais citer Lukas Rytz (à partir d' ici ):

La raison est que nous voulions un schéma de dénomination déterministe pour les méthodes générées qui retournent des arguments par défaut. Si vous écrivez

def f(a: Int = 1)

le compilateur génère

def f$default$1 = 1

Si vous avez deux surcharges avec des valeurs par défaut sur la même position de paramètre, nous aurions besoin d'un schéma de dénomination différent. Mais nous voulons garder le code d'octet généré stable sur plusieurs exécutions du compilateur.

Une solution pour la future version de Scala pourrait être d'incorporer les noms de type des arguments non par défaut (ceux au début d'une méthode, qui éliminent l'ambiguïté des versions surchargées) dans le schéma de nommage, par exemple dans ce cas:

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

ce serait quelque chose comme:

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

Quelqu'un est-il prêt à rédiger une proposition SIP ?

Eugen Labun
la source
2
Je pense que votre proposition ici a beaucoup de sens, et je ne vois pas ce qui serait si complexe à la spécifier / la mettre en œuvre. Essentiellement, les types de paramètres font partie de l'ID de la fonction. Que fait actuellement le compilateur avec foo (String) et foo (Int) (c'est-à-dire des méthodes surchargées SANS valeur par défaut)?
Mark le
Cela n'introduirait-il pas effectivement la notation hongroise obligatoire lors de l'accès aux méthodes Scala à partir de Java? Il semble que cela rendrait les interfaces extrêmement fragiles, obligeant l'utilisateur à faire attention lorsque les paramètres de type des fonctions changent.
blast_hardcheese
Et qu'en est-il des types complexes? A with B, par exemple?
blast_hardcheese
66

Il serait très difficile d'obtenir une spécification lisible et précise pour les interactions de résolution de surcharge avec les arguments par défaut. Bien sûr, pour de nombreux cas individuels, comme celui présenté ici, il est facile de dire ce qui doit se passer. Mais ce n'est pas assez. Nous aurions besoin d'une spécification qui décide de tous les cas de coin possibles. La résolution de surcharge est déjà très difficile à spécifier. L'ajout d'arguments par défaut dans le mix rendrait les choses encore plus difficiles. C'est pourquoi nous avons choisi de séparer les deux.

Martin Odersky
la source
4
Merci pour votre réponse. Ce qui m'a probablement dérouté, c'est que partout ailleurs, le compilateur ne se plaint que s'il y a effectivement une certaine ambiguïté. Mais ici, le compilateur se plaint car il pourrait y avoir des cas similaires où une ambiguïté pourrait survenir. Ainsi, dans le premier cas, le compilateur se plaint seulement s'il y a un problème avéré, mais dans le second cas, le comportement du compilateur est beaucoup moins précis et déclenche des erreurs pour du code "apparemment valide". Voyant cela avec le principe du moindre étonnement, c'est un peu malheureux.
soc
2
«Il serait très difficile d'obtenir une spécification lisible et précise [...]» signifie-t-il qu'il y a une chance réelle que la situation actuelle s'améliore si quelqu'un intensifie avec une bonne spécification et / ou implémentation? La situation actuelle limite un peu la convivialité des paramètres nommés / par défaut ...
soc
Il existe un processus pour proposer des modifications à la spécification. scala-lang.org/node/233
James Iry
2
J'ai quelques commentaires (voir mes commentaires ci-dessous la réponse liée) sur Scala rendant la surcharge désapprouvée et un citoyen de seconde zone. Si nous continuons à affaiblir délibérément la surcharge dans Scala, nous remplaçons la saisie par des noms, ce que l'OMI est une direction régressive.
Shelby Moore III
10
Si Python peut le faire, je ne vois aucune bonne raison pour laquelle Scala ne le pourrait pas. L'argument pour la complexité est bon: la mise en œuvre de cette fonctionnalité rendra Scale moins complexe du point de vue de l'utilisateur. Lisez d'autres réponses et vous verrez des gens inventer des choses très complexes juste pour résoudre un problème qui ne devrait même pas exister du point de vue des utilisateurs.
Richard Gomes
12

Je ne peux pas répondre à votre question, mais voici une solution de contournement:

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
}

Si vous avez deux très longues listes d'arguments qui diffèrent par un seul argument, cela pourrait valoir la peine ...

Landei
la source
1
Eh bien, j'ai essayé d'utiliser des arguments par défaut pour rendre mon code plus concis et plus lisible ... en fait, j'ai ajouté une conversion implicite à la classe dans un cas qui a simplement converti le type alternatif au type accepté. C'est juste moche. Et l'approche avec les arguments par défaut devrait juste fonctionner!
soc
Vous devriez faire attention à ces conversions, car ils sont valables pour toutes les utilisations Eitheret non seulement pour les foo- de cette façon, chaque fois qu'une Either[A, B]valeur est demandée, à la fois Aet Bsont acceptées. Il faut plutôt définir un type qui n'est accepté que par les fonctions ayant des arguments par défaut (comme fooici), si vous voulez aller dans cette direction; bien sûr, il devient encore moins clair s'il s'agit d'une solution pratique.
Blaisorblade
9

Ce qui a fonctionné pour moi, c'est de redéfinir (style Java) les méthodes de surcharge.

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"

Cela garantit au compilateur la résolution souhaitée en fonction des paramètres actuels.

Belka
la source
3

Voici une généralisation de la réponse @Landei:

Ce que vous voulez vraiment:

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

Solution de contournement

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é
la source
1

L'un des scénarios possibles est


  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

Le compilateur ne sait pas lequel appeler. Pour prévenir d'autres dangers possibles, le compilateur autoriserait au plus une méthode surchargée à avoir des arguments par défaut.

Juste mon avis :-)

Shiva Wu
la source
0

Je crois comprendre qu'il peut y avoir des collisions de noms dans les classes compilées avec des valeurs d'argument par défaut. J'ai vu quelque chose de ce genre mentionné dans plusieurs fils.

La spécification de l'argument nommé est ici: http://www.scala-lang.org/sites/default/files/sids/rytz/Mon,%202009-11-09,%2017:29/named-args.pdf

Il est dit:

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

Donc, pour le moment en tout cas, cela ne fonctionnera pas.

Vous pouvez faire quelque chose comme ce que vous pourriez faire en Java, par exemple:

def foo(a: String)(b: Int) =  a + (if (b > 0) b else 42)
Janx
la source