Ketik Kelas di Scala3: Panduan Pemula | buku besar

Ketik Kelas di Scala3: Panduan Pemula | buku besar

Node Sumber: 2409157

Dokumen ini ditujukan untuk pengembang Scala3 pemula yang sudah berpengalaman dalam prosa Scala, namun bingung dengan semua `implicits` dan sifat-sifat yang diparameterisasi dalam kode.

Dokumen ini menjelaskan mengapa, bagaimana, dimana dan kapan Jenis Kelas (TC).

Setelah membaca dokumen ini, pengembang Scala3 pemula akan memperoleh pengetahuan yang kuat untuk menggunakan dan mendalami kode sumbernya banyak perpustakaan Scala dan mulai menulis kode Scala idiomatik.

Letโ€™s start with the whyโ€ฆ

Daftar Isi

Masalah ekspresi

Dalam 1998, Philip Wadler menyatakan bahwa โ€œmasalah ekspresi adalah nama baru untuk masalah lamaโ€. Ini adalah masalah ekstensibilitas perangkat lunak. Menurut tulisan Pak Wadler, penyelesaian masalah ekspresi harus mematuhi aturan berikut:

  • Aturan 1: Izinkan penerapan perilaku yang ada (pikirkan sifat Scala) untuk diterapkan representasi baru (pikirkan kelas kasus)
  • Aturan 2:  Izinkan penerapan perilaku baru untuk diterapkan representasi yang ada
  • Aturan 3: Tidak boleh membahayakan ketik keamanan
  • Aturan 4: Tidak perlu dikompilasi ulang kode yang ada

Memecahkan masalah ini akan menjadi benang merah artikel ini.

Aturan 1: implementasi perilaku yang ada pada representasi baru

Bahasa berorientasi objek apa pun memiliki solusi bawaan untuk aturan 1 dengan polimorfisme subtipe. Anda dapat dengan aman mengimplementasikan `trait` didefinisikan dalam ketergantungan pada `class` dalam kode Anda sendiri, tanpa mengkompilasi ulang ketergantungan. Mari kita lihat aksinya:

Scala

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()

Dalam contoh fiktif ini, perpustakaan `Lib1` (baris 5) mendefinisikan suatu sifat `Blockchain` (baris 6) dengan 2 implementasinya (baris 9 & 12). `Lib1` akan tetap sama di SEMUA dokumen ini (penerapan aturan 4).

`Lib2` (baris 15) mengimplementasikan perilaku yang ada `Blockchain`di kelas baru`Polkadot` (aturan 1) dengan cara tipe aman (aturan 3), tanpa kompilasi ulang `Lib1` (aturan 4). 

Aturan 2: penerapan perilaku baru untuk diterapkan pada representasi yang sudah ada

Mari kita bayangkan di `Lib2`kami ingin perilaku baru`lastBlock` untuk diterapkan secara khusus untuk masing-masing `Blockchain`.

Hal pertama yang terlintas dalam pikiran adalah membuat saklar besar berdasarkan jenis parameter.

Scala

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()))

Solusi ini adalah implementasi ulang yang lemah dari polimorfisme berbasis tipe yang sudah ada dalam bahasa!

`Lib1` tidak tersentuh (ingat, aturan 4 diterapkan di seluruh dokumen ini). 

Solusi diimplementasikan di `Lib2` adalah oke sampai blockchain lain diperkenalkan di `Lib3`. Itu melanggar aturan keamanan tipe (aturan 3) karena kode ini gagal saat runtime di baris 37. Dan memodifikasi `Lib2` akan melanggar aturan 4.

Solusi lain adalah menggunakan `extension`.

Scala

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` tidak tersentuh (penerapan aturan 4 di seluruh dokumen). 

`Lib2` mendefinisikan perilaku untuk tipenya (baris 21) dan `ekstensi` untuk tipe yang ada (baris 23 & 25).

Baris 28-30, behavior baru dapat digunakan di setiap kelas. 

Namun tidak ada cara untuk menyebut perilaku baru ini secara polimorfik (baris 32). Setiap upaya untuk melakukannya akan menyebabkan kesalahan kompilasi (baris 33) atau switch berbasis tipe. 

Peraturan nยฐ2 ini rumit. Kami mencoba menerapkannya dengan definisi polimorfisme dan trik `ekstensi` kami sendiri. Dan itu aneh.

Ada bagian yang hilang disebut polimorfisme ad-hoc: kemampuan untuk mengirimkan implementasi perilaku dengan aman menurut suatu tipe, di mana pun perilaku dan tipe tersebut ditentukan. Masukkan Tipe Kelas pola.

Pola Kelas Tipe

Resep pola Type Class (singkatnya TC) memiliki 3 langkah. 

  1. Tentukan perilaku baru
  2. Terapkan perilaku tersebut
  3. Gunakan perilaku tersebut

Di bagian berikut, saya menerapkan pola TC dengan cara yang paling mudah. Ini bertele-tele, kikuk dan tidak praktis. Namun tunggu dulu, peringatan tersebut akan diperbaiki langkah demi langkah lebih lanjut dalam dokumen.

1. Definisikan perilaku baru
Scala

object Lib2:
 import Lib1.*

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

`Lib1` sekali lagi, tidak tersentuh.

Perilaku baru is TC diwujudkan oleh sifat tersebut. Fungsi yang didefinisikan dalam suatu sifat adalah cara untuk menerapkan beberapa aspek perilaku tersebut.

Parameter `A` mewakili tipe yang ingin kita terapkan perilakunya, yang merupakan subtipe dari `Blockchain` dalam kasus kami.

Beberapa komentar:

  • Jika diperlukan, tipe parameter `A` selanjutnya dapat dibatasi oleh sistem tipe Scala. Misalnya, kita dapat menerapkan `A` menjadi `Blockchain`. 
  • Selain itu, TC dapat memiliki lebih banyak fungsi yang dideklarasikan di dalamnya.
  • Terakhir, setiap fungsi mungkin memiliki lebih banyak parameter arbitrer.

Tapi mari kita buat semuanya tetap sederhana demi keterbacaan.

2. Menerapkan perilaku tersebut
Scala

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")

Untuk setiap jenis yang baru `LastBlock` perilaku yang diharapkan, ada contoh spesifik dari perilaku tersebut. 

`Ethereum` implementasi baris 22 dihitung dari `eth` instance diteruskan sebagai parameter. 

Implementasi `LastBlock`untuk`Bitcoin` baris 25 diimplementasikan dengan IO yang tidak dikelola dan tidak menggunakan parameternya.

Jadi, `Lib2` mengimplementasikan perilaku baru `LastBlock`untuk`Lib1` kelas.

3. Gunakan perilaku tersebut
Scala

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))

Baris 30`useLastBlock` menggunakan contoh `A` dan `LastBlock` perilaku yang ditentukan untuk contoh itu.

Baris 33`useLastBlock` dipanggil dengan instance `Ethereum` dan implementasi dari `LastBlock` didefinisikan dalam `Lib2`. Perhatikan bahwa penerapan alternatif apa pun dari ` dapat diteruskanLastBlock[A]` (pikirkan injeksi ketergantungan).

`useLastBlock` adalah perekat antara representasi (A sebenarnya) dan perilakunya. Data dan perilaku dipisahkan, itulah yang dianjurkan oleh pemrograman fungsional.

Diskusi

Mari kita rekap aturan soal ekspresi:

  • Aturan 1: Izinkan penerapan perilaku yang ada  untuk diterapkan kelas baru
  • Aturan 2:  Izinkan penerapan perilaku baru untuk diterapkan kelas yang ada
  • Aturan 3: Tidak boleh membahayakan ketik keamanan
  • Aturan 4: Tidak perlu dikompilasi ulang kode yang ada

Aturan 1 dapat diselesaikan secara langsung dengan polimorfisme subtipe.

Pola TC yang baru saja disajikan (lihat tangkapan layar sebelumnya) menyelesaikan aturan 2. Tipenya aman (aturan 3) dan kami tidak pernah menyentuh `Lib1` (aturan 4). 

Namun ini tidak praktis untuk digunakan karena beberapa alasan:

  • Baris 33-34 kita harus meneruskan perilaku secara eksplisit di sepanjang instance-nya. Ini adalah biaya tambahan. Kita sebaiknya menulis saja `useLastBlock(Bitcoin())`.
  • Baris 31 sintaksnya tidak umum. Kami lebih memilih untuk menulis secara ringkas dan lebih berorientasi objek ``instance.lastBlock()` pernyataan.

Mari kita soroti beberapa fitur Scala untuk penggunaan TC praktis. 

Pengalaman pengembang yang ditingkatkan

Scala memiliki serangkaian fitur unik dan gula sintaksis yang menjadikan TC pengalaman yang benar-benar menyenangkan bagi pengembang.

Tersirat

Cakupan implisit adalah cakupan khusus yang diselesaikan pada waktu kompilasi di mana hanya satu instance dari tipe tertentu yang dapat ada. 

Sebuah program menempatkan sebuah instance dalam lingkup implisit dengan `given` kata kunci. Alternatifnya, suatu program dapat mengambil sebuah instance dari cakupan implisit dengan kata kunci `using`.

Cakupan implisit diselesaikan pada waktu kompilasi, ada cara untuk mengubahnya secara dinamis saat runtime. Jika program dikompilasi, cakupan implisitnya teratasi. Pada saat runtime, tidak mungkin ada instance implisit yang hilang saat instance tersebut digunakan. Satu-satunya kebingungan yang mungkin terjadi mungkin berasal dari penggunaan contoh implisit yang salah, tetapi masalah ini diserahkan pada makhluk antara kursi dan keyboard.

Berbeda dengan cakupan global karena: 

  1. Ini diselesaikan secara kontekstual. Dua lokasi program dapat menggunakan sebuah instance dengan tipe tertentu yang sama dalam cakupan implisit, namun kedua instance tersebut mungkin berbeda.
  2. Di belakang layar, kode meneruskan fungsi argumen implisit ke fungsi hingga penggunaan implisit tercapai. Itu tidak menggunakan ruang memori global.

Kembali ke kelas tipe! Mari kita ambil contoh yang sama persis.

Scala

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` adalah kode yang sama yang tidak dimodifikasi yang kami definisikan sebelumnya. 

Scala

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()))

Baris 19 perilaku baru `LastBlock` didefinisikan, persis seperti yang kita lakukan sebelumnya.

Baris 22 dan baris 25, `val` digantikan oleh `given`. Kedua implementasi `LastBlock` ditempatkan dalam lingkup implisit.

Baris 31`useLastBlock` mendeklarasikan perilaku `LastBlock` sebagai parameter implisit. Kompiler menyelesaikan contoh yang sesuai dari `LastBlock` dari cakupan implisit yang dikontekstualisasikan dari lokasi penelepon (baris 33 dan 34). Baris 28 mengimpor semuanya dari `Lib2`, termasuk cakupan implisit. Jadi, kompiler meneruskan instance yang ditentukan baris 22 dan 25 sebagai parameter terakhir `useLastBlock`. 

Sebagai pengguna perpustakaan, menggunakan kelas tipe lebih mudah dari sebelumnya. Baris 34 dan 35 pengembang hanya perlu memastikan bahwa sebuah instance dari perilaku dimasukkan ke dalam cakupan implisit (dan ini bisa berupa `hanyaimport`). Jika implisit bukan `given`di mana kodenya`using` itu, kompiler memberitahunya.

Implisit Scala memudahkan tugas meneruskan instance kelas beserta contoh perilakunya.

Gula implisit

Baris 22 dan 25 dari kode sebelumnya dapat ditingkatkan lebih lanjut! Mari kita ulangi penerapan TC.

Scala

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")

Baris 22 dan 25, jika nama instance tidak digunakan, dapat dihilangkan.

Scala


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

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

Baris 22 dan 25, pengulangan tipenya dapat diganti dengan `with` kata kunci.

Scala

given LastBlock[Ethereum] = _.lastBlock

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

Karena kami menggunakan sifat yang mengalami degenerasi dengan satu fungsi di dalamnya, IDE mungkin menyarankan untuk menyederhanakan kode dengan ekspresi SAM. Meskipun benar, menurut saya ini bukanlah penggunaan SAM yang tepat, kecuali Anda sedang bermain golf dengan santai.

Scala menawarkan gula sintaksis untuk menyederhanakan sintaksis, menghilangkan penamaan, deklarasi, dan redundansi tipe yang tidak perlu.

Perpanjangan

Digunakan dengan bijak, `extensionMekanisme ` dapat menyederhanakan sintaks untuk menggunakan kelas tipe.

Scala

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)

Baris 28-29 merupakan metode ekstensi umum `lastBlock` didefinisikan untuk semua `A` dengan `LastBlock` Parameter TC dalam cakupan implisit.

Baris 33-34 ekstensi memanfaatkan sintaks berorientasi objek untuk menggunakan TC.

Scala

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)

Baris 28, parameter TC juga dapat ditentukan untuk keseluruhan ekstensi untuk menghindari pengulangan. Baris 30 kami menggunakan kembali TC di ekstensi untuk mendefinisikan `penultimateBlock` (walaupun bisa diterapkan pada `LastBlock` sifat secara langsung)

Keajaiban terjadi ketika TC digunakan. Ekspresinya terasa lebih alami, memberikan ilusi perilaku `lastBlock` digabungkan dengan instance.

Tipe generik dengan TC
Scala

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))

Baris 34 fungsinya menggunakan TC implisit. Perhatikan bahwa TC tidak perlu disebutkan namanya jika nama tersebut tidak diperlukan.

Pola TC digunakan secara luas sehingga terdapat sintaks tipe generik untuk mengekspresikan โ€œtipe dengan perilaku implisitโ€. Sintaks baris 36 merupakan alternatif yang lebih ringkas dari yang sebelumnya (baris 34). Ini menghindari deklarasi secara spesifik parameter TC implisit yang tidak disebutkan namanya.

Ini mengakhiri bagian pengalaman pengembang. Kita telah melihat bagaimana ekstensi, implisit, dan beberapa gula sintaksis dapat memberikan sintaksis yang tidak terlalu berantakan ketika TC digunakan dan didefinisikan.

Derivasi otomatis

Banyak perpustakaan Scala menggunakan TC, membiarkan pemrogram mengimplementasikannya dalam basis kode mereka.

Misalnya Circe (perpustakaan de-serialisasi json) menggunakan TC `Encoder[T]` dan `Decoder[T]` untuk diimplementasikan oleh pemrogram dalam basis kode mereka. Setelah diimplementasikan, seluruh ruang lingkup perpustakaan dapat digunakan. 

Penerapan TC tersebut lebih sering terjadi pembuat peta berorientasi data. Mereka tidak memerlukan logika bisnis apa pun, membosankan untuk ditulis, dan menjadi beban untuk tetap sinkron dengan kelas kasus.

Dalam situasi seperti ini, perpustakaan tersebut menawarkan apa yang disebut otomatis turunan atau setengah otomatis penurunan. Lihat misalnya Circe otomatis dan setengah otomatis penurunan. Dengan derivasi semi-otomatis, pemrogram dapat mendeklarasikan turunan kelas tipe dengan beberapa sintaks minor, sedangkan derivasi otomatis tidak memerlukan modifikasi kode apa pun kecuali untuk impor.

Di balik terpal, pada waktu kompilasi, makro generik melakukan introspeksi jenis sebagai struktur data murni dan menghasilkan TC[T] untuk pengguna perpustakaan. 

Mendapatkan TC secara umum adalah hal yang sangat umum, jadi Scala memperkenalkan kotak alat lengkap untuk tujuan tersebut. Metode ini tidak selalu diiklankan oleh dokumentasi perpustakaan meskipun ini adalah cara Scala 3 menggunakan derivasi.

Scala

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)

Baris 18 adalah TC baru `Named` diperkenalkan. TC ini sebenarnya tidak ada hubungannya dengan bisnis blockchain. Tujuannya adalah memberi nama blockchain berdasarkan nama kelas kasusnya.

Fokus pertama pada definisi baris 36-38. Ada 2 sintaksis untuk mendapatkan TC:

  1. Baris 36 instance TC dapat didefinisikan secara langsung pada kelas kasus dengan `derives` kata kunci. Di balik terpal, kompiler menghasilkan `Named` contoh di `Polkadot` objek pendamping.
  2. Baris 37 dan 38, instance kelas tipe diberikan pada kelas yang sudah ada sebelumnya dengan `TC.derived

Baris 31 ekstensi umum didefinisikan (lihat bagian sebelumnya) dan `blockchainName` digunakan secara alami.  

`derives` kata kunci mengharapkan metode dengan bentuk`inline def derived[T](using Mirror.Of[T]): TC[T] = ???` yang didefinisikan pada baris 24. Saya tidak akan menjelaskan secara mendalam apa fungsi kode tersebut. Secara garis besar:

  • `inline def` mendefinisikan makro
  • `Mirror` adalah bagian dari kotak alat untuk melakukan introspeksi tipe. Ada berbagai jenis mirror, dan baris 26 kodenya berfokus pada `Product` mirror (kelas kasus adalah produk). Baris 27, jika pemrogram mencoba menurunkan sesuatu yang bukan `Product`, kode tidak dapat dikompilasi.
  • `Mirror` berisi tipe lain. Salah satunya, `MirrorLabel`, adalah string yang berisi nama tipe. Nilai ini digunakan dalam implementasi, baris 29, dari `Named`TC.

Penulis TC dapat menggunakan pemrograman meta untuk menyediakan fungsi yang secara umum menghasilkan instance TC berdasarkan tipenya. Pemrogram dapat menggunakan API perpustakaan khusus atau alat turunan Scala untuk membuat instance untuk kode mereka.

Baik Anda memerlukan kode umum atau spesifik untuk mengimplementasikan TC, selalu ada solusi untuk setiap situasi. 

Ringkasan semua manfaat

  • Ini memecahkan masalah ekspresi
    • Tipe baru dapat menerapkan perilaku yang ada melalui pewarisan sifat tradisional
    • Perilaku baru dapat diimplementasikan pada tipe yang sudah ada
  • Pemisahan kekhawatiran
    • Kode tidak rusak dan mudah dihapus. TC memisahkan data dan perilaku, yang merupakan moto pemrograman fungsional.
  • Itu aman
    • Tipe ini aman karena tidak mengandalkan introspeksi. Ini menghindari pencocokan pola besar yang melibatkan tipe. jika Anda menemukan diri Anda menulis kode seperti itu, Anda mungkin mendeteksi kasus di mana pola TC akan sangat cocok.
    • Mekanisme implisitnya aman untuk dikompilasi! Jika sebuah instance hilang pada waktu kompilasi, kode tidak akan dapat dikompilasi. Tidak mengherankan saat runtime.
  • Ini membawa polimorfisme ad-hoc
    • Polimorfisme ad hoc biasanya tidak ada dalam pemrograman berorientasi objek tradisional.
    • Dengan polimorfisme ad-hoc, pengembang dapat menerapkan perilaku yang sama untuk berbagai tipe yang tidak terkait tanpa menggunakan sub pengetikan tradisional (yang memasangkan kode)
  • Injeksi ketergantungan menjadi mudah
    • Contoh TC dapat diubah sehubungan dengan prinsip substitusi Liskov. 
    • Ketika komponen memiliki ketergantungan pada TC, TC tiruan dapat dengan mudah dimasukkan untuk tujuan pengujian. 

Kontra indikasi

Setiap palu dirancang untuk berbagai masalah.

Kelas Tipe ditujukan untuk masalah perilaku dan tidak boleh digunakan untuk pewarisan data. Gunakan komposisi untuk tujuan itu.

Subtipe biasa lebih mudah. Jika Anda memiliki basis kode dan tidak menginginkan ekstensibilitas, kelas tipe mungkin berlebihan.

Misalnya, di inti Scala, ada `Numeric` ketik kelas:

Scala

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

Sangat masuk akal untuk menggunakan kelas tipe seperti itu karena tidak hanya memungkinkan penggunaan kembali algoritma aljabar pada tipe yang tertanam dalam Scala (Int, BigInt,โ€ฆ), tetapi juga pada tipe yang ditentukan pengguna (a `ComplexNumber` misalnya).

Di sisi lain, implementasi koleksi Scala sebagian besar menggunakan subtipe daripada kelas tipe. Desain ini masuk akal karena beberapa alasan:

  • API pengumpulan seharusnya lengkap dan stabil. Ini memperlihatkan perilaku umum melalui sifat-sifat yang diwarisi oleh implementasi. Menjadi sangat dapat diperluas bukanlah tujuan khusus di sini.
  • Itu harus mudah digunakan. TC menambah beban mental pada pemrogram pengguna akhir.
  • TC mungkin juga menimbulkan sedikit overhead dalam kinerja. Ini mungkin penting untuk API koleksi.
  • Meskipun demikian, API koleksi masih dapat diperluas melalui TC baru yang ditentukan oleh perpustakaan pihak ketiga.

Kesimpulan

Kita telah melihat bahwa TC adalah pola sederhana yang memecahkan masalah besar. Berkat sintaksis Scala yang kaya, pola TC dapat diimplementasikan dan digunakan dalam banyak cara. Pola TC sejalan dengan paradigma pemrograman fungsional dan merupakan alat luar biasa untuk arsitektur bersih. Tidak ada solusi yang tepat dan pola TC harus diterapkan jika cocok.

Semoga Anda memperoleh pengetahuan dengan membaca dokumen ini. 

Kode tersedia di https://github.com/jprudent/type-class-article. Silakan hubungi saya jika Anda memiliki pertanyaan atau komentar. Anda dapat menggunakan isu atau komentar kode di repositori jika Anda mau.


Jerome BIJAKSANA

Software Engineer

Stempel Waktu:

Lebih dari Buku besar