สกาล่ามองหาการปลูกฝังที่ไหน?


398

นัยคำถามที่จะมาใหม่เพื่อ Scala น่าจะเป็น: ที่ไม่ดูคอมไพเลอร์สำหรับ implicits? ฉันหมายถึงโดยปริยายเพราะคำถามดูเหมือนจะไม่ได้เกิดขึ้นอย่างสมบูรณ์ราวกับว่าไม่มีคำพูด :-) ตัวอย่างเช่นค่าสำหรับintegralด้านล่างมาจากไหน

scala> import scala.math._
import scala.math._

scala> def foo[T](t: T)(implicit integral: Integral[T]) {println(integral)}
foo: [T](t: T)(implicit integral: scala.math.Integral[T])Unit

scala> foo(0)
scala.math.Numeric$IntIsIntegral$@3dbea611

scala> foo(0L)
scala.math.Numeric$LongIsIntegral$@48c610af

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

ยกตัวอย่างเช่นscala.PredefกำหนดสองแปลงจากString: หนึ่งไปWrappedStringและอื่น ๆ StringOpsเพื่อ ทั้งชั้นเรียน แต่แบ่งปันมากในวิธีการดังนั้นทำไมไม่ Scala บ่นเกี่ยวกับความคลุมเครือเมื่อพูดเรียกmap?

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

คำตอบ:


554

ประเภทของการใช้ความหมาย

นัยใน Scala หมายถึงค่าที่สามารถส่งผ่าน "อัตโนมัติ" เพื่อพูดหรือแปลงจากประเภทหนึ่งไปเป็นอีกประเภทหนึ่งที่สร้างขึ้นโดยอัตโนมัติ

การแปลงโดยนัย

พูดสั้น ๆ มากเกี่ยวกับประเภทหลังถ้าใครเรียกวิธีการmบนวัตถุoของชั้นCและชั้นที่ไม่สนับสนุนวิธีการmแล้ว Scala จะมองหาแปลงนัยจากCสิ่งที่ไม่mสนับสนุน ตัวอย่างง่ายๆจะเป็นวิธีการmapในString:

"abc".map(_.toInt)

Stringไม่รองรับวิธีการmapนี้ แต่StringOpsทำได้และมีการแปลงโดยนัยจากStringเป็นStringOpsใช้ได้ (ดูimplicit def augmentStringที่Predef)

พารามิเตอร์โดยปริยาย

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

ในกรณีนี้เราต้องประกาศความต้องการโดยนัยเช่นfooการประกาศวิธีการ:

def foo[T](t: T)(implicit integral: Integral[T]) {println(integral)}

ดูขอบเขต

มีสถานการณ์หนึ่งที่ implicit เป็นทั้งการแปลงโดยนัยและพารามิเตอร์ implicit ตัวอย่างเช่น:

def getIndex[T, CC](seq: CC, value: T)(implicit conv: CC => Seq[T]) = seq.indexOf(value)

getIndex("abc", 'a')

วิธีการที่getIndexจะได้รับวัตถุใด ๆ Seq[T]ตราบใดที่มีการแปลงนัยใช้ได้จากชั้นเลิศ ด้วยเหตุนี้ฉันสามารถส่งผ่านStringไปยังgetIndexและมันจะทำงาน

เบื้องหลังคอมไพเลอร์ที่มีการเปลี่ยนแปลงไปseq.IndexOf(value)conv(seq).indexOf(value)

สิ่งนี้มีประโยชน์มากที่จะมีน้ำตาลซินแทคติคในการเขียน การใช้น้ำตาล syntactic นี้getIndexสามารถกำหนดเช่นนี้:

def getIndex[T, CC <% Seq[T]](seq: CC, value: T) = seq.indexOf(value)

น้ำตาลซินแทคติคนี้มีคำอธิบายว่าเป็นมุมมองที่ถูกผูกไว้คล้ายกับขอบบน ( CC <: Seq[Int]) หรือขอบล่าง ( T >: Null)

ขอบเขตของบริบท

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

Integralระดับที่คุณกล่าวถึงเป็นตัวอย่างที่คลาสสิกของรูปแบบคลาสประเภท Orderingตัวอย่างเช่นในห้องสมุดมาตรฐานสกาล่าก็คือ มีห้องสมุดที่ใช้รูปแบบนี้อย่างหนักเรียกว่า Scalaz

นี่คือตัวอย่างการใช้งาน:

def sum[T](list: List[T])(implicit integral: Integral[T]): T = {
    import integral._   // get the implicits in question into scope
    list.foldLeft(integral.zero)(_ + _)
}

นอกจากนี้ยังมีน้ำตาล syntactic สำหรับมันเรียกว่าบริบทที่ถูกผูกไว้ซึ่งทำให้มีประโยชน์น้อยลงโดยจำเป็นต้องอ้างถึงนัย การแปลงวิธีดังกล่าวจะมีลักษณะดังนี้:

def sum[T : Integral](list: List[T]): T = {
    val integral = implicitly[Integral[T]]
    import integral._   // get the implicits in question into scope
    list.foldLeft(integral.zero)(_ + _)
}

ขอบเขตของบริบทมีประโยชน์มากขึ้นเมื่อคุณต้องการส่งผ่านไปยังวิธีอื่นที่ใช้งาน ตัวอย่างเช่นวิธีการsortedเกี่ยวกับความต้องการโดยปริยายSeq Orderingในการสร้างวิธีการreverseSortหนึ่งสามารถเขียน:

def reverseSort[T : Ordering](seq: Seq[T]) = seq.sorted.reverse

เพราะOrdering[T]ถูกส่งผ่านไปโดยปริยายreverseSortจึงสามารถส่งต่อไปโดยปริยายsortedได้

Implicits มาจากไหน

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

การค้นหานี้ปฏิบัติตามกฎบางอย่างที่กำหนดว่า implicits ใดที่สามารถมองเห็นได้ ตารางต่อไปนี้แสดงให้เห็นว่าผู้รวบรวมจะค้นหา implicits จากการนำเสนอที่ยอดเยี่ยมเกี่ยวกับการ implicits โดย Josh Suereth ซึ่งฉันแนะนำให้ทุกคนที่ต้องการพัฒนาความรู้สกาล่าของพวกเขาอย่างเต็มที่ มันได้รับการเติมเต็มตั้งแต่นั้นมาพร้อมกับข้อเสนอแนะและการปรับปรุง

implicits ที่มีอยู่ภายใต้หมายเลข 1 ด้านล่างมีความสำคัญเหนือกว่าภายใต้หมายเลข 2 นอกเหนือจากนั้นหากมีข้อโต้แย้งที่มีสิทธิ์หลายอย่างซึ่งตรงกับประเภทของพารามิเตอร์ implicit หนึ่งที่เฉพาะเจาะจงที่สุดจะถูกเลือกโดยใช้กฎของการแก้ไขปัญหา ข้อมูลจำเพาะ§6.26.3) ข้อมูลรายละเอียดเพิ่มเติมสามารถพบได้ในคำถามที่ฉันลิงก์ไปที่ท้ายคำตอบนี้

  1. ดูครั้งแรกในขอบเขตปัจจุบัน
    • นัยที่กำหนดไว้ในขอบเขตปัจจุบัน
    • การนำเข้าที่ชัดเจน
    • สัญลักษณ์ตัวแทนนำเข้า
    • ขอบเขตเดียวกันในไฟล์อื่น ๆ
  2. ตอนนี้ดูประเภทที่เกี่ยวข้องใน
    • วัตถุร่วมประเภท
    • ขอบเขตของประเภทอาร์กิวเมนต์โดยนัย(2.9.1)
    • ขอบเขตโดยนัยของอาร์กิวเมนต์ชนิด(2.8.0)
    • วัตถุภายนอกสำหรับชนิดซ้อนกัน
    • มิติอื่น ๆ

ลองยกตัวอย่างสำหรับพวกเขา:

นัยที่กำหนดไว้ในขอบเขตปัจจุบัน

implicit val n: Int = 5
def add(x: Int)(implicit y: Int) = x + y
add(5) // takes n from the current scope

การนำเข้าที่ชัดเจน

import scala.collection.JavaConversions.mapAsScalaMap
def env = System.getenv() // Java map
val term = env("TERM")    // implicit conversion from Java Map to Scala Map

สัญลักษณ์ตัวแทนนำเข้า

def sum[T : Integral](list: List[T]): T = {
    val integral = implicitly[Integral[T]]
    import integral._   // get the implicits in question into scope
    list.foldLeft(integral.zero)(_ + _)
}

ขอบเขตเดียวกันในไฟล์อื่น ๆ

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

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

วัตถุร่วมของประเภท

มีโน้ตของวัตถุสองรายการที่นี่ ขั้นแรกให้มองหาคู่หูของวัตถุที่เป็น "แหล่งที่มา" ยกตัวอย่างเช่นภายในวัตถุOptionมีการแปลงส่อไปIterableดังนั้นหนึ่งสามารถเรียกIterableวิธีการในOptionหรือผ่านไปยังสิ่งที่คาดหวังว่าOption Iterableตัวอย่างเช่น:

for {
    x <- List(1, 2, 3)
    y <- Some('x')
} yield (x, y)

นิพจน์นั้นแปลโดยคอมไพเลอร์เป็น

List(1, 2, 3).flatMap(x => Some('x').map(y => (x, y)))

อย่างไรก็ตามList.flatMapคาดว่า a TraversableOnceซึ่งOptionไม่ใช่ คอมไพเลอร์นั้นจะมองเข้าไปข้างในOptionวัตถุที่เป็นเพื่อนร่วมทางและพบว่าการแปลงเป็นIterableซึ่งTraversableOnceทำให้การแสดงออกนี้ถูกต้อง

ประการที่สองวัตถุร่วมของประเภทที่คาดหวัง:

List(1, 2, 3).sorted

วิธีการที่จะใช้เวลาโดยปริยายsorted Orderingในกรณีนี้จะมีลักษณะภายในวัตถุOrderingสหายในชั้นเรียนOrderingและพบนัยOrdering[Int]มี

โปรดสังเกตว่าวัตถุที่เป็นคู่หูของคลาสพิเศษนั้นจะถูกมองด้วย ตัวอย่างเช่น:

class A(val n: Int)
object A { 
    implicit def str(a: A) = "A: %d" format a.n
}
class B(val x: Int, y: Int) extends A(y)
val b = new B(5, 2)
val s: String = b  // s == "A: 2"

นี่คือวิธีที่สกาล่าพบโดยปริยายNumeric[Int]และNumeric[Long]ในคำถามของคุณโดยวิธีการที่พวกเขาจะพบว่าข้างในไม่ได้NumericIntegral

ขอบเขตของประเภทอาร์กิวเมนต์โดยปริยาย

หากคุณมีเมธอดที่มีชนิดอาร์กิวเมนต์AขอบเขตของชนิดAนั้นจะถูกพิจารณาด้วย โดย "ขอบเขตโดยนัย" ฉันหมายความว่ากฎเหล่านี้ทั้งหมดจะถูกนำไปใช้ซ้ำ - ตัวอย่างเช่นวัตถุที่เป็นสหายของAจะถูกค้นหาโดยนัยตามกฎข้างต้น

โปรดทราบว่าสิ่งนี้ไม่ได้หมายความว่าขอบเขตโดยนัยของAจะถูกค้นหาการแปลงของพารามิเตอร์นั้น แต่เป็นการแสดงออกทั้งหมด ตัวอย่างเช่น:

class A(val n: Int) {
  def +(other: A) = new A(n + other.n)
}
object A {
  implicit def fromInt(n: Int) = new A(n)
}

// This becomes possible:
1 + new A(1)
// because it is converted into this:
A.fromInt(1) + new A(1)

มีให้ตั้งแต่ Scala 2.9.1

ขอบเขตของอาร์กิวเมนต์ชนิดโดยนัย

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

เริ่มจากการใช้งาน:

class A(val n: Int)
object A {
    implicit val ord = new Ordering[A] {
        def compare(x: A, y: A) = implicitly[Ordering[Int]].compare(x.n, y.n)
    }
}

ดังนั้นให้พิจารณาสิ่งที่เกิดขึ้นเมื่อคุณโทร

List(new A(5), new A(2)).sorted

อย่างที่เราเห็นวิธีการsortedคาดหวังOrdering[A](จริง ๆ แล้วมันคาดหวังว่าOrdering[B]ที่ไหนB >: A) ไม่มีสิ่งใดอยู่ข้างในOrderingและไม่มีประเภท "แหล่งที่มา" ที่จะดู เห็นได้ชัดว่ามันคือการหามันไว้ภายในAซึ่งเป็นอาร์กิวเมนต์ชนิดOrderingของ

และนี่ก็เป็นวิธีการวิธีการเก็บรวบรวมคาดหวังต่างๆCanBuildFromทำงาน: implicits CanBuildFromที่พบภายในวัตถุสหายกับพารามิเตอร์ประเภทของ

หมายเหตุ : Orderingถูกกำหนดเป็นtrait Ordering[T]โดยที่Tพารามิเตอร์ประเภท ก่อนหน้านี้ฉันบอกว่า Scala ดูพารามิเตอร์ประเภทภายในซึ่งไม่สมเหตุสมผลนัก นัยมองหาข้างต้นเป็นOrdering[A]ที่Aเป็นประเภทที่เกิดขึ้นจริงไม่ได้พิมพ์พารามิเตอร์: มันเป็นอาร์กิวเมนต์ชนิดOrderingไป ดูหัวข้อ 7.2 ของข้อมูลจำเพาะสกาล่า

มีให้ตั้งแต่ Scala 2.8.0

วัตถุภายนอกสำหรับประเภทซ้อนกัน

ฉันไม่ได้เห็นตัวอย่างของสิ่งนี้จริงๆ ฉันจะขอบคุณถ้ามีใครสามารถแบ่งปัน หลักการง่าย ๆ :

class A(val n: Int) {
  class B(val m: Int) { require(m < n) }
}
object A {
  implicit def bToString(b: A#B) = "B: %d" format b.m
}
val a = new A(5)
val b = new a.B(3)
val s: String = b  // s == "B: 3"

มิติอื่น ๆ

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

แก้ไข

คำถามที่น่าสนใจที่เกี่ยวข้อง:


60
ถึงเวลาที่คุณจะเริ่มใช้คำตอบของคุณในหนังสือตอนนี้มันเป็นเรื่องของการรวมเข้าด้วยกันเท่านั้น
pedrofurla

3
@pedrofurla ฉันถือว่าการเขียนหนังสือเป็นภาษาโปรตุเกส หากใครบางคนสามารถหาฉันติดต่อกับผู้เผยแพร่ด้านเทคนิค ...
Daniel C. Sobral

2
แพคเกจวัตถุของสหายของชิ้นส่วนประเภทนั้นยังถูกค้นหา lampsvn.epfl.ch/trac/scala/ticket/4427
retronym

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

2
ใช่แล้วstackoverflow.com/questions/8623055ครอบคลุมเฉพาะนั้น แต่ฉันสังเกตเห็นว่าคุณเขียนว่า "รายการต่อไปนี้มีวัตถุประสงค์เพื่อนำเสนอตามลำดับที่มาก่อน ... โปรดรายงาน" โดยทั่วไปรายการภายในควรไม่เรียงลำดับเนื่องจากทั้งหมดมีน้ำหนักเท่ากัน (อย่างน้อยใน 2.10)
Eugene Yokota

23

ฉันอยากจะพบความสำคัญของความละเอียดพารามิเตอร์นัยที่ไม่เพียง แต่ที่จะมองหาเพื่อให้ผมเขียนบล็อกโพสต์revisiting implicits โดยไม่มีภาษีนำเข้า (และความสำคัญพารามิเตอร์นัยอีกครั้งหลังจากที่ความคิดเห็นบางส่วน)

นี่คือรายการ:

  • 1) นัยที่มองเห็นได้กับขอบเขตการเรียกใช้ปัจจุบันผ่านการประกาศโลคัลการนำเข้าขอบเขตด้านนอกการสืบทอดวัตถุแพ็กเกจที่สามารถเข้าถึงได้โดยไม่ต้องมีคำนำหน้า
  • 2) ขอบเขตโดยนัยซึ่งมีการเรียงลำดับทั้งหมดของวัตถุสหายและแพคเกจวัตถุที่แบกความสัมพันธ์บางอย่างกับชนิดของ implicit ซึ่งเราค้นหา พารามิเตอร์ของมันถ้ามีและของ supertype และ supertraits ของมัน)

หากในแต่ละขั้นตอนเราพบมากกว่าหนึ่งกฎการโอเวอร์โหลดแบบสแตติกจะถูกใช้เพื่อแก้ไข


3
สิ่งนี้อาจปรับปรุงได้ถ้าคุณเขียนโค้ดบางอย่างเพียงกำหนดแพ็คเกจวัตถุลักษณะและคลาสและใช้ตัวอักษรของพวกเขาเมื่อคุณอ้างถึงขอบเขต ไม่จำเป็นต้องประกาศวิธีการใด ๆ เลยเพียงแค่ชื่อและผู้ที่ขยายใครและในขอบเขตใด
Daniel C. Sobral
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.