การแปลงโดยนัยกับคลาสประเภท


94

ใน Scala เราสามารถใช้อย่างน้อยสองวิธีในการติดตั้งประเภทที่มีอยู่หรือแบบใหม่ สมมติว่าเราต้องการแสดงว่าบางสิ่งสามารถหาปริมาณได้โดยใช้Int. เราสามารถกำหนดลักษณะต่อไปนี้

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

trait Quantifiable{ def quantify: Int }

จากนั้นเราสามารถใช้การแปลงโดยนัยเพื่อหาจำนวนเช่น Strings and Lists

implicit def string2quant(s: String) = new Quantifiable{ 
  def quantify = s.size 
}
implicit def list2quantifiable[A](l: List[A]) = new Quantifiable{ 
  val quantify = l.size 
}

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

พิมพ์คลาส

อีกทางเลือกหนึ่งคือการกำหนด "พยาน" Quantified[A]ที่ระบุว่าบางประเภทAสามารถหาปริมาณได้

trait Quantified[A] { def quantify(a: A): Int }

จากนั้นเราจะจัดเตรียมอินสแตนซ์ของคลาสประเภทนี้สำหรับStringและที่Listใดที่หนึ่ง

implicit val stringQuantifiable = new Quantified[String] {
  def quantify(s: String) = s.size 
}

และถ้าเราเขียนวิธีการที่ต้องการหาจำนวนอาร์กิวเมนต์เราจะเขียน:

def sumQuantities[A](as: List[A])(implicit ev: Quantified[A]) = 
  as.map(ev.quantify).sum

หรือใช้ไวยากรณ์ขอบเขตบริบท:

def sumQuantities[A: Quantified](as: List[A]) = 
  as.map(implicitly[Quantified[A]].quantify).sum

แต่จะใช้วิธีไหนเมื่อไหร่?

ตอนนี้มาถึงคำถาม ฉันจะตัดสินใจระหว่างสองแนวคิดนี้ได้อย่างไร

สิ่งที่ฉันสังเกตเห็นจนถึงตอนนี้

พิมพ์คลาส

  • ประเภทคลาสอนุญาตให้ใช้ไวยากรณ์ที่มีขอบเขตบริบทที่ดี
  • ด้วยคลาสประเภทฉันไม่ได้สร้างออบเจ็กต์ Wrapper ใหม่สำหรับการใช้งานแต่ละครั้ง
  • ไวยากรณ์ที่มีขอบเขตบริบทไม่ทำงานอีกต่อไปหากคลาส type มีพารามิเตอร์หลายประเภท จินตนาการฉันต้องการที่จะหาจำนวนสิ่งที่ไม่เพียง แต่มีจำนวนเต็ม Tแต่มีค่าบางประเภททั่วไป ฉันต้องการสร้างคลาสประเภทQuantified[A,T]

การแปลงโดยปริยาย

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

สิ่งที่ฉันคาดหวังจากคำตอบ

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


มีความสับสนในจุดคลาสประเภทที่คุณพูดถึง "มุมมองที่ถูกผูก" แม้ว่าคลาสประเภทจะใช้ขอบเขตบริบท
Daniel C. Sobral

1
+1 คำถามที่ยอดเยี่ยม ฉันสนใจคำตอบอย่างละเอียดเกี่ยวกับเรื่องนี้มาก
Dan Burton

@ แดเนียลขอบคุณครับ ฉันมักจะเข้าใจผิด
ziggystar

2
คุณเข้าใจผิดในที่เดียว: ในตัวอย่างการแปลงโดยนัยที่สองของคุณคุณจัดเก็บsizeรายการในมูลค่าและบอกว่าหลีกเลี่ยงการส่งผ่านรายการที่มีราคาแพงในการโทรครั้งต่อ ๆ ไปเพื่อหาปริมาณ แต่ในทุกครั้งที่คุณโทรไปยังสิ่งquantifyที่ทlist2quantifiableริกเกอร์ ทั้งหมดอีกครั้งจึงคืนสถานะQuantifiableและคำนวณquantifyทรัพย์สินใหม่ สิ่งที่ฉันกำลังพูดคือไม่มีวิธีแคชผลลัพธ์ที่มีการแปลงโดยปริยาย
Nikita Volkov

@NikitaVolkov การสังเกตของคุณถูกต้อง และฉันตอบคำถามนี้ในคำถามของฉันในวรรคสองถึงย่อหน้าสุดท้าย การแคชใช้งานได้เมื่ออ็อบเจ็กต์ที่แปลงถูกใช้งานนานขึ้นหลังจากการเรียกใช้เมธอดการแปลงหนึ่งครั้ง (และอาจถูกส่งต่อในรูปแบบที่แปลงแล้ว) ในขณะที่คลาสประเภทอาจถูกผูกมัดกับวัตถุที่ไม่ได้แปลงเมื่อลึกลงไป
ziggystar

คำตอบ:


42

แม้ว่าฉันจะไม่ต้องการทำซ้ำเนื้อหาของฉันจากScala In Depthแต่ฉันคิดว่ามันคุ้มค่าที่จะสังเกตว่าลักษณะของคลาส / ประเภทประเภทนั้นมีความยืดหยุ่นมากกว่าอย่างไม่มีที่สิ้นสุด

def foo[T: TypeClass](t: T) = ...

มีความสามารถในการค้นหาสภาพแวดล้อมโลคัลสำหรับคลาสชนิดดีฟอลต์ อย่างไรก็ตามฉันสามารถลบล้างพฤติกรรมเริ่มต้นได้ตลอดเวลาโดยหนึ่งในสองวิธี:

  1. การสร้าง / นำเข้าอินสแตนซ์คลาสประเภทโดยนัยในขอบเขตเพื่อการค้นหาโดยนัยแบบลัดวงจร
  2. ส่งผ่านคลาสประเภทโดยตรง

นี่คือตัวอย่าง:

def myMethod(): Unit = {
   // overrides default implicit for Int
   implicit object MyIntFoo extends Foo[Int] { ... }
   foo(5)
   foo(6) // These all use my overridden type class
   foo(7)(new Foo[Int] { ... }) // This one needs a different configuration
}

สิ่งนี้ทำให้คลาสประเภทมีความยืดหยุ่นมากขึ้นอย่างไร้ขีด จำกัด อีกสิ่งหนึ่งคือประเภทคลาส / ลักษณะสนับสนุนการค้นหาโดยนัยได้ดีขึ้น

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

Function1[Int, ?]

ซึ่งจะดูที่Function1วัตถุคู่หูและวัตถุที่Intแสดงร่วม

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

บนมืออื่น ๆ , ชั้นชนิดอย่างชัดเจน คุณจะเห็นสิ่งที่ต้องการในลายเซ็นของวิธีการ คุณยังมีการค้นหาโดยปริยายของไฟล์

Quantifiable[Int]

ซึ่งจะดูในQuantifiableวัตถุที่แสดงร่วมและ Intวัตถุที่แสดงร่วมกัน หมายความว่าคุณสามารถระบุค่าเริ่มต้นและประเภทใหม่ (เช่นMyStringคลาส) สามารถระบุค่าเริ่มต้นในอ็อบเจ็กต์ที่แสดงร่วมกันได้และจะถูกค้นหาโดยปริยาย

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


20

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

"my string".newFeature

... ในขณะที่ใช้คลาสประเภทจะดูเหมือนว่าคุณกำลังเรียกใช้ฟังก์ชันภายนอกเสมอ:

newFeature("my string")

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

trait Default[T] { def value : T }

implicit object DefaultInt extends Default[Int] {
  def value = 42
}

implicit def listsHaveDefault[T : Default] = new Default[List[T]] {
  def value = implicitly[Default[T]].value :: Nil
}

def default[T : Default] = implicitly[Default[T]].value

scala> default[List[List[Int]]]
resN: List[List[Int]] = List(List(42))

ตัวอย่างนี้ยังแสดงให้เห็นว่าแนวคิดมีความสัมพันธ์กันอย่างไร: คลาสประเภทจะไม่มีประโยชน์เกือบเท่าถ้าไม่มีกลไกในการสร้างอินสแตนซ์จำนวนมากอย่างไม่มีที่สิ้นสุด หากไม่มีimplicitวิธีการ (ไม่ใช่การแปลงเป็นที่ยอมรับ) ฉันสามารถมีคุณสมบัติได้หลายประเภทDefaultเท่านั้น


@Phillippe - ฉันสนใจเทคนิคที่คุณเขียนขึ้นมาก ... แต่ดูเหมือนว่าจะใช้ไม่ได้กับ Scala 2.11.6 ฉันโพสต์คำถามเพื่อขอข้อมูลอัปเดตเกี่ยวกับคำตอบของคุณ ขอบคุณล่วงหน้าหากคุณสามารถช่วย: โปรดดู: stackoverflow.com/questions/31910923/…
Chris Bedford

@ChrisBedford ฉันได้เพิ่มคำจำกัดความของdefaultผู้อ่านในอนาคต
Philippe

13

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

trait Foo1[A] { def foo(a: A): Int }  // analogous to A => Int
trait Foo0    { def foo: Int }        // analogous to Int

อินสแตนซ์ของอดีตห่อหุ้มฟังก์ชันประเภทA => Intในขณะที่อินสแตนซ์ของหลังถูกนำไปใช้กับAไฟล์. คุณสามารถดำเนินการต่อรูปแบบ ...

trait Foo2[A, B] { def foo(a: A, b: B): Int } // sort of like A => B => Int

ดังนั้นคุณอาจคิดว่าFoo1[B]การประยุกต์ใช้บางส่วนFoo2[A, B]กับAอินสแตนซ์บางส่วน เป็นตัวอย่างที่ดีของการนี้ถูกเขียนขึ้นโดยไมล์ซาบินเป็น"พึ่งพาหน้าที่ในสกาล่า"

โดยหลักการแล้วประเด็นของฉันก็คือ:

  • "pimping" ชั้นเรียน (ผ่านการแปลงโดยนัย) เป็นกรณี "ลำดับศูนย์" ...
  • การประกาศประเภทคลาสเป็นกรณี "ลำดับแรก" ...
  • ประเภทหลายพารามิเตอร์ที่มี fundeps (หรือบางอย่างเช่น fundeps) เป็นกรณีทั่วไป
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.