ภาพรวม
การเขียนโปรแกรมระดับประเภทมีความคล้ายคลึงกันมากกับการเขียนโปรแกรมระดับค่าดั้งเดิม อย่างไรก็ตามไม่เหมือนกับการเขียนโปรแกรมระดับค่าที่การคำนวณเกิดขึ้นที่รันไทม์ในการเขียนโปรแกรมระดับชนิดการคำนวณจะเกิดขึ้นในเวลาคอมไพล์ ฉันจะพยายามวาดแนวขนานระหว่างการเขียนโปรแกรมที่ระดับค่าและการเขียนโปรแกรมในระดับประเภท
กระบวนทัศน์
กระบวนทัศน์หลักในการเขียนโปรแกรมระดับชนิดมีสองกระบวนทัศน์: "เชิงวัตถุ" และ "ฟังก์ชัน" ตัวอย่างส่วนใหญ่ที่เชื่อมโยงจากที่นี่เป็นไปตามกระบวนทัศน์เชิงวัตถุ
ตัวอย่างที่ดีและค่อนข้างง่ายของการเขียนโปรแกรมระดับประเภทในกระบวนทัศน์เชิงวัตถุสามารถพบได้ในการใช้แคลคูลัสแลมบ์ดาของ 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
,apply
eval
และ
- ถัดไป: กำหนดการลบที่ขยายลักษณะนามธรรมและใช้ฟิลด์ประเภทนามธรรมต่างๆ
- บ่อยครั้งการลบเหล่านี้จะถูกกำหนดพารามิเตอร์ด้วยอาร์กิวเมนต์ ในตัวอย่างแคลคูลัสแลมบ์ดาชนิดย่อยจะ
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
ประเภทเพราะมันมีการระบุว่าจะเป็นชนิดย่อยของตามที่ระบุไว้ในลักษณะที่เป็นนามธรรมLambda
Lambda
กระบวนทัศน์เชิงฟังก์ชันประกอบด้วยการกำหนดคอนสตรัคเตอร์ประเภทพารามิเตอร์จำนวนมากที่ไม่ได้จัดกลุ่มเข้าด้วยกันในลักษณะ
การเปรียบเทียบระหว่างการเขียนโปรแกรมระดับค่าและการเขียนโปรแกรมระดับชนิด
- ชั้นนามธรรม
- ระดับมูลค่า:
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
และเป็นประเภทb
B
แหล่งข้อมูลเพิ่มเติม
ชุดที่สมบูรณ์ของโครงสร้างที่มีอยู่สามารถพบได้ในส่วนประเภทคู่มืออ้างอิงสกาล่าเอกสาร (pdf)
Adriaan Moorsมีเอกสารทางวิชาการหลายเรื่องเกี่ยวกับตัวสร้างประเภทและหัวข้อที่เกี่ยวข้องพร้อมตัวอย่างจากสกาลา:
Apocalispเป็นบล็อกที่มีตัวอย่างมากมายของการเขียนโปรแกรมระดับประเภทในสกาล่า
ScalaZเป็นโครงการที่มีการใช้งานมากซึ่งให้ฟังก์ชันการทำงานที่ขยาย Scala API โดยใช้คุณสมบัติการเขียนโปรแกรมระดับประเภทต่างๆ เป็นโครงการที่น่าสนใจมากที่มีผลต่อไปนี้
MetaScalaเป็นห้องสมุดประเภทระดับ Scala รวมถึงประเภทเมตาสำหรับตัวเลขธรรมชาติ booleans หน่วย HList ฯลฯ มันเป็นโครงการโดยJesper Nordenberg (บล็อกของเขา)
Michid (บล็อก)มีตัวอย่างน่ากลัวบางอย่างของการเขียนโปรแกรมประเภทในระดับสกาล่า (จากคำตอบอื่น ๆ ):
Debasish Ghosh (บล็อก)มีโพสต์ที่เกี่ยวข้องเช่นกัน:
(ฉันได้ทำการวิจัยเกี่ยวกับเรื่องนี้และนี่คือสิ่งที่ฉันได้เรียนรู้ฉันยังใหม่สำหรับเรื่องนี้ดังนั้นโปรดชี้ให้เห็นความไม่ถูกต้องในคำตอบนี้)