ผมอ่านทัวร์ของ Scala: นามธรรมประเภท เมื่อใดควรใช้ประเภทนามธรรม
ตัวอย่างเช่น,
abstract class Buffer {
type T
val element: T
}
ค่อนข้างทั่วไปเช่นนั้น
abstract class Buffer[T] {
val element: T
}
ผมอ่านทัวร์ของ Scala: นามธรรมประเภท เมื่อใดควรใช้ประเภทนามธรรม
ตัวอย่างเช่น,
abstract class Buffer {
type T
val element: T
}
ค่อนข้างทั่วไปเช่นนั้น
abstract class Buffer[T] {
val element: T
}
คำตอบ:
คุณมีมุมมองที่ดีเกี่ยวกับปัญหานี้ที่นี่:
วัตถุประสงค์ของระบบประเภทของสกาล่า
การสนทนากับ Martin Odersky ตอนที่สาม
โดย Bill Venners และ Frank Sommers (18 พฤษภาคม 2009)
Update (October2009): สิ่งที่ตามมาด้านล่างมีการแสดงจริงในบทความใหม่นี้โดย Bill Venners:
สมาชิกประเภทบทคัดย่อกับพารามิเตอร์ประเภททั่วไปใน Scala (ดูสรุปที่ส่วนท้าย)
(นี่คือสารสกัดที่เกี่ยวข้องของการสัมภาษณ์ครั้งแรกพฤษภาคม 2009 เน้นเหมือง)
มีสองแนวคิดเกี่ยวกับสิ่งที่เป็นนามธรรมเสมอ:
ใน Java คุณมีทั้งสองอย่าง แต่มันก็ขึ้นอยู่กับสิ่งที่คุณเป็นนามธรรม
ใน Java คุณมีวิธีนามธรรม แต่คุณไม่สามารถส่งเมธอดเป็นพารามิเตอร์ได้
คุณไม่มีฟิลด์บทคัดย่อ แต่คุณสามารถส่งค่าเป็นพารามิเตอร์ได้
และในทำนองเดียวกันคุณไม่มีสมาชิกประเภท abstract แต่คุณสามารถระบุประเภทเป็นพารามิเตอร์ได้
ดังนั้นใน Java คุณมีทั้งสามอย่างนี้ แต่มีความแตกต่างเกี่ยวกับหลักการที่เป็นนามธรรมที่คุณสามารถใช้กับสิ่งต่าง ๆ และคุณสามารถยืนยันได้ว่าความแตกต่างนี้ค่อนข้างมีข้อ จำกัด
เราตัดสินใจที่จะมีหลักการก่อสร้างเดียวกันสำหรับทั้งสามประเภทของสมาชิก
ดังนั้นคุณสามารถมีฟิลด์บทคัดย่อรวมถึงพารามิเตอร์ค่า
คุณสามารถส่งเมธอด (หรือ "ฟังก์ชั่น") เป็นพารามิเตอร์หรือคุณสามารถสรุปได้
คุณสามารถระบุประเภทเป็นพารามิเตอร์หรือคุณสามารถสรุปได้
และสิ่งที่เราได้รับแนวความคิดก็คือเราสามารถจำลองแบบหนึ่งในแง่ของสิ่งอื่น อย่างน้อยที่สุดในหลักการเราสามารถแสดงการกำหนดพารามิเตอร์ทุกประเภทเป็นรูปแบบของนามธรรมเชิงวัตถุ ดังนั้นในแง่หนึ่งคุณอาจพูดได้ว่า Scala เป็นภาษาที่มีฉากและสมบูรณ์มากกว่า
โดยเฉพาะอย่างยิ่งสิ่งที่เป็นนามธรรมซื้อคุณคือการรักษาที่ดีสำหรับปัญหาความแปรปรวนร่วมที่เราพูดถึงก่อน
ปัญหามาตรฐานหนึ่งปัญหาที่เกิดขึ้นมานานแล้วคือปัญหาของสัตว์และอาหาร
จิ๊กซอว์จะต้องมีชั้นเรียนAnimal
ด้วยวิธีการeat
ซึ่งกินอาหารบางอย่าง
ปัญหาคือถ้าเรา subclass สัตว์และมีคลาสเช่นวัวแล้วพวกเขาจะกินหญ้าเท่านั้นและไม่ใช่อาหารโดยพลการ ยกตัวอย่างเช่นวัวไม่สามารถกินปลาได้
สิ่งที่คุณต้องการคือสามารถพูดได้ว่าวัวมีวิธีกินที่กินหญ้าเท่านั้นและไม่ใช่สิ่งอื่น
ที่จริงแล้วคุณไม่สามารถทำเช่นนั้นใน Java เพราะปรากฎว่าคุณสามารถสร้างสถานการณ์ที่ไม่มั่นคงเช่นปัญหาของการกำหนดผลไม้ให้กับตัวแปร Apple ที่ฉันพูดถึงก่อนหน้านี้
คำตอบก็คือคุณเพิ่มประเภทนามธรรมลงในระดับสัตว์
คุณพูดว่าคลาสสัตว์ใหม่ของฉันมีประเภทSuitableFood
ที่ฉันไม่รู้
ดังนั้นมันจึงเป็นแบบนามธรรม คุณไม่ให้ประเภทของการใช้งาน แล้วคุณมีวิธีการที่กินเท่านั้นeat
จากนั้นในชั้นเรียนผมจะบอกว่าตกลงฉันมีวัวซึ่งขยายชั้นเรียนและ
ดังนั้นประเภทนามธรรมให้ความคิดนี้ของชนิดใน superclass ที่ผมไม่ทราบซึ่งผมแล้วกรอกข้อมูลในภายหลังใน subclasses กับสิ่งที่ฉันจะรู้SuitableFood
Cow
Animal
Cow type SuitableFood equals Grass
แน่นอนคุณสามารถ คุณสามารถกำหนดสัตว์ชั้นเรียนด้วยอาหารประเภทที่มันกินได้
แต่ในทางปฏิบัติเมื่อคุณทำอย่างนั้นกับสิ่งที่แตกต่างกันจำนวนมากก็จะนำไปสู่การระเบิดของพารามิเตอร์และมักจะมีอะไรมากขึ้นในขอบเขตของพารามิเตอร์
ที่ ECOOP ปี 1998 Kim Bruce, Phil Wadler และฉันมีกระดาษที่เราแสดงให้เห็นว่าเมื่อคุณเพิ่มจำนวนของสิ่งที่คุณไม่ทราบโปรแกรมทั่วไปจะเพิ่มขึ้นเป็นสองเท่า
ดังนั้นมีเหตุผลที่ดีมากที่จะไม่ทำพารามิเตอร์ แต่มีสมาชิกที่เป็นนามธรรมเหล่านี้เพราะพวกเขาไม่ได้ให้กำลังสองนี้แก่คุณ
thatismattถามในความคิดเห็น:
คุณคิดว่าต่อไปนี้เป็นบทสรุปที่ยุติธรรม:
- ประเภทนามธรรมถูกนำมาใช้ในความสัมพันธ์ 'has-a' หรือ 'uses-a' (เช่น a
Cow eats Grass
)- โดยทั่วไปแล้วความสัมพันธ์แบบ 'ทั่วไป' (เช่น
List of Ints
)
ฉันไม่แน่ใจว่าความสัมพันธ์นั้นแตกต่างกันระหว่างการใช้ประเภทนามธรรมหรือชื่อสามัญ สิ่งที่แตกต่างคือ:
เพื่อให้เข้าใจถึงสิ่งที่มาร์ตินพูดเกี่ยวกับเมื่อมันมาถึง "การระเบิดของพารามิเตอร์และมักจะมีอะไรมากขึ้นในขอบเขตของพารามิเตอร์ " และการเติบโต quadratically เมื่อประเภทนามธรรมเป็นรูปแบบการใช้ยาชื่อสามัญคุณสามารถพิจารณากระดาษ " Scalable ตัวแทนที่เป็นนามธรรม "เขียนโดย ... Martin Odersky และ Matthias Zenger สำหรับ OOPSLA 2005 อ้างอิงในสิ่งพิมพ์ของโครงการ Palcom (เสร็จสิ้นในปี 2550)
สารสกัดที่เกี่ยวข้อง
สมาชิกประเภทที่เป็นนามธรรมให้วิธีที่ยืดหยุ่นในการทำให้เป็นนามธรรมเหนือส่วนประกอบประเภทที่เป็นรูปธรรม
ประเภทนามธรรมสามารถซ่อนข้อมูลเกี่ยวกับภายในของส่วนประกอบซึ่งคล้ายกับการใช้งานในลายเซ็นSML ในเฟรมเวิร์กเชิงวัตถุที่คลาสสามารถขยายได้โดยการสืบทอดพวกมันอาจถูกใช้เป็นวิธีการกำหนดพารามิเตอร์ที่ยืดหยุ่น (มักเรียกว่า polymorphism ตระกูลดูรายการบันทึกทางเว็บนี้เป็นต้นและบทความที่เขียนโดยEric Ernst )
(หมายเหตุ: Family polymorphism ถูกเสนอสำหรับภาษาเชิงวัตถุเป็นวิธีแก้ปัญหาเพื่อสนับสนุนคลาส recursive ที่ใช้ร่วมกันได้ แต่ปลอดภัยชนิดที่ใช้ร่วมกัน
ความคิดหลักของ polymorphism ในครอบครัวคือแนวคิดของครอบครัวซึ่งใช้เพื่อจัดกลุ่มคลาสเรียกซ้ำกัน)
abstract class MaxCell extends AbsCell {
type T <: Ordered { type O = T }
def setMax(x: T) = if (get < x) set(x)
}
ที่นี่ประกาศประเภทของ T จะ จำกัด โดยประเภทบนผูกพัน
{ type O = T }
ซึ่งประกอบด้วยชื่อชั้นสั่งซื้อและการปรับแต่ง
ถูกผูกไว้บนข้อกำหนดด้านความเชี่ยวชาญของ T ใน subclasses เพื่อเชื้อของผู้สั่งซื้อที่ประเภทสมาชิกของO
เนื่องจากข้อ จำกัด นี้เมธอดของคลาส Ordered จึงรับประกันว่าจะสามารถใช้ได้กับผู้รับและอาร์กิวเมนต์ประเภท T ตัวอย่างแสดงให้เห็นว่าสมาชิกประเภทที่ถูกผูกมัดอาจปรากฏเป็นส่วนหนึ่งของขอบเขต (เช่น Scala รองรับความหลากหลายรูปแบบ F-bounded )equals T
<
(หมายเหตุจาก Peter Canning, William Cook, Walter Hill, Walter Olthoff paper:
ปริมาณที่ จำกัด ได้รับการแนะนำโดย Cardelli และ Wegner เป็นวิธีการพิมพ์ฟังก์ชั่นที่ทำงานเหมือนกันทุกประเภทย่อยที่กำหนด
พวกเขากำหนดรูปแบบ "วัตถุ" ที่เรียบง่าย และใช้ขอบเขตปริมาณเพื่อตรวจสอบประเภทของฟังก์ชั่นที่เหมาะสมกับวัตถุทั้งหมดที่มีชุดของ "คุณสมบัติ" ที่ระบุ
การนำเสนอที่เป็นจริงมากขึ้นของภาษาเชิงวัตถุจะช่วยให้วัตถุที่เป็นองค์ประกอบของประเภทกำหนดซ้ำ
ในบริบทนี้ การหาปริมาณไม่ตรงตามวัตถุประสงค์อีกต่อไปมันเป็นเรื่องง่ายที่จะหาฟังก์ชั่นที่เหมาะสมกับวัตถุทั้งหมดที่มีชุดวิธีการที่ระบุ แต่ไม่สามารถพิมพ์ในระบบ Cardelli-Wegner
เพื่อเป็นพื้นฐานสำหรับฟังก์ชั่น polymorphic ที่พิมพ์ในภาษาเชิงวัตถุเราแนะนำปริมาณ F-bounded)
มีสองรูปแบบหลักของนามธรรมในภาษาโปรแกรม:
รูปแบบแรกเป็นเรื่องปกติสำหรับภาษาที่ใช้งานได้ในขณะที่รูปแบบที่สองมักใช้ในภาษาเชิงวัตถุ
ตามเนื้อผ้าจาวาสนับสนุนการกำหนดพารามิเตอร์สำหรับค่าและสมาชิก abstraction สำหรับการดำเนินการ Java 5.0 ล่าสุดที่มี generics รองรับการกำหนดพารามิเตอร์ด้วยเช่นกัน
อาร์กิวเมนต์สำหรับการรวม generics ใน Scala นั้นมีสองแบบ:
อย่างแรกการเข้ารหัสในรูปแบบนามธรรมนั้นไม่ใช่เรื่องง่ายที่จะต้องทำด้วยมือ นอกจากการสูญเสียความกระชับแล้วยังมีปัญหาความขัดแย้งของชื่ออุบัติเหตุระหว่างชื่อประเภทนามธรรมที่เลียนแบบพารามิเตอร์ประเภท
ประการที่สองประเภททั่วไปและนามธรรมมักจะให้บริการบทบาทที่แตกต่างในโปรแกรมสกาล่า
ในระบบที่มีความแตกต่างขอบเขตการเขียนประเภทนามธรรมเข้า generics อาจนำมาซึ่งการขยายตัวของกำลังสองประเภทขอบเขต
บทคัดย่อประเภทสมาชิกเมื่อเทียบกับพารามิเตอร์ประเภททั่วไปใน Scala (Bill Venners)
(เน้นที่เหมือง)
การสังเกตของฉันเกี่ยวกับสมาชิกประเภทนามธรรมคือว่าพวกเขาเป็นตัวเลือกที่ดีกว่าพารามิเตอร์ประเภททั่วไปเมื่อ:
- คุณต้องการที่จะให้คนผสมในคำจำกัดความของประเภทที่ผ่านลักษณะ
- คุณคิดว่าชัดเจนถึงชื่อประเภทสมาชิกเมื่อมันถูกกำหนดไว้จะช่วยให้การอ่านรหัส
ตัวอย่าง:
หากคุณต้องการผ่านการติดตั้งวัตถุที่แตกต่างกันสามแบบเข้าสู่การทดสอบคุณจะสามารถทำได้ แต่คุณจะต้องระบุสามประเภทหนึ่งประเภทสำหรับแต่ละพารามิเตอร์ ดังนั้นฉันจึงใช้วิธีการพิมพ์พารามิเตอร์คลาสชุดของคุณอาจมีลักษณะเช่นนี้:
// Type parameter version
class MySuite extends FixtureSuite3[StringBuilder, ListBuffer, Stack] with MyHandyFixture {
// ...
}
ในขณะที่วิธีการแบบสมาชิกจะมีลักษณะเช่นนี้:
// Type member version
class MySuite extends FixtureSuite3 with MyHandyFixture {
// ...
}
ความแตกต่างเล็กน้อยอีกอย่างหนึ่งระหว่างสมาชิกประเภทนามธรรมและพารามิเตอร์ประเภททั่วไปคือเมื่อระบุพารามิเตอร์ประเภททั่วไปผู้อ่านรหัสจะไม่เห็นชื่อของพารามิเตอร์ประเภท ดังนั้นมีคนเห็นรหัสบรรทัดนี้:
// Type parameter version
class MySuite extends FixtureSuite[StringBuilder] with StringBuilderFixture {
// ...
}
พวกเขาไม่รู้ว่าชื่อของพารามิเตอร์ประเภทที่ระบุเป็น StringBuilder คืออะไรโดยไม่ต้องค้นหา ในขณะที่ชื่อของพารามิเตอร์ type อยู่ที่นั่นในรหัสในแนวทางสมาชิกแบบนามธรรม:
// Type member version
class MySuite extends FixtureSuite with StringBuilderFixture {
type FixtureParam = StringBuilder
// ...
}
ในกรณีหลังผู้อ่านรหัสจะเห็นว่า
StringBuilder
เป็นชนิด "พารามิเตอร์การติดตั้ง"
พวกเขายังคงต้องคิดออกว่า "พารามิเตอร์การติดตั้ง" หมายถึง แต่อย่างน้อยพวกเขาสามารถได้รับชื่อของประเภทโดยไม่ต้องดูในเอกสารประกอบ
ฉันมีคำถามเดียวกันเมื่อฉันอ่านเกี่ยวกับสกาล่า
ข้อได้เปรียบของการใช้ยาชื่อสามัญคือคุณกำลังสร้างตระกูลประเภท ไม่มีใครจะต้องซับคลาสBuffer
-they ก็สามารถใช้Buffer[Any]
, Buffer[String]
ฯลฯ
หากคุณใช้ประเภทนามธรรมผู้คนจะถูกบังคับให้สร้างคลาสย่อย คนจะต้องเรียนเหมือนAnyBuffer
, StringBuffer
ฯลฯ
คุณต้องตัดสินใจว่าอะไรดีกว่าสำหรับความต้องการเฉพาะของคุณ
Buffer { type T <: String }
หรือBuffer { type T = String }
ขึ้นอยู่กับความต้องการของคุณ
คุณสามารถใช้ประเภทนามธรรมร่วมกับพารามิเตอร์ประเภทเพื่อสร้างแม่แบบกำหนดเอง
สมมติว่าคุณจำเป็นต้องสร้างรูปแบบที่มีลักษณะการเชื่อมต่อที่สาม:
trait AA[B,C]
trait BB[C,A]
trait CC[A,B]
ในทางที่ข้อโต้แย้งที่กล่าวถึงในพารามิเตอร์ประเภทคือ AA, BB, CC เองด้วยความเคารพ
คุณอาจมีรหัสบางประเภท:
trait AA[B<:BB[C,AA[B,C]],C<:CC[AA[B,C],B]]
trait BB[C<:CC[A,BB[C,A]],A<:AA[BB[C,A],C]]
trait CC[A<:AA[B,CC[A,B]],B<:BB[CC[A,B],A]]
ซึ่งจะไม่ทำงานในวิธีที่ง่ายนี้เพราะพันธบัตรพารามิเตอร์ประเภท คุณต้องทำให้มันแปรปรวนร่วมเพื่อสืบทอดอย่างถูกต้อง
trait AA[+B<:BB[C,AA[B,C]],+C<:CC[AA[B,C],B]]
trait BB[+C<:CC[A,BB[C,A]],+A<:AA[BB[C,A],C]]
trait CC[+A<:AA[B,CC[A,B]],+B<:BB[CC[A,B],A]]
ตัวอย่างนี้จะรวบรวม แต่มีข้อกำหนดที่เข้มงวดเกี่ยวกับกฎความแปรปรวนและไม่สามารถใช้ได้ในบางโอกาส
trait AA[+B<:BB[C,AA[B,C]],+C<:CC[AA[B,C],B]] {
def forth(x:B):C
def back(x:C):B
}
trait BB[+C<:CC[A,BB[C,A]],+A<:AA[BB[C,A],C]] {
def forth(x:C):A
def back(x:A):C
}
trait CC[+A<:AA[B,CC[A,B]],+B<:BB[CC[A,B],A]] {
def forth(x:A):B
def back(x:B):A
}
คอมไพเลอร์จะคัดค้านข้อผิดพลาดการตรวจสอบความแปรปรวนจำนวนมาก
ในกรณีนี้คุณอาจรวบรวมข้อกำหนดประเภททั้งหมดในลักษณะเพิ่มเติมและกำหนดลักษณะอื่น ๆ
//one trait to rule them all
trait OO[O <: OO[O]] { this : O =>
type A <: AA[O]
type B <: BB[O]
type C <: CC[O]
}
trait AA[O <: OO[O]] { this : O#A =>
type A = O#A
type B = O#B
type C = O#C
def left(l:B):C
def right(r:C):B = r.left(this)
def join(l:B, r:C):A
def double(l:B, r:C):A = this.join( l.join(r,this), r.join(this,l) )
}
trait BB[O <: OO[O]] { this : O#B =>
type A = O#A
type B = O#B
type C = O#C
def left(l:C):A
def right(r:A):C = r.left(this)
def join(l:C, r:A):B
def double(l:C, r:A):B = this.join( l.join(r,this), r.join(this,l) )
}
trait CC[O <: OO[O]] { this : O#C =>
type A = O#A
type B = O#B
type C = O#C
def left(l:A):B
def right(r:B):A = r.left(this)
def join(l:A, r:B):C
def double(l:A, r:B):C = this.join( l.join(r,this), r.join(this,l) )
}
ตอนนี้เราสามารถเขียนรูปแบบที่เป็นรูปธรรมสำหรับรูปแบบที่อธิบายกำหนดซ้ายและเข้าร่วมวิธีการในชั้นเรียนทั้งหมดและได้รับสิทธิและสองครั้งฟรี
class ReprO extends OO[ReprO] {
override type A = ReprA
override type B = ReprB
override type C = ReprC
}
case class ReprA(data : Int) extends AA[ReprO] {
override def left(l:B):C = ReprC(data - l.data)
override def join(l:B, r:C) = ReprA(l.data + r.data)
}
case class ReprB(data : Int) extends BB[ReprO] {
override def left(l:C):A = ReprA(data - l.data)
override def join(l:C, r:A):B = ReprB(l.data + r.data)
}
case class ReprC(data : Int) extends CC[ReprO] {
override def left(l:A):B = ReprB(data - l.data)
override def join(l:A, r:B):C = ReprC(l.data + r.data)
}
ดังนั้นทั้ง abstract type และพารามิเตอร์ type ใช้สำหรับการสร้าง abstractions พวกเขาทั้งคู่มีจุดอ่อนและจุดแข็ง ประเภทนามธรรมมีความเฉพาะเจาะจงมากขึ้นและสามารถอธิบายโครงสร้างประเภทใดก็ได้ แต่มีความละเอียดและจำเป็นต้องระบุอย่างชัดเจน พารามิเตอร์ประเภทสามารถสร้างกลุ่มประเภทได้ทันที แต่ให้ความกังวลเพิ่มเติมเกี่ยวกับการสืบทอดและขอบเขตชนิด
พวกเขาให้ความร่วมมือซึ่งกันและกันและสามารถใช้ร่วมกันในการสร้าง abstractions ที่ซับซ้อนซึ่งไม่สามารถแสดงออกได้ด้วยสิ่งใดสิ่งหนึ่งเท่านั้น
ฉันคิดว่ามันไม่แตกต่างกันมากที่นี่ สมาชิกประเภทนามธรรมสามารถมองเห็นเป็นประเภทที่มีอยู่เพียงซึ่งคล้ายกับประเภทบันทึกในภาษาการทำงานอื่น ๆ
ตัวอย่างเช่นเรามี:
class ListT {
type T
...
}
และ
class List[T] {...}
จากนั้นก็เป็นเพียงเช่นเดียวกับListT
List[_]
ความเชื่อมั่นของสมาชิกประเภทคือเราสามารถใช้คลาสที่ไม่มีประเภทชัดเจนและหลีกเลี่ยงพารามิเตอร์ประเภทมากเกินไป