พิมพ์คลาสใน Scala3: คู่มือสำหรับผู้เริ่มต้น | บัญชีแยกประเภท

พิมพ์คลาสใน Scala3: คู่มือสำหรับผู้เริ่มต้น | บัญชีแยกประเภท

โหนดต้นทาง: 3028113

เอกสารนี้มีไว้สำหรับนักพัฒนา Scala3 มือใหม่ที่เชี่ยวชาญเรื่องร้อยแก้ว Scala อยู่แล้ว แต่ยังรู้สึกงุนงงกับ `implicits` และลักษณะที่เป็นพารามิเตอร์ในโค้ด

เอกสารนี้จะอธิบายว่าทำไม อย่างไร ที่ไหน และเมื่อใด ประเภทคลาส (TC).

หลังจากอ่านเอกสารนี้แล้ว นักพัฒนา Scala3 ระดับเริ่มต้นจะได้รับความรู้ที่ชัดเจนในการใช้งานและเจาะลึกซอร์สโค้ดของ มาก ของไลบรารี Scala และเริ่มเขียนโค้ด Scala สำนวน

มาเริ่มกันว่าทำไม…

ปัญหาการแสดงออก

ใน 1998, ฟิลิป วัดเลอร์ กล่าว ว่า “ปัญหาการแสดงออกเป็นชื่อใหม่ของปัญหาเก่า” มันเป็นปัญหาของการขยายซอฟต์แวร์ ตามการเขียนของมิสเตอร์ Wadler การแก้ปัญหานิพจน์ต้องเป็นไปตามกฎต่อไปนี้:

  • กฎข้อที่ 1: อนุญาตให้มีการดำเนินการของ พฤติกรรมที่มีอยู่ (คิดถึงลักษณะสกาล่า) มาประยุกต์ใช้ การนำเสนอใหม่ (คิดถึงคลาสเคส)
  • กฎข้อที่ 2:  อนุญาตให้มีการดำเนินการของ พฤติกรรมใหม่ ที่จะนำไปใช้กับ การเป็นตัวแทนที่มีอยู่
  • กฎข้อที่ 3: มันจะต้องไม่เป็นอันตรายต่อ ประเภทความปลอดภัย
  • กฎข้อที่ 4: จะต้องไม่จำเป็นต้องคอมไพล์ใหม่ รหัสที่มีอยู่

การแก้ปัญหานี้จะเป็นหัวข้อสีเงินของบทความนี้

กฎข้อที่ 1: การใช้พฤติกรรมที่มีอยู่กับการนำเสนอใหม่

ภาษาเชิงวัตถุใด ๆ มีวิธีแก้ปัญหาแบบรวมสำหรับกฎ 1 ด้วย ความหลากหลายชนิดย่อย. คุณสามารถใช้ `trait` กำหนดโดยขึ้นอยู่กับ `class` ในโค้ดของคุณเอง โดยไม่ต้องคอมไพล์การขึ้นต่อกันใหม่ มาดูกันว่าในการดำเนินการ:

สกาล่า

def todo = 42
type Height = Int
type Block = Int

object Lib1:
  trait Blockchain:
    def getBlock(height: Height): Block

  case class Ethereum() extends Blockchain:
    override def getBlock(height: Height) = todo

  case class Bitcoin() extends Blockchain:
    override def getBlock(height: Height) = todo


object Lib2:
  import Lib1.*

  case class Polkadot() extends Blockchain:
    override def getBlock(height: Height): Block = todo

val eth = Lib1.Ethereum()
val btc = Lib1.Bitcoin()
val dot = Lib2.Polkadot()

ในตัวอย่างสมมตินี้ ห้องสมุด `Lib1` (บรรทัดที่ 5) กำหนดลักษณะ `Blockchain` (บรรทัดที่ 6) พร้อมการใช้งาน 2 รายการ (บรรทัดที่ 9 และ 12) `Lib1` จะยังคงเหมือนเดิมในเอกสารนี้ทั้งหมด (การบังคับใช้กฎข้อ 4)

`Lib2` (บรรทัดที่ 15) ใช้พฤติกรรมที่มีอยู่ `Blockchain` ในชั้นเรียนใหม่ `Polkadot` (กฎ 1) ในลักษณะที่ปลอดภัย (กฎ 3) โดยไม่ต้องคอมไพล์ใหม่ `Lib1` (กฎข้อ 4) 

กฎข้อที่ 2: การนำพฤติกรรมใหม่ไปใช้กับการเป็นตัวแทนที่มีอยู่

ลองจินตนาการใน `Lib2` เราต้องการพฤติกรรมใหม่ `lastBlock` ที่จะนำไปใช้โดยเฉพาะสำหรับแต่ละ `Blockchain`.

สิ่งแรกที่ต้องคำนึงถึงคือการสร้างสวิตช์ขนาดใหญ่ตามประเภทของพารามิเตอร์

สกาล่า

def todo = 42
type Height = Int
type Block = Int

object Lib1:
  trait Blockchain:
    def getBlock(height: Height): Block

  case class Ethereum() extends Blockchain:
    override def getBlock(height: Height) = todo

  case class Bitcoin() extends Blockchain:
    override def getBlock(height: Height) = todo

object Lib2:
  import Lib1.*

  case class Polkadot() extends Blockchain:
    override def getBlock(height: Height): Block = todo

  def lastBlock(blockchain: Blockchain): Block = blockchain match
      case _:Ethereum => todo
      case _:Bitcoin  => todo
      case _:Polkadot => todo
  

object Lib3:
  import Lib1.*

  case class Polygon() extends Blockchain:
    override def getBlock(height: Height): Block = todo

import Lib1.*, Lib2.*, Lib3.*
println(lastBlock(Bitcoin()))
println(lastBlock(Ethereum()))
println(lastBlock(Polkadot()))
println(lastBlock(Polygon()))

วิธีการแก้ปัญหานี้เป็นการปรับใช้ความหลากหลายตามประเภทที่เป็นภาษาอบอยู่แล้ว!

`Lib1` ไม่ถูกแตะต้อง (โปรดจำไว้ว่า มีการบังคับใช้กฎ 4 ทั่วทั้งเอกสารนี้) 

โซลูชันที่นำไปใช้ใน `Lib2` คือ โอเค จนกว่าจะมีการเปิดตัวบล็อคเชนอื่นใน `Lib3`. มันละเมิดกฎความปลอดภัยของประเภท (กฎ 3) เนื่องจากรหัสนี้ล้มเหลวที่รันไทม์บนบรรทัด 37 และการแก้ไข `Lib2` จะละเมิดกฎข้อ 4

วิธีแก้ไขปัญหาอื่นคือการใช้ `extension`.

สกาล่า

def todo = 42
type Height = Int
type Block = Int

object Lib1:
  trait Blockchain:
    def getBlock(height: Height): Block

  case class Ethereum() extends Blockchain:
    override def getBlock(height: Height) = todo

  case class Bitcoin() extends Blockchain:
    override def getBlock(height: Height) = todo

object Lib2:
  import Lib1.*

  case class Polkadot() extends Blockchain:
    override def getBlock(height: Height): Block = todo

    def lastBlock(): Block = todo

  extension (eth: Ethereum) def lastBlock(): Block = todo

  extension (btc: Bitcoin) def lastBlock(): Block = todo

import Lib1.*, Lib2.*
println(Bitcoin().lastBlock())
println(Ethereum().lastBlock())
println(Polkadot().lastBlock())

def polymorphic(blockchain: Blockchain) =
  // blockchain.lastBlock()
  ???

`Lib1` ไม่ถูกแตะต้อง (การบังคับใช้กฎ 4 ในเอกสารทั้งหมด) 

`Lib2` กำหนดพฤติกรรมสำหรับประเภทของมัน (บรรทัดที่ 21) และ `ส่วนขยายสำหรับประเภทที่มีอยู่ (บรรทัดที่ 23 และ 25)

บรรทัดที่ 28-30 ลักษณะการทำงานใหม่สามารถใช้ได้ในแต่ละคลาส 

แต่ไม่มีทางที่จะเรียกพฤติกรรมใหม่นี้ว่ามีความหลากหลาย (บรรทัดที่ 32) ความพยายามใดๆ ที่จะทำเช่นนั้นทำให้เกิดข้อผิดพลาดในการคอมไพล์ (บรรทัดที่ 33) หรือการพิมพ์สวิตช์ตาม 

กฎข้อที่ 2 นี้ยุ่งยาก เราพยายามใช้มันด้วยคำจำกัดความของความหลากหลายและเคล็ดลับ "ส่วนขยาย" ของเราเอง และนั่นก็แปลก

มีชิ้นส่วนที่หายไปเรียกว่า ความหลากหลายเฉพาะกิจ: ความสามารถในการจัดส่งการนำพฤติกรรมไปใช้ตามประเภทได้อย่างปลอดภัย ไม่ว่าจะกำหนดพฤติกรรมและประเภทไว้ที่ใดก็ตาม ป้อน ประเภทคลาส ลวดลาย

รูปแบบคลาสประเภท

สูตรรูปแบบ Type Class (เรียกสั้น ๆ ว่า TC) มี 3 ขั้นตอน 

  1. กำหนดพฤติกรรมใหม่
  2. ประพฤติปฏิบัติ
  3. ใช้พฤติกรรม

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

1. กำหนดพฤติกรรมใหม่
สกาล่า

object Lib2:
  import Lib1.*

  trait LastBlock[A]:
    def lastBlock(instance: A): Block

`Lib1` เป็นอีกครั้งหนึ่งที่ถูกปล่อยทิ้งไว้โดยไม่มีใครแตะต้อง

พฤติกรรมใหม่ๆ is TC ปรากฏตามลักษณะ ฟังก์ชั่นที่กำหนดไว้ในลักษณะเป็นวิธีการประยุกต์ลักษณะบางอย่างของพฤติกรรมนั้น

พารามิเตอร์ `A` แสดงถึงประเภทที่เราต้องการปรับใช้พฤติกรรม ซึ่งเป็นประเภทย่อยของ `Blockchain` ในกรณีของเรา.

ข้อสังเกตบางประการ:

  • หากจำเป็น ให้ระบุประเภทพารามิเตอร์ `A` สามารถถูกจำกัดเพิ่มเติมโดยระบบประเภท Scala ตัวอย่างเช่น เราสามารถบังคับใช้ `A` เพื่อเป็น `Blockchain`. 
  • นอกจากนี้ TC อาจมีฟังก์ชันอื่นๆ อีกมากมายที่ประกาศไว้ด้วย
  • ในที่สุด แต่ละฟังก์ชันอาจมีพารามิเตอร์ที่กำหนดเองได้อีกมากมาย

แต่มาทำให้สิ่งต่าง ๆ เรียบง่ายเพื่อความสะดวกในการอ่าน

2. นำพฤติกรรมไปใช้
สกาล่า

object Lib2:
  import Lib1.*

  trait LastBlock[A]:
    def lastBlock(instance: A): Block

  val ethereumLastBlock = new LastBlock[Ethereum]:
    def lastBlock(eth: Ethereum) = eth.lastBlock

  val bitcoinLastBlock = new LastBlock[Bitcoin]:
    def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

สำหรับแต่ละประเภท ` ใหม่LastBlock` พฤติกรรมเป็นสิ่งที่คาดหวัง มีตัวอย่างเฉพาะของพฤติกรรมนั้น 

`Ethereum` บรรทัดการใช้งาน 22 คำนวณจาก `eth` อินสแตนซ์ที่ส่งผ่านเป็นพารามิเตอร์ 

การดำเนินการของ `LastBlock` สำหรับ 'Bitcoin` บรรทัด 25 ถูกนำมาใช้กับ IO ที่ไม่มีการจัดการและไม่ได้ใช้พารามิเตอร์

ดังนั้น `Lib2` ใช้พฤติกรรมใหม่ `LastBlock` สำหรับ 'Lib1` ชั้นเรียน

3.ใช้พฤติกรรม
สกาล่า

object Lib2:
  import Lib1.*

  trait LastBlock[A]:
    def lastBlock(instance: A): Block

  val ethereumLastBlock = new LastBlock[Ethereum]:
    def lastBlock(eth: Ethereum) = eth.lastBlock

  val bitcoinLastBlock = new LastBlock[Bitcoin]:
    def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

import Lib1.*, Lib2.*

def useLastBlock[A](instance: A, behavior: LastBlock[A]) =
  behavior.lastBlock(instance)

println(useLastBlock(Ethereum(lastBlock = 2), ethereumLastBlock))
println(useLastBlock(Bitcoin(), bitcoinLastBlock))

บรรทัดที่ 30 `useLastBlock` ใช้อินสแตนซ์ของ `A` และ 'LastBlock` พฤติกรรมที่กำหนดไว้สำหรับอินสแตนซ์นั้น

บรรทัดที่ 33 `useLastBlock` ถูกเรียกด้วยอินสแตนซ์ของ `Ethereum` และการดำเนินการของ `LastBlock` กำหนดไว้ใน `Lib2`. โปรดทราบว่าคุณสามารถส่งผ่านการใช้งานแบบอื่นของ ` ได้LastBlock[A]`(คิดถึง. การฉีดพึ่งพา).

`useLastBlock` คือกาวระหว่างการเป็นตัวแทน (A จริง) และพฤติกรรมของมัน ข้อมูลและพฤติกรรมจะถูกแยกออกจากกัน ซึ่งเป็นสิ่งที่สนับสนุนการเขียนโปรแกรมเชิงฟังก์ชัน

การสนทนา

เรามาสรุปกฎของปัญหานิพจน์กัน:

  • กฎข้อที่ 1: อนุญาตให้มีการดำเนินการของ พฤติกรรมที่มีอยู่  ที่จะนำไปใช้กับ ชั้นเรียนใหม่
  • กฎข้อที่ 2:  อนุญาตให้มีการดำเนินการของ พฤติกรรมใหม่ ที่จะนำไปใช้กับ ชั้นเรียนที่มีอยู่
  • กฎข้อที่ 3: มันจะต้องไม่เป็นอันตรายต่อ ประเภทความปลอดภัย
  • กฎข้อที่ 4: จะต้องไม่จำเป็นต้องคอมไพล์ใหม่ รหัสที่มีอยู่

กฎข้อที่ 1 สามารถแก้ไขได้นอกกรอบด้วยความหลากหลายชนิดย่อย

รูปแบบ TC ที่เพิ่งนำเสนอ (ดูภาพหน้าจอก่อนหน้า) แก้กฎ 2 เป็นประเภทที่ปลอดภัย (กฎ 3) และเราไม่เคยแตะต้อง `Lib1` (กฎข้อ 4) 

อย่างไรก็ตาม ไม่เหมาะสมที่จะใช้ด้วยเหตุผลหลายประการ:

  • บรรทัดที่ 33-34 เราต้องถ่ายทอดพฤติกรรมไปพร้อมๆ กับตัวอย่างอย่างชัดเจน นี่เป็นค่าใช้จ่ายเพิ่มเติม เราควรจะเขียนว่า `useLastBlock(Bitcoin())`.
  • บรรทัดที่ 31 ไวยากรณ์เป็นเรื่องผิดปกติ เราอยากจะเขียนให้กระชับและเน้นไปที่วัตถุมากกว่า  `instance.lastBlock()`คำสั่ง

เรามาเน้นคุณสมบัติบางอย่างของ Scala สำหรับการใช้งาน TC ในทางปฏิบัติกันดีกว่า 

ประสบการณ์นักพัฒนาที่ได้รับการปรับปรุง

Scala มีชุดคุณสมบัติเฉพาะตัวและวากยสัมพันธ์ที่ทำให้ TC เป็นประสบการณ์ที่สนุกสนานอย่างแท้จริงสำหรับนักพัฒนา

โดยปริยาย

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

โปรแกรมวางอินสแตนซ์ไว้ในขอบเขตโดยนัยด้วย `given` คำสำคัญ อีกทางหนึ่ง โปรแกรมสามารถดึงอินสแตนซ์จากขอบเขตโดยนัยด้วยคำหลัก `using`.

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

แตกต่างจากขอบเขตทั่วโลกเนื่องจาก: 

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

กลับไปสู่คลาสประเภท! ลองใช้ตัวอย่างเดียวกันนี้กัน

สกาล่า

def todo = 42
type Height = Int
type Block = Int
def http(uri: String): Block = todo

object Lib1:
  trait Blockchain:
    def getBlock(height: Height): Block

  case class Ethereum() extends Blockchain:
    override def getBlock(height: Height) = todo

  case class Bitcoin() extends Blockchain:
    override def getBlock(height: Height) = todo

`Lib1` เป็นรหัสที่ยังไม่ได้แก้ไขเดียวกันกับที่เรากำหนดไว้ก่อนหน้านี้ 

สกาล่า

object Lib2:
  import Lib1.*

  trait LastBlock[A]:
    def lastBlock(instance: A): Block

  given ethereumLastBlock:LastBlock[Ethereum] = new LastBlock[Ethereum]:
    def lastBlock(eth: Ethereum) = eth.lastBlock

  given bitcoinLastBlock:LastBlock[Bitcoin] = new LastBlock[Bitcoin]:
    def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

import Lib1.*, Lib2.*

def useLastBlock[A](instance: A)(using behavior: LastBlock[A]) =
  behavior.lastBlock(instance)

println(useLastBlock(Ethereum(lastBlock = 2)))
println(useLastBlock(Bitcoin()))

บรรทัดที่ 19 พฤติกรรมใหม่ `LastBlock` ถูกกำหนดไว้เหมือนกับที่เราเคยทำก่อนหน้านี้ทุกประการ

บรรทัดที่ 22 และบรรทัดที่ 25, `val` ถูกแทนที่ด้วย `given`. การใช้งานทั้งสองของ `LastBlock` อยู่ในขอบเขตโดยนัย

บรรทัดที่ 31 `useLastBlock` ประกาศพฤติกรรม `LastBlock` เป็นพารามิเตอร์โดยนัย คอมไพเลอร์แก้ไขอินสแตนซ์ที่เหมาะสมของ `LastBlock` จากขอบเขตโดยนัยตามบริบทจากตำแหน่งผู้โทร (สาย 33 และ 34) บรรทัดที่ 28 นำเข้าทุกอย่างจาก `Lib2` รวมถึงขอบเขตโดยนัยด้วย ดังนั้น คอมไพเลอร์ส่งผ่านอินสแตนซ์ที่กำหนดบรรทัด 22 และ 25 เป็นพารามิเตอร์สุดท้ายของ `useLastBlock`. 

ในฐานะผู้ใช้ไลบรารี การใช้คลาสประเภทจะง่ายกว่าเมื่อก่อน บรรทัดที่ 34 และ 35 นักพัฒนาต้องทำให้แน่ใจว่าอินสแตนซ์ของพฤติกรรมถูกแทรกเข้าไปในขอบเขตโดยนัย (และอาจเป็นเพียง `import`) หากนัยไม่ใช่ `given` โดยที่รหัสอยู่ `using` มันคอมไพเลอร์บอกเขา

โดยนัยของ Scala ช่วยให้งานผ่านอินสแตนซ์ของคลาสง่ายขึ้นพร้อมกับอินสแตนซ์ของพฤติกรรมของพวกเขา

น้ำตาลโดยนัย

บรรทัดที่ 22 และ 25 ของโค้ดก่อนหน้าสามารถปรับปรุงเพิ่มเติมได้ ! มาย้ำเกี่ยวกับการใช้งาน TC กัน

สกาล่า

given LastBlock[Ethereum] = new LastBlock[Ethereum]:
    def lastBlock(eth: Ethereum) = eth.lastBlock

  given LastBlock[Bitcoin] = new LastBlock[Bitcoin]:
    def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

บรรทัดที่ 22 และ 25 หากไม่ได้ใช้ชื่อของอินสแตนซ์ ก็สามารถละเว้นได้

สกาล่า


  given LastBlock[Ethereum] with
    def lastBlock(eth: Ethereum) = eth.lastBlock

  given LastBlock[Bitcoin] with
    def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

บรรทัดที่ 22 และ 25 การซ้ำของประเภทสามารถแทนที่ได้ด้วย `with` คำสำคัญ

สกาล่า

given LastBlock[Ethereum] = _.lastBlock

  given LastBlock[Bitcoin] = _ => http("http://bitcoin/last")

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

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

นามสกุล

ใช้อย่างชาญฉลาด `extension`กลไกสามารถลดความซับซ้อนของไวยากรณ์สำหรับการใช้คลาสประเภท

สกาล่า

object Lib2:
  import Lib1.*

  trait LastBlock[A]:
    def lastBlock(instance: A): Block

  given LastBlock[Ethereum] with
    def lastBlock(eth: Ethereum) = eth.lastBlock

  given LastBlock[Bitcoin] with
    def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

  extension[A](instance: A)
    def lastBlock(using tc: LastBlock[A]) = tc.lastBlock(instance)

import Lib1.*, Lib2.*

println(Ethereum(lastBlock = 2).lastBlock)
println(Bitcoin().lastBlock)

บรรทัดที่ 28-29 เป็นวิธีการขยายทั่วไป `lastBlock` ถูกกำหนดไว้สำหรับ ` ใดๆA` ด้วย `LastBlock` พารามิเตอร์ TC ในขอบเขตโดยนัย

บรรทัดที่ 33-34 ส่วนขยายใช้ประโยชน์จากไวยากรณ์เชิงวัตถุเพื่อใช้ TC

สกาล่า

object Lib2:
  import Lib1.*

  trait LastBlock[A]:
    def lastBlock(instance: A): Block

  given LastBlock[Ethereum] with
    def lastBlock(eth: Ethereum) = eth.lastBlock

  given LastBlock[Bitcoin] with
    def lastBlock(btc: Bitcoin) = http("http://bitcoin/last")

  extension[A](instance: A)(using tc: LastBlock[A])
    def lastBlock = tc.lastBlock(instance)
    def penultimateBlock = tc.lastBlock(instance) - 1

import Lib1.*, Lib2.*

val eth = Ethereum(lastBlock = 2)
println(eth.lastBlock)
println(eth.penultimateBlock)

val btc = Bitcoin()
println(btc.lastBlock)
println(btc.penultimateBlock)

บรรทัดที่ 28 สามารถกำหนดพารามิเตอร์ TC สำหรับส่วนขยายทั้งหมดเพื่อหลีกเลี่ยงการทำซ้ำ บรรทัดที่ 30 เราใช้ TC ซ้ำในส่วนขยายเพื่อกำหนด `penultimateBlock` (แม้ว่าจะสามารถนำไปใช้กับ `LastBlock` ลักษณะโดยตรง)

ความมหัศจรรย์เกิดขึ้นเมื่อมีการใช้ TC การแสดงออกให้ความรู้สึกเป็นธรรมชาติมากขึ้น ทำให้เกิดภาพลวงตาว่าเป็นพฤติกรรม `lastBlock` เชื่อมโยงกับอินสแตนซ์

ประเภททั่วไปที่มี TC
สกาล่า

import Lib1.*, Lib2.*

def useLastBlock1[A](instance: A)(using LastBlock[A]) = instance.lastBlock

def useLastBlock2[A: LastBlock](instance: A) = instance.lastBlock

val eth = Ethereum(lastBlock = 2)
assert(useLastBlock1(eth) == useLastBlock2(eth))

บรรทัดที่ 34 ฟังก์ชันใช้ TC โดยนัย โปรดทราบว่าไม่จำเป็นต้องตั้งชื่อ TC หากชื่อนั้นไม่จำเป็น

รูปแบบ TC มีการใช้กันอย่างแพร่หลายจนมีไวยากรณ์ประเภททั่วไปเพื่อแสดง "ประเภทที่มีพฤติกรรมโดยนัย" บรรทัดที่ 36 ไวยากรณ์เป็นทางเลือกที่กระชับกว่ารูปแบบก่อนหน้า (บรรทัดที่ 34) โดยหลีกเลี่ยงการประกาศพารามิเตอร์ TC โดยนัยที่ไม่มีชื่อโดยเฉพาะ

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

ที่มาอัตโนมัติ

ไลบรารี Scala จำนวนมากใช้ TC โดยปล่อยให้โปรแกรมเมอร์นำไปใช้ในฐานโค้ดของตน

ตัวอย่างเช่น Circe (ไลบรารีการดีซีเรียลไลเซชัน json) ใช้ TC `Encoder[T]` และ `Decoder[T]` เพื่อให้โปรแกรมเมอร์นำไปใช้ในโค้ดเบสของตน เมื่อนำไปใช้แล้ว ขอบเขตทั้งหมดของห้องสมุดจะสามารถใช้งานได้ 

การใช้งาน TC เหล่านั้นมีบ่อยกว่าปกติ ผู้ทำแผนที่เชิงข้อมูล. พวกเขาไม่ต้องการตรรกะทางธุรกิจ น่าเบื่อในการเขียน และเป็นภาระที่ต้องรักษาให้สอดคล้องกับคลาสเคส

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

ภายใต้ประทุนในเวลารวบรวมมาโครทั่วไปจะพิจารณาใคร่ครวญ ชนิด เป็นโครงสร้างข้อมูลล้วนๆ และสร้าง TC[T] สำหรับผู้ใช้ห้องสมุด 

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

สกาล่า

object GenericLib:

  trait Named[A]:
    def blockchainName(instance: A): String

  object Named:
    import scala.deriving.*

    inline final def derived[A](using inline m: Mirror.Of[A]): Named[A] =
      val nameOfType: String = inline m match
        case p: Mirror.ProductOf[A] => compiletime.constValue[p.MirroredLabel]
        case _ => compiletime.error("Not a product")
      new Named[A]:
        override def blockchainName(instance: A):String = nameOfType.toLowerCase

  extension[A] (instance: A)(using tc: Named[A])
    def blockchainName = tc.blockchainName(instance)

import Lib1.*, GenericLib.*

case class Polkadot() derives Named
given Named[Bitcoin] = Named.derived
given Named[Ethereum] = Named.derived

println(Ethereum(lastBlock = 2).blockchainName)
println(Bitcoin().blockchainName)
println(Polkadot().blockchainName)

บรรทัดที่ 18 TC ใหม่ `Named` ได้รับการแนะนำ TC นี้ไม่เกี่ยวข้องกับธุรกิจบล็อกเชนหากพูดอย่างเคร่งครัด จุดประสงค์คือการตั้งชื่อบล็อคเชนตามชื่อของคลาสเคส

อันดับแรกให้เน้นไปที่คำจำกัดความบรรทัดที่ 36-38 มี 2 ​​ไวยากรณ์สำหรับการได้รับ TC:

  1. บรรทัดที่ 36 สามารถกำหนดอินสแตนซ์ TC ได้โดยตรงบนคลาสเคสด้วย `derives` คำสำคัญ ภายใต้ประทุนคอมไพเลอร์จะสร้าง ` ที่กำหนดNamed` ตัวอย่างใน 'Polkadot` วัตถุที่แสดงร่วม
  2. บรรทัดที่ 37 และ 38 อินสแตนซ์คลาสประเภทจะได้รับในคลาสที่มีอยู่แล้วด้วย `TC.derived

บรรทัดที่ 31 มีการกำหนดส่วนขยายทั่วไป (ดูหัวข้อก่อนหน้า) และ `blockchainName` ถูกใช้อย่างเป็นธรรมชาติ  

`derives` คำหลักต้องการวิธีการที่มีรูปแบบ `inline def derived[T](using Mirror.Of[T]): TC[T] = ???` ซึ่งกำหนดไว้ในบรรทัดที่ 24 ฉันจะไม่อธิบายเชิงลึกว่าโค้ดทำอะไร ในโครงร่างกว้างๆ:

  • `inline def` กำหนดมาโคร
  • `Mirror` เป็นส่วนหนึ่งของกล่องเครื่องมือในการวิปัสสนาประเภท มิเรอร์มีหลายประเภท และบรรทัดที่ 26 โค้ดจะเน้นที่ `Product` มิเรอร์ (คลาสเคสคือผลิตภัณฑ์) บรรทัดที่ 27 หากโปรแกรมเมอร์พยายามรับสิ่งที่ไม่ใช่ `Product` โค้ดจะไม่คอมไพล์
  • `Mirror` มีประเภทอื่น ๆ หนึ่งในนั้น `MirrorLabel` คือสตริงที่มีชื่อประเภท ค่านี้ใช้ในการดำเนินการบรรทัดที่ 29 ของ `Named` ทีซี.

ผู้เขียน TC สามารถใช้การเขียนโปรแกรมเมตาเพื่อจัดเตรียมฟังก์ชันที่สร้างอินสแตนซ์ของ TC โดยทั่วไปตามประเภทได้ โปรแกรมเมอร์สามารถใช้ API ไลบรารีเฉพาะหรือเครื่องมือที่ได้รับ Scala เพื่อสร้างอินสแตนซ์สำหรับโค้ดของตน

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

สรุปสิทธิประโยชน์ทั้งหมด

  • มันแก้ปัญหาการแสดงออก
    • ประเภทใหม่สามารถนำพฤติกรรมที่มีอยู่ไปใช้ผ่านการสืบทอดลักษณะแบบดั้งเดิม
    • ลักษณะการทำงานใหม่สามารถนำไปใช้กับประเภทที่มีอยู่ได้
  • แยกความกังวล
    • รหัสไม่เสียหายและลบได้ง่าย TC แยกข้อมูลและพฤติกรรม ซึ่งเป็นคำขวัญการเขียนโปรแกรมเชิงฟังก์ชัน
  • มันปลอดภัย
    • เป็นประเภทที่ปลอดภัยเพราะไม่ต้องพึ่งพาวิปัสสนา หลีกเลี่ยงการจับคู่รูปแบบใหญ่ที่เกี่ยวข้องกับประเภทต่างๆ หากคุณพบว่าตัวเองกำลังเขียนโค้ดดังกล่าว คุณอาจตรวจพบกรณีที่รูปแบบ TC เหมาะสมที่สุด
    • กลไกโดยนัยนั้นรวบรวมได้อย่างปลอดภัย! หากอินสแตนซ์หายไปในขณะคอมไพล์ โค้ดจะไม่คอมไพล์ ไม่แปลกใจเลยที่รันไทม์
  • มันนำมาซึ่งความหลากหลายเฉพาะกิจ
    • Ad hoc polymorphism มักจะหายไปในการเขียนโปรแกรมเชิงวัตถุแบบดั้งเดิม
    • ด้วย ad-hoc polymorphism นักพัฒนาสามารถใช้พฤติกรรมเดียวกันสำหรับประเภทต่างๆ ที่ไม่เกี่ยวข้องกัน โดยไม่ต้องใช้การพิมพ์ย่อยแบบดั้งเดิม (ซึ่งจับคู่โค้ด)
  • การฉีดพึ่งพาทำได้ง่าย
    • อินสแตนซ์ TC สามารถเปลี่ยนแปลงได้ตามหลักการทดแทน Liskov 
    • เมื่อส่วนประกอบต้องพึ่งพา TC ก็สามารถฉีด TC จำลองเพื่อการทดสอบได้อย่างง่ายดาย 

ตัวบ่งชี้ที่เคาน์เตอร์

ค้อนทุกตัวได้รับการออกแบบมาเพื่อแก้ไขปัญหาต่างๆ

คลาสประเภทใช้สำหรับปัญหาด้านพฤติกรรมและต้องไม่ใช้สำหรับการสืบทอดข้อมูล ใช้การจัดองค์ประกอบเพื่อจุดประสงค์นั้น

การพิมพ์ย่อยตามปกติจะตรงไปตรงมามากกว่า หากคุณเป็นเจ้าของ Code Base และไม่ได้มุ่งเป้าไปที่ความสามารถในการขยาย คลาสประเภทอาจจะเกินความจำเป็น

ตัวอย่างเช่น ในแกน Scala จะมี `Numeric` ประเภทคลาส:

สกาล่า

trait Numeric[T] extends Ordering[T] {
  def plus(x: T, y: T): T
  def minus(x: T, y: T): T
  def times(x: T, y: T): T

มันสมเหตุสมผลจริงๆ ที่จะใช้คลาสประเภทดังกล่าว เพราะไม่เพียงแต่อนุญาตให้นำอัลกอริธึมพีชคณิตกลับมาใช้ซ้ำกับประเภทที่ฝังอยู่ใน Scala (Int, BigInt, …) แต่ยังรวมถึงประเภทที่ผู้ใช้กำหนดด้วย (a `ComplexNumber` เป็นต้น)

ในทางกลับกัน การใช้งานคอลเลกชัน Scala ส่วนใหญ่จะใช้การพิมพ์ย่อยแทนคลาสประเภท การออกแบบนี้สมเหตุสมผลด้วยเหตุผลหลายประการ:

  • API การรวบรวมควรจะสมบูรณ์และเสถียร มันเปิดเผยพฤติกรรมทั่วไปผ่านลักษณะที่สืบทอดมาจากการใช้งาน การขยายขอบเขตได้อย่างมากไม่ใช่เป้าหมายเฉพาะที่นี่
  • มันจะต้องใช้งานง่าย TC เพิ่มค่าใช้จ่ายทางจิตให้กับโปรแกรมเมอร์ผู้ใช้ปลายทาง
  • TC อาจมีค่าใช้จ่ายด้านประสิทธิภาพเล็กน้อย นี่อาจมีความสำคัญสำหรับ API คอลเลกชัน
  • แม้ว่า API คอลเลกชันจะยังคงขยายได้ผ่าน TC ใหม่ที่กำหนดโดยไลบรารีบุคคลที่สาม

สรุป

เราได้เห็นแล้วว่า TC เป็นรูปแบบง่ายๆ ที่ช่วยแก้ปัญหาใหญ่ได้ ต้องขอบคุณไวยากรณ์ Scala ที่หลากหลาย ทำให้รูปแบบ TC สามารถนำไปใช้และใช้งานได้หลายวิธี รูปแบบ TC สอดคล้องกับกระบวนทัศน์การเขียนโปรแกรมเชิงฟังก์ชัน และเป็นเครื่องมือที่ยอดเยี่ยมสำหรับสถาปัตยกรรมที่สะอาดตา ไม่มีกระสุนสีเงินและต้องใช้รูปแบบ TC เมื่อพอดี

หวังว่าคุณจะได้รับความรู้จากการอ่านเอกสารนี้ 

รหัสมีจำหน่ายที่ https://github.com/jprudent/type-class-article. โปรดติดต่อฉันหากคุณมีคำถามหรือข้อสังเกตใดๆ คุณสามารถใช้ปัญหาหรือความคิดเห็นเกี่ยวกับโค้ดในพื้นที่เก็บข้อมูลได้หากต้องการ


เจอโรม พรูเดนท์

วิศวกรซอฟต์แวร์

ประทับเวลา:

เพิ่มเติมจาก บัญชีแยกประเภท