¿Por qué el compilador Scala no permite métodos sobrecargados con argumentos predeterminados?

148

Si bien puede haber casos válidos en los que tales sobrecargas de métodos podrían volverse ambiguas, ¿por qué el compilador no permite el código que no es ambiguo en el momento de la compilación ni en el tiempo de ejecución?

Ejemplo:

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

¿Hay alguna razón por la cual estas restricciones no se pueden aflojar un poco?

Especialmente cuando la conversión de código Java muy sobrecargado a argumentos predeterminados de Scala es muy importante y no es bueno descubrir después de reemplazar muchos métodos Java por uno de los métodos Scala que el compilador / especificación impone restricciones arbitrarias.

soc
fuente
18
"restricciones arbitrarias" :-)
KajMagnus
1
Parece que puede solucionar el problema utilizando argumentos de tipo. Esto 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
@ user1609012: Tu truco no funcionó para mí. Lo probé usando Scala 2.12.0 y Scala 2.11.8.
Surfista sin litoral
44
En mi humilde opinión, este es uno de los puntos de dolor más fuertes en Scala. Cada vez que intento proporcionar una API flexible, a menudo me encuentro con este problema, en particular al sobrecargar el objeto complementario apply (). Aunque prefiero un poco Scala sobre Kotlin, en Kotlin puedes hacer este tipo de sobrecarga ...
lechuga cúbica

Respuestas:

113

Me gustaría citar a Lukas Rytz (desde aquí ):

La razón es que queríamos un esquema de nombres determinista para los métodos generados que devuelven argumentos predeterminados. Si tú escribes

def f(a: Int = 1)

el compilador genera

def f$default$1 = 1

Si tiene dos sobrecargas con valores predeterminados en la misma posición de parámetro, necesitaríamos un esquema de nomenclatura diferente. Pero queremos mantener estable el código de bytes generado en múltiples ejecuciones del compilador.

Una solución para la futura versión de Scala podría ser incorporar nombres de tipo de los argumentos no predeterminados (los que se encuentran al comienzo de un método, que desambigua las versiones sobrecargadas) en el esquema de nombres, por ejemplo, en este caso:

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

sería algo como:

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

Alguien dispuesto a escribir una propuesta SIP ?

Eugen Labun
fuente
2
Creo que su propuesta aquí tiene mucho sentido, y no veo qué sería tan complejo especificarla / implementarla. Esencialmente, los tipos de parámetros son parte de la ID de la función. ¿Qué hace el compilador actualmente con foo (String) y foo (Int) (es decir, métodos sobrecargados SIN valor predeterminado)?
Mark
¿No introduciría esto efectivamente la notación húngara obligatoria al acceder a los métodos Scala desde Java? Parece que haría que las interfaces fueran extremadamente frágiles, lo que obligaría al usuario a tener cuidado cuando cambien los parámetros de tipo a las funciones.
blast_hardcheese
Además, ¿qué pasa con los tipos complejos? A with B, ¿por ejemplo?
blast_hardcheese
66

Sería muy difícil obtener una especificación legible y precisa para las interacciones de la resolución de sobrecarga con argumentos predeterminados. Por supuesto, para muchos casos individuales, como el presentado aquí, es fácil decir qué debería suceder. Pero eso no es suficiente. Necesitaríamos una especificación que decida todos los casos de esquina posibles. La resolución de sobrecarga ya es muy difícil de especificar. Agregar argumentos predeterminados en la mezcla lo haría aún más difícil. Es por eso que hemos optado por separar los dos.

Martin Odersky
fuente
44
Gracias por tu respuesta. Lo que probablemente me confundió fue que, básicamente, en todas partes el compilador solo se queja si realmente hay alguna ambigüedad. Pero aquí el compilador se queja porque podría haber casos similares en los que podría surgir ambigüedad. Entonces, en el primer caso, el compilador solo se queja si hay un problema comprobado, pero en el segundo caso, el comportamiento del compilador es mucho menos preciso y desencadena errores para el código "aparentemente válido". Al ver esto con el principio del menor asombro, esto es un poco desafortunado.
soc
2
¿"Sería muy difícil obtener una especificación legible y precisa" [...] significa que existe una posibilidad real de que la situación actual pueda mejorar si alguien avanza con una buena especificación y / o implementación? La situación actual en mi humilde opinión limita la capacidad de uso de parámetros con nombre / por defecto un poco ...
soc
Hay un proceso para proponer cambios a la especificación. scala-lang.org/node/233
James Iry
2
Tengo algunos comentarios (ver mis comentarios debajo de la respuesta vinculada) sobre Scala haciendo que la sobrecarga esté mal vista y sea un ciudadano de segunda clase. Si continuamos debilitando deliberadamente la sobrecarga en Scala, estamos reemplazando la escritura con nombres, lo que en mi opinión es una dirección regresiva.
Shelby Moore III el
10
Si Python puede hacerlo, no veo ninguna buena razón por la que Scala no pueda. El argumento a favor de la complejidad es bueno: implementar esta característica hará que Scale sea menos complejo desde la perspectiva del usuario. Lea otras respuestas y verá personas inventando cosas muy complejas solo para resolver un problema que ni siquiera debería existir desde la perspectiva de los usuarios.
Richard Gomes el
12

No puedo responder a su pregunta, pero aquí hay una solución 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
}

Si tiene dos listas arg muy largas que difieren en un solo argumento, podría valer la pena ...

Landei
fuente
1
Bueno, intenté usar argumentos predeterminados para hacer mi código más conciso y legible ... en realidad agregué una conversión implícita a la clase en un caso que simplemente convirtió el tipo alternativo en el tipo aceptado. Simplemente se siente feo. ¡Y el enfoque con los argumentos predeterminados debería funcionar!
soc
Usted debe tener cuidado con este tipo de conversiones, ya que se aplican para todos los usos de Eithery no sólo para foo- de esta manera, cada vez que una Either[A, B]se solicita el valor, tanto Ay Bson aceptadas. En su lugar, se debe definir un tipo que solo sea aceptado por las funciones que tienen argumentos predeterminados (como fooaquí), si desea ir en esta dirección; por supuesto, queda aún menos claro si esta es una solución conveniente.
Blaisorblade
9

Lo que funcionó para mí es redefinir (estilo Java) los métodos de sobrecarga.

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"

Esto garantiza al compilador la resolución que desea de acuerdo con los parámetros actuales.

belka
fuente
3

Aquí hay una generalización de la respuesta de @Landei:

Lo que realmente quieres:

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 ..."
Guillaume Massé
fuente
1

Uno de los posibles escenarios es


  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

El compilador estará confundido sobre a quién llamar. Para evitar otros posibles peligros, el compilador permitiría que, como máximo, un método sobrecargado tenga argumentos predeterminados.

Solo mi suposición :-)

Shiva Wu
fuente
0

Tengo entendido que puede haber colisiones de nombres en las clases compiladas con valores de argumento predeterminados. He visto algo en este sentido mencionado en varios hilos.

La especificación del argumento nombrado está aquí: http://www.scala-lang.org/sites/default/files/sids/rytz/Mon,%202009-11-09,%2017:29/named-args.pdf

Afirma:

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

Entonces, por el momento, en cualquier caso, no va a funcionar.

Podría hacer algo como lo que podría hacer en Java, por ejemplo:

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