ภาพรวม
การเขียนโปรแกรมระดับประเภทมีความคล้ายคลึงกันมากกับการเขียนโปรแกรมระดับค่าดั้งเดิม อย่างไรก็ตามไม่เหมือนกับการเขียนโปรแกรมระดับค่าที่การคำนวณเกิดขึ้นที่รันไทม์ในการเขียนโปรแกรมระดับชนิดการคำนวณจะเกิดขึ้นในเวลาคอมไพล์ ฉันจะพยายามวาดแนวขนานระหว่างการเขียนโปรแกรมที่ระดับค่าและการเขียนโปรแกรมในระดับประเภท
กระบวนทัศน์
กระบวนทัศน์หลักในการเขียนโปรแกรมระดับชนิดมีสองกระบวนทัศน์: "เชิงวัตถุ" และ "ฟังก์ชัน" ตัวอย่างส่วนใหญ่ที่เชื่อมโยงจากที่นี่เป็นไปตามกระบวนทัศน์เชิงวัตถุ
ตัวอย่างที่ดีและค่อนข้างง่ายของการเขียนโปรแกรมระดับประเภทในกระบวนทัศน์เชิงวัตถุสามารถพบได้ในการใช้แคลคูลัสแลมบ์ดาของ apocalisp ซึ่งจำลองแบบที่นี่:
// Abstract trait
trait Lambda {
type subst[U <: Lambda] <: Lambda
type apply[U <: Lambda] <: Lambda
type eval <: Lambda
}
// Implementations
trait App[S <: Lambda, T <: Lambda] extends Lambda {
type subst[U <: Lambda] = App[S#subst[U], T#subst[U]]
type apply[U] = Nothing
type eval = S#eval#apply[T]
}
trait Lam[T <: Lambda] extends Lambda {
type subst[U <: Lambda] = Lam[T]
type apply[U <: Lambda] = T#subst[U]#eval
type eval = Lam[T]
}
trait X extends Lambda {
type subst[U <: Lambda] = U
type apply[U] = Lambda
type eval = X
}
ดังที่เห็นได้จากตัวอย่างกระบวนทัศน์เชิงวัตถุสำหรับการเขียนโปรแกรมระดับชนิดดำเนินการดังนี้:
- ขั้นแรก: กำหนดลักษณะนามธรรมด้วยเขตข้อมูลประเภทนามธรรมต่างๆ (ดูด้านล่างว่าเขตข้อมูลนามธรรมคืออะไร) นี่คือเทมเพลตสำหรับการรับประกันว่าฟิลด์บางประเภทมีอยู่ในการใช้งานทั้งหมดโดยไม่ต้องบังคับให้นำไปใช้งาน ในตัวอย่างแลมบ์ดาแคลคูลัสตรงนี้เพื่อ
trait Lambdaรับประกันว่าที่ประเภทดังต่อไปนี้: subst,applyevalและ
- ถัดไป: กำหนดการลบที่ขยายลักษณะนามธรรมและใช้ฟิลด์ประเภทนามธรรมต่างๆ
- บ่อยครั้งการลบเหล่านี้จะถูกกำหนดพารามิเตอร์ด้วยอาร์กิวเมนต์ ในตัวอย่างแคลคูลัสแลมบ์ดาชนิดย่อยจะ
trait App extends Lambdaถูกกำหนดพารามิเตอร์ด้วยสองประเภท ( SและTทั้งสองต้องเป็นชนิดย่อยของLambda) ซึ่งtrait Lam extends Lambdaกำหนดพารามิเตอร์ด้วยประเภทเดียว (T ) และtrait X extends Lambda(ซึ่งไม่กำหนดพารามิเตอร์)
- ฟิลด์ประเภทมักจะถูกนำมาใช้โดยอ้างถึงพารามิเตอร์ประเภทของการลบและบางครั้งการอ้างอิงฟิลด์ประเภทของพวกเขาผ่านตัวดำเนินการแฮช:
#(ซึ่งคล้ายกับตัวดำเนินการจุด: .สำหรับค่า) ในลักษณะAppของตัวอย่างเช่นแลมบ์ดาแคลคูลัสประเภทจะดำเนินการดังต่อไปนี้:eval type eval = S#eval#apply[T]โดยพื้นฐานแล้วจะเรียกevalประเภทของพารามิเตอร์ของลักษณะSและการเรียกapplyด้วยพารามิเตอร์Tบนผลลัพธ์ หมายเหตุSรับประกันได้ว่าจะมีประเภทเพราะพารามิเตอร์ระบุว่ามันจะเป็นชนิดย่อยของeval Lambdaในทำนองเดียวกันผลมาจากการevalต้องมีapplyประเภทเพราะมันมีการระบุว่าจะเป็นชนิดย่อยของตามที่ระบุไว้ในลักษณะที่เป็นนามธรรมLambdaLambda
กระบวนทัศน์เชิงฟังก์ชันประกอบด้วยการกำหนดคอนสตรัคเตอร์ประเภทพารามิเตอร์จำนวนมากที่ไม่ได้จัดกลุ่มเข้าด้วยกันในลักษณะ
การเปรียบเทียบระหว่างการเขียนโปรแกรมระดับค่าและการเขียนโปรแกรมระดับชนิด
- ชั้นนามธรรม
- ระดับมูลค่า:
abstract class C { val x }
- ประเภทระดับ:
trait C { type X }
- ประเภทที่ขึ้นกับเส้นทาง
C.x (การอ้างอิงค่าฟิลด์ / ฟังก์ชัน x ในออบเจ็กต์ C)
C#x (การอ้างอิงฟิลด์ชนิด x ในลักษณะ C)
- ลายเซ็นฟังก์ชัน (ไม่มีการใช้งาน)
- ระดับมูลค่า:
def f(x:X) : Y
- type-level:
type f[x <: X] <: Y(สิ่งนี้เรียกว่า "type constructor" และมักเกิดขึ้นในลักษณะนามธรรม)
- การใช้งานฟังก์ชัน
- ระดับมูลค่า:
def f(x:X) : Y = x
- ประเภทระดับ:
type f[x <: X] = x
- เงื่อนไข
- ตรวจสอบความเท่าเทียมกัน
- ระดับมูลค่า:
a:A == b:B
- ประเภทระดับ:
implicitly[A =:= B]
- ระดับค่า: เกิดขึ้นใน JVM ผ่านการทดสอบหน่วยที่รันไทม์ (เช่นไม่มีข้อผิดพลาดรันไทม์):
- ในสาระสำคัญคือการยืนยัน:
assert(a == b)
- ประเภทระดับ: เกิดขึ้นในคอมไพเลอร์ผ่านการตรวจสอบการพิมพ์ (กล่าวคือไม่มีข้อผิดพลาดของคอมไพเลอร์):
- ในสาระสำคัญคือการเปรียบเทียบประเภท: เช่น
implicitly[A =:= B]
A <:< Bคอมไพล์ก็ต่อเมื่อAเป็นประเภทย่อยของB
A =:= Bคอมไพล์ก็ต่อเมื่อAเป็นประเภทย่อยBและBเป็นประเภทย่อยของA
A <%< B, ("ดูได้ในฐานะ") คอมไพล์ก็ต่อเมื่อAสามารถดูได้เป็นB(กล่าวคือมีการแปลงโดยปริยายจากAเป็นประเภทย่อยของB )
- ตัวอย่าง
- ตัวดำเนินการเปรียบเทียบเพิ่มเติม
การแปลงระหว่างประเภทและค่า
ในหลาย ๆ ตัวอย่างประเภทที่กำหนดผ่านลักษณะมักเป็นทั้งนามธรรมและแบบปิดผนึกดังนั้นจึงไม่สามารถสร้างอินสแตนซ์โดยตรงหรือผ่านคลาสย่อยที่ไม่ระบุตัวตนได้ ดังนั้นจึงเป็นเรื่องปกติที่จะใช้nullเป็นค่าตัวยึดเมื่อทำการคำนวณระดับมูลค่าโดยใช้ความสนใจบางประเภท:
- เช่นประเภทที่คุณสนใจอยู่
val x:A = nullที่ไหนA
เนื่องจาก type-erasure ประเภทที่กำหนดพารามิเตอร์จึงมีลักษณะเหมือนกัน นอกจากนี้ (ตามที่กล่าวไว้ข้างต้น) ค่าที่คุณกำลังทำงานด้วยมักจะเป็นnullดังนั้นการปรับสภาพตามประเภทวัตถุ (เช่นผ่านคำสั่งจับคู่) จึงไม่ได้ผล
เคล็ดลับคือการใช้ฟังก์ชันและค่าโดยนัย กรณีฐานมักเป็นค่าโดยนัยและกรณีที่เกิดซ้ำมักเป็นฟังก์ชันโดยปริยาย อันที่จริงการเขียนโปรแกรมระดับประเภทใช้ประโยชน์อย่างมากโดยนัย
ลองพิจารณาตัวอย่างนี้ ( นำมาจาก metascalaและapocalisp ):
sealed trait Nat
sealed trait _0 extends Nat
sealed trait Succ[N <: Nat] extends Nat
ที่นี่คุณมีการเข้ารหัส Peano ของตัวเลขธรรมชาติ นั่นคือคุณมีประเภทสำหรับจำนวนเต็มที่ไม่เป็นลบแต่ละประเภท: ชนิดพิเศษสำหรับ 0 คือ_0; และแต่ละจำนวนเต็มที่มากกว่าศูนย์จะมีประเภทของฟอร์มSucc[A]โดยที่Aชนิดนั้นแสดงจำนวนเต็มที่น้อยกว่า ตัวอย่างเช่นประเภทที่แสดงถึง 2 จะเป็น: Succ[Succ[_0]](ใช้ตัวตายตัวแทนสองครั้งกับประเภทที่แสดงถึงศูนย์)
เราสามารถใช้นามแฝงตัวเลขธรรมชาติต่างๆเพื่อการอ้างอิงที่สะดวกยิ่งขึ้น ตัวอย่าง:
type _3 = Succ[Succ[Succ[_0]]]
(ซึ่งเหมือนกับการกำหนด a valให้เป็นผลลัพธ์ของฟังก์ชัน)
ตอนนี้สมมติว่าเราต้องการกำหนดฟังก์ชันระดับค่าdef toInt[T <: Nat](v : T)ซึ่งรับค่าอาร์กิวเมนต์vซึ่งสอดคล้องNatและส่งคืนจำนวนเต็มแทนจำนวนธรรมชาติที่เข้ารหัสในvประเภทของ ตัวอย่างเช่นถ้าเรามีค่าval x:_3 = null( nullประเภทSucc[Succ[Succ[_0]]]) เราต้องการที่จะกลับมาtoInt(x)3
ในการนำไปใช้toIntเราจะใช้ประโยชน์จากคลาสต่อไปนี้:
class TypeToValue[T, VT](value : VT) { def getValue() = value }
ในฐานะที่เราจะดูที่ด้านล่างจะมีวัตถุที่สร้างขึ้นจากชั้นเรียนTypeToValueสำหรับแต่ละNatจาก_0ขึ้นไป (เช่น) _3และแต่ละคนจะเก็บแทนค่าของชนิดที่สอดคล้องกัน (เช่นTypeToValue[_0, Int]จะเก็บค่า0, TypeToValue[Succ[_0], Int]จะเก็บค่า1อื่น ๆ ) หมายเหตุTypeToValueถูกกำหนดพารามิเตอร์โดยสองประเภท: TและVT. Tสอดคล้องกับประเภทที่เราพยายามกำหนดค่าให้ (ในตัวอย่างของเราNat) และVTสอดคล้องกับประเภทของค่าที่เรากำหนดให้ (ในตัวอย่างของเราInt )
ตอนนี้เราสร้างคำจำกัดความโดยนัยสองประการต่อไปนี้:
implicit val _0ToInt = new TypeToValue[_0, Int](0)
implicit def succToInt[P <: Nat](implicit v : TypeToValue[P, Int]) =
new TypeToValue[Succ[P], Int](1 + v.getValue())
และเราดำเนินการtoIntดังนี้:
def toInt[T <: Nat](v : T)(implicit ttv : TypeToValue[T, Int]) : Int = ttv.getValue()
เพื่อให้เข้าใจถึงวิธีการtoIntทำงานลองพิจารณาว่ามันทำอะไรกับอินพุตสองตัว:
val z:_0 = null
val y:Succ[_0] = null
เมื่อเราเรียกtoInt(z)คอมไพลเลอร์จะค้นหาอาร์กิวเมนต์โดยนัยttvของประเภทTypeToValue[_0, Int](เนื่องจากzเป็นประเภท_0) พบวัตถุ_0ToIntมันเรียกใช้getValueเมธอดของวัตถุนี้และกลับมา0วิธีการของวัตถุนี้และได้รับกลับประเด็นสำคัญที่ควรทราบคือเราไม่ได้ระบุให้โปรแกรมใช้วัตถุใดคอมไพเลอร์พบว่าโดยปริยาย
toInt(y)ตอนนี้ขอพิจารณา คราวนี้คอมไพลเลอร์จะค้นหาอาร์กิวเมนต์โดยปริยายttvของประเภทTypeToValue[Succ[_0], Int](เนื่องจากyเป็นประเภทSucc[_0]) พบฟังก์ชันsuccToIntซึ่งสามารถส่งคืนอ็อบเจ็กต์ประเภทที่เหมาะสม ( TypeToValue[Succ[_0], Int]) และประเมินค่าได้ ฟังก์ชันนี้ใช้อาร์กิวเมนต์โดยนัย ( v) ของชนิดTypeToValue[_0, Int](นั่นคือโดยTypeToValueที่พารามิเตอร์ประเภทแรกมีน้อยกว่าหนึ่งตัวSucc[_]) วัสดุคอมไพเลอร์_0ToInt(ตามที่ได้ดำเนินการในการประเมินผลtoInt(z)ดังกล่าวข้างต้น) และsuccToIntสร้างใหม่วัตถุที่มีค่าTypeToValue 1โปรดทราบอีกครั้งว่าคอมไพเลอร์กำลังให้ค่าเหล่านี้ทั้งหมดโดยปริยายเนื่องจากเราไม่สามารถเข้าถึงได้อย่างชัดเจน
ตรวจสอบงานของคุณ
มีหลายวิธีในการตรวจสอบว่าการคำนวณระดับประเภทของคุณกำลังทำในสิ่งที่คุณคาดหวัง วิธีการบางประการมีดังนี้ ทำให้สองประเภทAและBที่คุณต้องการตรวจสอบว่าเท่ากัน จากนั้นตรวจสอบว่าคอมไพล์ต่อไปนี้:
Equal[A, B]
implicitly[A =:= B]
หรือคุณสามารถแปลงประเภทเป็นค่า (ดังที่แสดงด้านบน) และทำการตรวจสอบรันไทม์ของค่า เช่นassert(toInt(a) == toInt(b))ที่aเป็นประเภทAและเป็นประเภทbB
แหล่งข้อมูลเพิ่มเติม
ชุดที่สมบูรณ์ของโครงสร้างที่มีอยู่สามารถพบได้ในส่วนประเภทคู่มืออ้างอิงสกาล่าเอกสาร (pdf)
Adriaan Moorsมีเอกสารทางวิชาการหลายเรื่องเกี่ยวกับตัวสร้างประเภทและหัวข้อที่เกี่ยวข้องพร้อมตัวอย่างจากสกาลา:
Apocalispเป็นบล็อกที่มีตัวอย่างมากมายของการเขียนโปรแกรมระดับประเภทในสกาล่า
ScalaZเป็นโครงการที่มีการใช้งานมากซึ่งให้ฟังก์ชันการทำงานที่ขยาย Scala API โดยใช้คุณสมบัติการเขียนโปรแกรมระดับประเภทต่างๆ เป็นโครงการที่น่าสนใจมากที่มีผลต่อไปนี้
MetaScalaเป็นห้องสมุดประเภทระดับ Scala รวมถึงประเภทเมตาสำหรับตัวเลขธรรมชาติ booleans หน่วย HList ฯลฯ มันเป็นโครงการโดยJesper Nordenberg (บล็อกของเขา)
Michid (บล็อก)มีตัวอย่างน่ากลัวบางอย่างของการเขียนโปรแกรมประเภทในระดับสกาล่า (จากคำตอบอื่น ๆ ):
Debasish Ghosh (บล็อก)มีโพสต์ที่เกี่ยวข้องเช่นกัน:
(ฉันได้ทำการวิจัยเกี่ยวกับเรื่องนี้และนี่คือสิ่งที่ฉันได้เรียนรู้ฉันยังใหม่สำหรับเรื่องนี้ดังนั้นโปรดชี้ให้เห็นความไม่ถูกต้องในคำตอบนี้)