ทำไมคอมไพเลอร์ Scala ไม่อนุญาตให้โอเวอร์โหลดวิธีที่มีอาร์กิวเมนต์เริ่มต้น


148

ในขณะที่อาจมีกรณีที่ถูกต้องซึ่งวิธีการบรรทุกเกินพิกัดดังกล่าวอาจไม่ชัดเจนทำไมคอมไพเลอร์ไม่อนุญาตรหัสที่ไม่ชัดเจนในเวลารวบรวมหรือเวลาทำงาน?

ตัวอย่าง:

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

มีเหตุผลใดบ้างที่ทำให้ข้อ จำกัด เหล่านี้ไม่สามารถคลายได้บ้าง?

โดยเฉพาะอย่างยิ่งเมื่อทำการแปลงโค้ด Java ที่มีการโอเวอร์โหลดอย่างมากเป็นอาร์กิวเมนต์เริ่มต้นของ Scala นั้นเป็นสิ่งที่สำคัญมากและมันก็ไม่ดีที่จะหาคำตอบหลังจากเปลี่ยนวิธี Java มากมายโดยวิธี Scala หนึ่งวิธีที่ spec / compiler กำหนดข้อ จำกัด โดยพลการ


18
"ข้อ จำกัด โดยพลการ" :-)
KajMagnus

1
ดูเหมือนว่าคุณสามารถแก้ไขปัญหาโดยใช้อาร์กิวเมนต์ชนิด คอมไพล์นี้: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: เคล็ดลับของคุณไม่ทำงานสำหรับฉัน ฉันลองใช้ Scala 2.12.0 และ Scala 2.11.8
นักท่องที่ไม่มีทางออกสู่ทะเล

4
IMHO นี่คือหนึ่งในจุดปวดที่แข็งแกร่งที่สุดในสกาล่า เมื่อใดก็ตามที่ฉันพยายามให้ API ที่ยืดหยุ่นฉันมักพบปัญหานี้โดยเฉพาะอย่างยิ่งเมื่อมีการใช้งานวัตถุที่แสดงร่วมมากเกินไป () แม้ว่าฉันจะชอบ Scala มากกว่า Kotlin เล็กน้อยใน Kotlin คุณสามารถทำมากไป ...
cubic lettuce

คำตอบ:


113

ฉันต้องการอ้างอิง Lukas Rytz (จากที่นี่ ):

เหตุผลก็คือเราต้องการรูปแบบการตั้งชื่อที่กำหนดไว้สำหรับวิธีการที่สร้างขึ้นซึ่งคืนค่าอาร์กิวเมนต์เริ่มต้น ถ้าคุณเขียน

def f(a: Int = 1)

คอมไพเลอร์สร้าง

def f$default$1 = 1

หากคุณมีสองโอเวอร์โหลดที่มีค่าเริ่มต้นในตำแหน่งพารามิเตอร์เดียวกันเราจะต้องมีรูปแบบการตั้งชื่อที่แตกต่างกัน แต่เราต้องการให้โค้ดไบต์ที่สร้างขึ้นมีความเสถียรในการรันหลายคอมไพเลอร์

วิธีแก้ปัญหาสำหรับรุ่น Scala ในอนาคตอาจรวมชื่อประเภทของอาร์กิวเมนต์ที่ไม่ใช่ค่าเริ่มต้น (ที่จุดเริ่มต้นของวิธีการซึ่งแก้ปัญหารุ่นที่มีการโหลดมากเกินไป) ลงใน schema การตั้งชื่อเช่นในกรณีนี้:

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

มันจะเป็นสิ่งที่ชอบ:

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

มีคนยินดีที่จะเขียนข้อเสนอ SIPหรือไม่


2
ฉันคิดว่าข้อเสนอของคุณที่นี่สมเหตุสมผลและฉันไม่เห็นว่าอะไรจะซับซ้อนในการระบุ / นำไปปฏิบัติ โดยพื้นฐานแล้วชนิดพารามิเตอร์เป็นส่วนหนึ่งของ ID ของฟังก์ชัน คอมไพเลอร์ในปัจจุบันทำอะไรกับ foo (String) และ foo (Int) (เช่นวิธีโอเวอร์โหลดโดยไม่มีค่าเริ่มต้น)
ทำเครื่องหมาย

สิ่งนี้จะไม่แนะนำสัญกรณ์ฮังการีอย่างมีประสิทธิภาพเมื่อเข้าสู่วิธีการ Scala จาก Java หรือไม่? ดูเหมือนว่ามันจะทำให้ส่วนต่อประสานที่บอบบางมากบังคับให้ผู้ใช้ดูแลเมื่อพารามิเตอร์ประเภทของฟังก์ชันเปลี่ยนไป
blast_hardcheese

แล้วประเภทที่ซับซ้อนล่ะ A with Bเช่น
blast_hardcheese

66

มันจะยากมากที่จะได้ข้อมูลจำเพาะที่อ่านได้และแม่นยำสำหรับการโต้ตอบของการแก้ปัญหาการโหลดมากเกินไปด้วยอาร์กิวเมนต์เริ่มต้น แน่นอนสำหรับหลาย ๆ กรณีเช่นเดียวกับที่นำเสนอที่นี่มันง่ายที่จะพูดว่าควรจะเกิดอะไรขึ้น แต่นั่นยังไม่เพียงพอ เราต้องการสเป็คที่จะตัดสินกรณีมุมที่เป็นไปได้ทั้งหมด การระบุความละเอียดที่มากเกินไปนั้นยากที่จะระบุ การเพิ่มอาร์กิวเมนต์เริ่มต้นในการผสมจะทำให้มันยากขึ้น นั่นเป็นเหตุผลที่เราเลือกที่จะแยกทั้งสองอย่าง


4
ขอบคุณสำหรับคำตอบ. สิ่งที่อาจทำให้ฉันสับสนก็คือโดยทั่ว ๆ ไปแล้วผู้เรียบเรียงบ่นว่าถ้ามีความกำกวมจริง ๆ แต่ที่นี่คอมไพเลอร์บ่นเพราะอาจมีกรณีที่คล้ายกันที่อาจเกิดความกำกวม ดังนั้นในกรณีแรกคอมไพเลอร์จะบ่นว่ามีปัญหาที่พิสูจน์แล้วเท่านั้น แต่ในกรณีที่สองพฤติกรรมของคอมไพเลอร์นั้นมีความแม่นยำน้อยกว่ามากและก่อให้เกิดข้อผิดพลาดสำหรับรหัส "ดูเหมือนถูกต้อง" การเห็นสิ่งนี้ด้วยหลักการแห่งความประหลาดใจอย่างน้อยที่สุดก็เป็นเรื่องที่โชคร้ายเล็กน้อย
soc

2
"มันยากมากที่จะได้ข้อมูลจำเพาะที่สามารถอ่านได้และแม่นยำ [... ]" หมายความว่ามีโอกาสจริงที่สถานการณ์ปัจจุบันอาจจะดีขึ้นหากมีคนก้าวเข้ามาด้วยคุณสมบัติและ / หรือการนำไปปฏิบัติที่ดี? สถานการณ์ปัจจุบัน IMHO จำกัด การใช้งานของชื่อพารามิเตอร์เริ่มต้น / ค่อนข้างบิต ...
SOC

มีกระบวนการเสนอการเปลี่ยนแปลงข้อมูลจำเพาะ scala-lang.org/node/233
James Iry

2
ฉันมีความคิดเห็นบางส่วน (ดูความคิดเห็นของฉันด้านล่างคำตอบที่เชื่อมโยง) เกี่ยวกับสกาลาทำให้การรับภาระมากเกินไปและเป็นพลเมืองชั้นสอง หากเรายังคงมีงานพิมพ์ล้นเกินใน Scala อย่างต่อเนื่องเราจะทำการแทนที่การพิมพ์ด้วยชื่อซึ่ง IMO เป็นทิศทางถอยหลัง
Shelby Moore III

10
ถ้า Python สามารถทำได้ฉันไม่เห็นเหตุผลที่ดีว่าทำไมสกาล่าทำไม่ได้ อาร์กิวเมนต์สำหรับความซับซ้อนนั้นเป็นสิ่งที่ดีการใช้คุณลักษณะนี้จะทำให้ Scale มีความซับซ้อนน้อยลงจากมุมมองของผู้ใช้ อ่านคำตอบอื่น ๆ แล้วคุณจะเห็นผู้ประดิษฐ์สิ่งที่ซับซ้อนมากเพียงเพื่อแก้ปัญหาซึ่งไม่ควรมีอยู่ในมุมมองของผู้ใช้
Richard Gomes

12

ฉันไม่สามารถตอบคำถามของคุณได้ แต่นี่เป็นวิธีแก้ปัญหา:

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
}

หากคุณมีสองรายการ ARG ที่ยาวมากซึ่งต่างกันใน ARG เพียงอันเดียวมันอาจคุ้มค่ากับปัญหา ...


1
ดีฉันพยายามใช้อาร์กิวเมนต์เริ่มต้นเพื่อให้รหัสของฉันกระชับและอ่านง่ายขึ้น ... จริง ๆ แล้วฉันเพิ่มการแปลงโดยนัยไปยังคลาสในกรณีหนึ่งซึ่งเพิ่งแปลงประเภทอื่นเป็นชนิดที่ยอมรับ มันรู้สึกน่าเกลียด และแนวทางที่มีค่าเริ่มต้นควรจะทำงาน!
soc

คุณควรระมัดระวังกับการแปลงเหล่านี้เนื่องจากพวกเขาใช้สำหรับการใช้งานทั้งหมดEitherและไม่เพียง แต่foo- วิธีนี้เมื่อใดก็ตามที่มีการEither[A, B]ร้องขอค่าทั้งสองAและBได้รับการยอมรับ หนึ่งควรกำหนดแทนประเภทซึ่งเป็นที่ยอมรับโดยฟังก์ชั่นที่มีข้อโต้แย้งเริ่มต้น (เช่นfooที่นี่) ถ้าคุณต้องการที่จะไปทิศทางนี้ แน่นอนมันชัดเจนน้อยลงว่านี่เป็นวิธีแก้ปัญหาที่สะดวก
Blaisorblade

9

สิ่งที่ได้ผลสำหรับฉันคือการกำหนดวิธีการโอเวอร์โหลดอีกครั้ง (สไตล์ Java)

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"

สิ่งนี้ทำให้มั่นใจได้ว่าคอมไพเลอร์ต้องการความละเอียดที่คุณต้องการตามพารามิเตอร์ปัจจุบัน


3

นี่คือลักษณะทั่วไปของ @Landei คำตอบ:

สิ่งที่คุณต้องการ:

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

หนึ่งในสถานการณ์ที่เป็นไปได้คือ


  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

คอมไพเลอร์จะสับสนเกี่ยวกับการที่จะโทร ในการป้องกันอันตรายอื่น ๆ ที่เป็นไปได้คอมไพเลอร์จะอนุญาตวิธีการโอเวอร์โหลดที่มากที่สุดหนึ่งวิธีมีอาร์กิวเมนต์เริ่มต้น

แค่เดาของฉัน :-)


0

ความเข้าใจของฉันคือว่าสามารถมีการชนกันของชื่อในชั้นเรียนที่คอมไพล์ด้วยค่าอาร์กิวเมนต์เริ่มต้น ฉันเห็นบางสิ่งตามบรรทัดเหล่านี้ที่กล่าวถึงในหลายเธรด

ข้อมูลจำเพาะอาร์กิวเมนต์ที่มีชื่ออยู่ที่นี่: http://www.scala-lang.org/sites/default/files/sids/rytz/Mon,%202009-11-09,%2017:29/named-args.pdf

มันระบุว่า:

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

ดังนั้นสำหรับเวลาที่อัตราใด ๆ ก็จะไม่ทำงาน

คุณสามารถทำสิ่งที่ชอบใน Java เช่น:

def foo(a: String)(b: Int) =  a + (if (b > 0) b else 42)
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.