ทรัพยากรการเขียนโปรแกรมประเภท Scala


102

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

นี่คือแหล่งข้อมูลที่ฉันพบจนถึงตอนนี้:

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

มีแหล่งข้อมูลเบื้องต้นที่ดีหรือไม่?


โดยส่วนตัวแล้วฉันพบข้อสันนิษฐานว่าผู้ที่ต้องการเขียนโปรแกรมระดับประเภทใน Scala รู้วิธีการเขียนโปรแกรมใน Scala ค่อนข้างสมเหตุสมผลแล้ว แม้ว่าจะหมายความว่าฉันไม่เข้าใจคำของบทความที่คุณเชื่อมโยงถึง :-)
Jörg W Mittag

คำตอบ:


140

ภาพรวม

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

กระบวนทัศน์

กระบวนทัศน์หลักในการเขียนโปรแกรมระดับชนิดมีสองกระบวนทัศน์: "เชิงวัตถุ" และ "ฟังก์ชัน" ตัวอย่างส่วนใหญ่ที่เชื่อมโยงจากที่นี่เป็นไปตามกระบวนทัศน์เชิงวัตถุ

ตัวอย่างที่ดีและค่อนข้างง่ายของการเขียนโปรแกรมระดับประเภทในกระบวนทัศน์เชิงวัตถุสามารถพบได้ในการใช้แคลคูลัสแลมบ์ดาของ 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ที่คุณต้องการตรวจสอบว่าเท่ากัน จากนั้นตรวจสอบว่าคอมไพล์ต่อไปนี้:

หรือคุณสามารถแปลงประเภทเป็นค่า (ดังที่แสดงด้านบน) และทำการตรวจสอบรันไทม์ของค่า เช่นassert(toInt(a) == toInt(b))ที่aเป็นประเภทAและเป็นประเภทbB

แหล่งข้อมูลเพิ่มเติม

ชุดที่สมบูรณ์ของโครงสร้างที่มีอยู่สามารถพบได้ในส่วนประเภทคู่มืออ้างอิงสกาล่าเอกสาร (pdf)

Adriaan Moorsมีเอกสารทางวิชาการหลายเรื่องเกี่ยวกับตัวสร้างประเภทและหัวข้อที่เกี่ยวข้องพร้อมตัวอย่างจากสกาลา:

Apocalispเป็นบล็อกที่มีตัวอย่างมากมายของการเขียนโปรแกรมระดับประเภทในสกาล่า

  • Type-Level Programming ใน Scalaเป็นทัวร์แนะนำที่ยอดเยี่ยมของการเขียนโปรแกรมระดับบางประเภทซึ่งรวมถึงบูลีนตัวเลขธรรมชาติ (ตามด้านบน) เลขฐานสองรายการที่ต่างกันและอื่น ๆ
  • Scala Typehackery เพิ่มเติมคือการนำแคลคูลัสแลมบ์ดาไปใช้ข้างต้น

ScalaZเป็นโครงการที่มีการใช้งานมากซึ่งให้ฟังก์ชันการทำงานที่ขยาย Scala API โดยใช้คุณสมบัติการเขียนโปรแกรมระดับประเภทต่างๆ เป็นโครงการที่น่าสนใจมากที่มีผลต่อไปนี้

MetaScalaเป็นห้องสมุดประเภทระดับ Scala รวมถึงประเภทเมตาสำหรับตัวเลขธรรมชาติ booleans หน่วย HList ฯลฯ มันเป็นโครงการโดยJesper Nordenberg (บล็อกของเขา)

Michid (บล็อก)มีตัวอย่างน่ากลัวบางอย่างของการเขียนโปรแกรมประเภทในระดับสกาล่า (จากคำตอบอื่น ๆ ):

Debasish Ghosh (บล็อก)มีโพสต์ที่เกี่ยวข้องเช่นกัน:

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


12

นอกจากลิงค์อื่น ๆ ที่นี่แล้วยังมีบล็อกโพสต์ของฉันเกี่ยวกับการเขียนโปรแกรมเมตาระดับประเภทใน Scala:


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



โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.