Mengapa kompilator Scala melarang metode kelebihan beban dengan argumen default?

148

Meskipun mungkin ada kasus yang valid di mana metode overloading bisa menjadi ambigu, mengapa kompiler melarang kode yang tidak ambigu pada waktu kompilasi atau pada saat run time?

Contoh:

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

Adakah alasan mengapa pembatasan ini tidak bisa dilonggarkan sedikit?

Terutama ketika mengonversi kode Java yang kelebihan beban ke argumen default Scala adalah sangat penting dan tidak baik untuk mengetahuinya setelah mengganti banyak metode Java dengan satu metode Scala yang spek / kompiler memberlakukan pembatasan sewenang-wenang.

soc
sumber
18
"batasan arbitrer" :-)
KajMagnus
1
Sepertinya Anda bisa menyelesaikan masalah menggunakan argumen tipe. Ini mengkompilasi: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: Trik Anda tidak berhasil untuk saya. Saya mencobanya menggunakan Scala 2.12.0 dan Scala 2.11.8.
Surfer yang Terkurung daratan
4
IMHO ini adalah salah satu poin rasa sakit terkuat di Scala. Setiap kali saya mencoba memberikan API yang fleksibel, saya sering mengalami masalah ini, khususnya ketika membebani objek pendamping yang berlaku (). Meskipun saya sedikit lebih suka Scala daripada Kotlin, di Kotlin Anda dapat melakukan overloading semacam ini ...
selada kubik

Jawaban:

113

Saya ingin mengutip Lukas Rytz (dari sini ):

Alasannya adalah bahwa kami ingin skema penamaan deterministik untuk metode yang dihasilkan yang mengembalikan argumen default. Jika kamu menulis

def f(a: Int = 1)

kompiler menghasilkan

def f$default$1 = 1

Jika Anda memiliki dua kelebihan beban dengan pengaturan default pada posisi parameter yang sama, kami membutuhkan skema penamaan yang berbeda. Tetapi kami ingin menjaga agar kode byte yang dihasilkan lebih stabil dari beberapa proses kompiler.

Solusi untuk versi Scala di masa depan bisa dengan memasukkan nama tipe dari argumen non-default (yang di awal metode, yang mendambiguasi versi kelebihan beban) ke dalam skema penamaan, misalnya dalam kasus ini:

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

itu akan menjadi seperti:

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

Seseorang mau menulis proposal SIP ?

Eugen Labun
sumber
2
Saya pikir proposal Anda di sini sangat masuk akal, dan saya tidak melihat apa yang akan begitu rumit dalam menentukan / mengimplementasikannya. Pada dasarnya, tipe-tipe parameter adalah bagian dari ID fungsi. Apa yang dilakukan kompiler dengan foo (String) dan foo (Int) (yaitu, metode kelebihan beban TANPA default)?
Mark
Tidakkah ini secara efektif memperkenalkan Notasi Hongaria yang wajib ketika mengakses metode Scala dari Jawa? Sepertinya itu akan membuat antarmuka sangat rapuh, memaksa pengguna untuk berhati-hati ketika mengetik parameter ke fungsi berubah.
blast_hardcheese
Juga, bagaimana dengan tipe kompleks? A with B, misalnya?
blast_hardcheese
66

Akan sangat sulit untuk mendapatkan spesifikasi yang dapat dibaca dan tepat untuk interaksi resolusi kelebihan beban dengan argumen default. Tentu saja, untuk banyak kasus individual, seperti yang disajikan di sini, mudah untuk mengatakan apa yang harus terjadi. Tetapi itu tidak cukup. Kami membutuhkan spesifikasi yang memutuskan semua kasus sudut yang memungkinkan. Resolusi overloading sudah sangat sulit ditentukan. Menambahkan argumen default dalam campuran akan membuatnya lebih sulit lagi. Itu sebabnya kami memilih untuk memisahkan keduanya.

Martin Odersky
sumber
4
Terima kasih atas jawaban anda. Hal yang mungkin membingungkan saya adalah bahwa pada dasarnya di mana-mana kompiler hanya mengeluh jika sebenarnya ada beberapa ambiguitas. Tapi di sini kompiler mengeluh karena mungkin ada kasus serupa di mana ambiguitas dapat muncul. Jadi dalam kasus pertama kompiler hanya mengeluh jika ada masalah yang terbukti, tetapi dalam kasus kedua perilaku kompiler jauh kurang tepat dan memicu kesalahan untuk kode "yang tampaknya valid". Melihat ini dengan prinsip yang paling tidak mengejutkan, ini agak disayangkan.
soc
2
Apakah "Akan sangat sulit untuk mendapatkan spesifikasi yang dapat dibaca dan tepat [...]" berarti ada kemungkinan aktual bahwa situasi saat ini dapat ditingkatkan jika seseorang meningkatkan spesifikasi dan / atau implementasi yang baik? Situasi saat ini membatasi sedikit kegunaan parameter bernama / standar ...
soc
Ada proses untuk mengusulkan perubahan pada spesifikasi. scala-lang.org/node/233
James Iry
2
Saya punya beberapa komentar (lihat komentar saya di bawah jawaban terkait) tentang Scala membuat overloading disukai dan warga negara kelas dua. Jika kami terus dengan sengaja melemahkan kelebihan beban di Scala, kami mengganti mengetik dengan nama, yang IMO merupakan arah regresif.
Shelby Moore III
10
Jika Python bisa melakukannya, saya tidak melihat alasan bagus mengapa Scala tidak bisa. Argumen untuk kompleksitas adalah argumen yang bagus: menerapkan fitur ini akan membuat Skala lebih kompleks dari perspektif pengguna. Baca jawaban lain dan Anda akan melihat orang-orang menciptakan hal-hal yang sangat kompleks hanya untuk menyelesaikan masalah yang seharusnya tidak ada dari perspektif pengguna.
Richard Gomes
12

Saya tidak bisa menjawab pertanyaan Anda, tetapi ini solusinya:

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
}

Jika Anda memiliki dua daftar arg yang sangat panjang yang berbeda hanya dalam satu arg, itu mungkin sepadan dengan masalahnya ...

Landei
sumber
1
Yah, saya mencoba menggunakan argumen default untuk membuat kode saya lebih ringkas dan mudah dibaca ... sebenarnya saya menambahkan konversi implisit ke kelas dalam satu kasus yang hanya mengubah tipe alternatif ke jenis yang diterima. Rasanya jelek. Dan pendekatan dengan arg default seharusnya bekerja!
soc
Anda harus berhati-hati dengan konversi semacam itu, karena konversi tersebut berlaku untuk semua penggunaan Eitherdan bukan hanya untuk foo- dengan cara ini, kapan pun suatu Either[A, B]nilai diminta, keduanya Adan Bditerima. Seseorang seharusnya mendefinisikan tipe yang hanya diterima oleh fungsi yang memiliki argumen default (seperti di foosini), jika Anda ingin pergi ke arah ini; tentu saja, menjadi semakin tidak jelas apakah ini solusi yang nyaman.
Blaisorblade
9

Apa yang berhasil bagi saya adalah mendefinisikan kembali (gaya Java) metode overloading.

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"

Ini memastikan kompiler resolusi apa yang Anda inginkan sesuai dengan parameter yang ada.

belka
sumber
3

Berikut adalah generalisasi jawaban @Landei:

Apa yang benar-benar Anda inginkan:

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

Salah satu skenario yang mungkin adalah


  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

Kompiler akan bingung tentang yang harus dipanggil. Dalam pencegahan kemungkinan bahaya lain, kompiler akan memungkinkan paling banyak satu metode kelebihan beban memiliki argumen default.

Hanya tebakan saya :-)

Siwa Wu
sumber
0

Pemahaman saya adalah bahwa mungkin ada tabrakan nama di kelas yang dikompilasi dengan nilai argumen default. Saya telah melihat sesuatu di sepanjang baris ini yang disebutkan dalam beberapa utas.

Spec argumen bernama di sini: http://www.scala-lang.org/sites/default/files/sids/rytz/Mon,%202009-11-09,%2017:29/named-args.pdf

Ini menyatakan:

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

Jadi, untuk sementara waktu, itu tidak akan berhasil.

Anda dapat melakukan sesuatu seperti apa yang mungkin Anda lakukan di Jawa, misalnya:

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