Por que o compilador Scala não permite métodos sobrecarregados com argumentos padrão?

148

Embora possa haver casos válidos em que essas sobrecargas de método possam se tornar ambíguas, por que o compilador desaprova o código que não é ambíguo no tempo de compilação nem no tempo de execução?

Exemplo:

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

Existem razões pelas quais essas restrições não podem ser um pouco mais flexíveis?

Especialmente ao converter códigos Java altamente sobrecarregados em argumentos padrão do Scala, é muito importante e não é bom descobrir após a substituição de muitos métodos Java por um método Scala que o spec / compiler impõe restrições arbitrárias.

soc
fonte
18
"restrições arbitrárias" :-)
KajMagnus 18/02
1
Parece que você pode solucionar o problema usando argumentos de tipo. Isso 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 23/11
@ user1609012: Seu truque não funcionou para mim. Eu tentei usando o Scala 2.12.0 e o Scala 2.11.8.
Surfista sem litoral
4
IMHO, este é um dos pontos mais fortes em Scala. Sempre que tento fornecer uma API flexível, frequentemente encontro esse problema, principalmente ao sobrecarregar o apply () do objeto complementar. Embora eu prefira um pouco Scala sobre Kotlin, em Kotlin você pode fazer este tipo de sobrecarga ...
alface cúbico

Respostas:

113

Gostaria de citar Lukas Rytz ( daqui ):

A razão é que queríamos um esquema de nomeação determinístico para os métodos gerados que retornam argumentos padrão. Se você escrever

def f(a: Int = 1)

o compilador gera

def f$default$1 = 1

Se você tiver duas sobrecargas com padrões na mesma posição de parâmetro, precisaríamos de um esquema de nomeação diferente. Mas queremos manter o código de bytes gerado estável em várias execuções do compilador.

Uma solução para a versão futura do Scala poderia ser incorporar nomes de tipo dos argumentos não padrão (aqueles no início de um método, que desambiguam versões sobrecarregadas) no esquema de nomenclatura, por exemplo, neste caso:

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

seria algo como:

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

Alguém disposto a escrever uma proposta SIP ?

Eugen Labun
fonte
2
Eu acho que sua proposta aqui faz muito sentido e não vejo o que seria tão complexo em especificá-la / implementá-la. Essencialmente, os tipos de parâmetros fazem parte do ID da função. O que o compilador atualmente faz com foo (String) e foo (Int) (isto é, métodos sobrecarregados sem um padrão)?
Mark
Isso não introduziria efetivamente a notação húngara obrigatória ao acessar os métodos Scala a partir de Java? Parece que isso tornaria as interfaces extremamente frágeis, forçando o usuário a tomar cuidado ao alterar parâmetros de tipo para funções.
Blast_hardcheese
Além disso, e os tipos complexos? A with B, por exemplo?
Blast_hardcheese
66

Seria muito difícil obter uma especificação legível e precisa para as interações da sobrecarga de resolução com argumentos padrão. Obviamente, para muitos casos individuais, como o apresentado aqui, é fácil dizer o que deve acontecer. Mas isso não é suficiente. Precisávamos de uma especificação que decida todos os casos de canto possíveis. A resolução de sobrecarga já é muito difícil de especificar. A adição de argumentos padrão no mix tornaria ainda mais difícil. Por isso, optamos por separar os dois.

Martin Odersky
fonte
4
Obrigado pela sua resposta. O que provavelmente me confundiu foi que basicamente em qualquer outro lugar o compilador reclama apenas se houver alguma ambiguidade. Mas aqui o compilador se queixa porque pode haver casos semelhantes em que a ambiguidade pode surgir. Portanto, no primeiro caso, o compilador reclama apenas se houver um problema comprovado, mas no segundo caso, o comportamento do compilador é muito menos preciso e aciona erros para o código "aparentemente válido". Vendo isso com o princípio do mínimo espanto, isso é um pouco lamentável.
soc
2
"Seria muito difícil obter uma especificação legível e precisa" [...] significa que há uma chance real de que a situação atual possa ser melhorada se alguém apresentar uma boa especificação e / ou implementação? A corrente imho situação limita a usabilidade de parâmetros nomeados / padrão um pouco ...
soc
Existe um processo para propor alterações nas especificações. scala-lang.org/node/233
James Iry em
2
Tenho alguns comentários (veja meus comentários abaixo da resposta vinculada) sobre o Scala fazer com que a sobrecarga seja desaprovada e um cidadão de segunda classe. Se continuarmos enfraquecendo propositalmente a sobrecarga no Scala, estamos substituindo a digitação por nomes, que IMO é uma direção regressiva.
precisa saber é o seguinte
10
Se o Python pode fazer, não vejo nenhuma boa razão para o Scala não poder. O argumento da complexidade é bom: a implementação desse recurso tornará o Scale menos complexo da perspectiva do usuário. Leia outras respostas e você verá pessoas inventando coisas muito complexas apenas para resolver um problema que nem deveria existir da perspectiva dos usuários.
Richard Gomes
12

Não consigo responder à sua pergunta, mas aqui está uma solução 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 você tiver duas listas de argumentos muito longas que diferem em apenas um argumento, pode valer a pena ...

Landei
fonte
1
Bem, tentei usar argumentos padrão para tornar meu código mais conciso e legível ... na verdade, adicionei uma conversão implícita à classe em um caso, que acabou de converter o tipo alternativo para o tipo aceito. Parece feio. E a abordagem com os argumentos padrão deve funcionar!
soc 10/01
Você deve ter cuidado com essas conversões, pois elas se aplicam a todos os usos Eithere não apenas a foo- dessa maneira, sempre que um Either[A, B]valor é solicitado, ambos Ae Bsão aceitos. Em vez disso, deve-se definir um tipo que seja aceito apenas por funções com argumentos padrão (como fooaqui), se você quiser ir nessa direção; é claro, fica ainda menos claro se essa é uma solução conveniente.
Blaisorblade 13/01/12
9

O que funcionou para mim é redefinir (no estilo Java) os 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"

Isso garante ao compilador qual resolução você deseja de acordo com os parâmetros presentes.

Belka
fonte
3

Aqui está uma generalização da resposta @Landei:

O que você realmente quer:

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

Solução alternativa

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é
fonte
1

Um dos cenários possíveis é


  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

O compilador ficará confuso sobre qual chamar. Na prevenção de outros perigos possíveis, o compilador permitiria no máximo um método sobrecarregado ter argumentos padrão.

Apenas meu palpite :-)

Shiva Wu
fonte
0

Meu entendimento é que pode haver colisões de nomes nas classes compiladas com valores de argumento padrão. Eu vi algo nesse sentido mencionado em vários tópicos.

A especificação do argumento nomeado está aqui: 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.

Então, por enquanto, de qualquer forma, não vai funcionar.

Você pode fazer algo parecido com o que você pode fazer em Java, por exemplo:

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