Τύπος μαθημάτων στο Scala3: Οδηγός για αρχάριους | Καθολικό

Τύπος μαθημάτων στο Scala3: A Beginner's Guide | Καθολικό

Κόμβος πηγής: 3028113

Αυτό το έγγραφο προορίζεται για τον αρχάριο προγραμματιστή Scala3 που είναι ήδη έμπειρος στην πρόζα του Scala, αλλά έχει προβληματιστεί σχετικά με όλα τα «implicits` και παραμετροποιημένα χαρακτηριστικά στον κώδικα.

Αυτό το έγγραφο εξηγεί το γιατί, πώς, πού και πότε Κατηγορίες τύπου (TC).

Μετά την ανάγνωση αυτού του εγγράφου, ο αρχάριος προγραμματιστής του Scala3 θα αποκτήσει στέρεες γνώσεις για τη χρήση και τον πηγαίο κώδικα του πολύ των βιβλιοθηκών Scala και αρχίστε να γράφετε ιδιωματικό κώδικα Scala.

Ας ξεκινήσουμε με το γιατί…

Το πρόβλημα της έκφρασης

Σε 1998, δήλωσε ο Philip Wadler ότι «το πρόβλημα έκφρασης είναι ένα νέο όνομα για ένα παλιό πρόβλημα». Είναι το πρόβλημα της επεκτασιμότητας του λογισμικού. Σύμφωνα με τον κύριο Wadler που γράφει, η λύση στο πρόβλημα έκφρασης πρέπει να συμμορφώνεται με τους ακόλουθους κανόνες:

  • Κανόνας 1: Επιτρέψτε την εφαρμογή του υπάρχουσες συμπεριφορές (σκεφτείτε το χαρακτηριστικό Scala) που πρέπει να εφαρμοστεί νέες παραστάσεις (σκεφτείτε μια κατηγορία περίπτωσης)
  • Κανόνας 2:  Να επιτρέπεται η εφαρμογή του νέες συμπεριφορές να εφαρμοστεί σε υπάρχουσες παραστάσεις
  • Κανόνας 3: Δεν πρέπει να θέτει σε κίνδυνο την ασφάλεια τύπου
  • Κανόνας 4: Δεν πρέπει να απαιτείται εκ νέου μεταγλώττιση υπάρχον κώδικα

Η επίλυση αυτού του προβλήματος θα είναι το ασημένιο νήμα αυτού του άρθρου.

Κανόνας 1: εφαρμογή της υπάρχουσας συμπεριφοράς σε νέα αναπαράσταση

Οποιαδήποτε αντικειμενοστραφή γλώσσα έχει μια ψημένη λύση για τον κανόνα 1 με υποτύπος πολυμορφισμός. Μπορείτε να εφαρμόσετε με ασφάλεια οποιοδήποτε `trait« ορίζεται σε μια εξάρτηση από ένα «class` στον δικό σας κώδικα, χωρίς να μεταγλωττίσετε ξανά την εξάρτηση. Ας το δούμε στην πράξη:

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

Σε αυτό το εικονικό παράδειγμα, βιβλιοθήκη `Lib1« (γραμμή 5) ορίζει ένα χαρακτηριστικό `Blockchain` (γραμμή 6) με 2 υλοποιήσεις του (γραμμές 9 & 12). `Lib1` θα παραμείνει το ίδιο σε ΟΛΑ αυτό το έγγραφο (επιβολή του κανόνα 4).

`Lib2« (γραμμή 15) υλοποιεί την υπάρχουσα συμπεριφορά `Blockchain«σε νέα τάξη».Polkadot` (κανόνας 1) με τρόπο ασφαλή τύπου (κανόνας 3), χωρίς επαναμεταγλώττιση`Lib1(κανόνας 4). 

Κανόνας 2: εφαρμογή νέων συμπεριφορών που θα εφαρμοστούν σε υπάρχουσες αναπαραστάσεις

Ας φανταστούμε στο `Lib2«Θέλουμε μια νέα συμπεριφορά».lastBlock«θα εφαρμοστεί ειδικά για κάθε».Blockchain`.

Το πρώτο πράγμα που έρχεται στο μυαλό είναι η δημιουργία ενός μεγάλου διακόπτη με βάση τον τύπο της παραμέτρου.

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

Αυτή η λύση είναι μια αδύναμη επανεφαρμογή του πολυμορφισμού βασισμένου σε τύπους που είναι ήδη ψημένη στη γλώσσα!

`Lib1` παραμένει ανέγγιχτο (θυμηθείτε, επιβάλλεται ο κανόνας 4 σε όλο αυτό το έγγραφο). 

Η λύση που εφαρμόστηκε στο `Lib2` είναι εντάξει μέχρι να εισαχθεί ένα ακόμη blockchain στο `Lib3`. Παραβιάζει τον κανόνα ασφαλείας τύπου (κανόνας 3) επειδή αυτός ο κώδικας αποτυγχάνει κατά το χρόνο εκτέλεσης στη γραμμή 37. Και τροποποιεί `Lib2«θα παραβίαζε τον κανόνα 4.

Μια άλλη λύση είναι η χρήση ενός «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` παραμένει ανέγγιχτη (επιβολή του κανόνα 4 σε ολόκληρο το έγγραφο). 

`Lib2« ορίζει συμπεριφορά για τον τύπο του (γραμμή 21) και «επέκταση» για υπάρχοντες τύπους (γραμμές 23 & 25).

Γραμμές 28-30, η νέα συμπεριφορά μπορεί να χρησιμοποιηθεί σε κάθε τάξη. 

Αλλά δεν υπάρχει τρόπος να ονομάσουμε αυτή τη νέα συμπεριφορά πολυμορφικά (γραμμή 32). Οποιαδήποτε προσπάθεια να γίνει αυτό οδηγεί σε σφάλματα μεταγλώττισης (γραμμή 33) ή σε διακόπτες που βασίζονται στον τύπο. 

Αυτός ο κανόνας αριθ. 2 είναι δύσκολος. Προσπαθήσαμε να το εφαρμόσουμε με τον δικό μας ορισμό του πολυμορφισμού και του κόλπου «επέκτασης». Και αυτό ήταν περίεργο.

Λείπει ένα κομμάτι που ονομάζεται ad-hoc πολυμορφισμός: τη δυνατότητα ασφαλούς αποστολής μιας υλοποίησης συμπεριφοράς σύμφωνα με έναν τύπο, όπου και αν ορίζεται η συμπεριφορά και ο τύπος. Εισάγετε το Τάξη τύπου μοτίβο.

Το μοτίβο Type Class

Η συνταγή για μοτίβο κατηγορίας τύπου (TC για συντομία) έχει 3 βήματα. 

  1. Καθορίστε μια νέα συμπεριφορά
  2. Εφαρμόστε τη συμπεριφορά
  3. Χρησιμοποιήστε τη συμπεριφορά

Στην επόμενη ενότητα, εφαρμόζω το μοτίβο TC με τον πιο απλό τρόπο. Είναι περίπλοκο, βαρετό και μη πρακτικό. Αλλά περιμένετε, αυτές οι προειδοποιήσεις θα διορθωθούν βήμα προς βήμα περαιτέρω στο έγγραφο.

1. Καθορίστε μια νέα συμπεριφορά
Scala

object Lib2:
  import Lib1.*

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

`Lib1μένει, για άλλη μια φορά, ανέγγιχτη.

Η νέα συμπεριφορά is το TC υλοποιήθηκε από το χαρακτηριστικό. Οι συναρτήσεις που ορίζονται στο χαρακτηριστικό είναι ένας τρόπος εφαρμογής ορισμένων πτυχών αυτής της συμπεριφοράς.

Η παράμετρος `AΤο ` αντιπροσωπεύει τον τύπο στον οποίο θέλουμε να εφαρμόσουμε συμπεριφορά, οι οποίοι είναι υποτύποι του `Blockchain«στην περίπτωσή μας.

Μερικές παρατηρήσεις:

  • Εάν χρειάζεται, ο παραμετροποιημένος τύπος `A« μπορεί να περιοριστεί περαιτέρω από το σύστημα τύπου Scala. Για παράδειγμα, θα μπορούσαμε να επιβάλουμε `A«να είσαι ένα».Blockchain`. 
  • Επίσης, το TC θα μπορούσε να έχει πολλές περισσότερες λειτουργίες δηλωμένες σε αυτό.
  • Τέλος, κάθε συνάρτηση μπορεί να έχει πολλές περισσότερες αυθαίρετες παραμέτρους.

Αλλά ας κρατήσουμε τα πράγματα απλά για λόγους αναγνωσιμότητας.

2. Εφαρμόστε τη συμπεριφορά
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")

Για κάθε τύπο το νέο `LastBlock` αναμένεται συμπεριφορά, υπάρχει ένα συγκεκριμένο παράδειγμα αυτής της συμπεριφοράς. 

Το `EthereumΗ γραμμή υλοποίησης 22 υπολογίζεται από το `ethΤο παράδειγμα πέρασε ως παράμετρος. 

Η εφαρμογή του `LastBlock`για`BitcoinΗ γραμμή 25 υλοποιείται με μη διαχειριζόμενο IO και δεν χρησιμοποιεί την παράμετρό του.

Λοιπόν, `Lib2«εφαρμόζει νέα συμπεριφορά».LastBlock`για`Lib1τάξεις.

3. Χρησιμοποιήστε τη συμπεριφορά
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))

Γραμμή 30 `useLastBlockΤο ` χρησιμοποιεί μια παρουσία του `Aκαι το `LastBlock« συμπεριφορά που ορίζεται για εκείνη την περίπτωση.

Γραμμή 33 `useLastBlockΤο ` καλείται με μια παρουσία του `Ethereum«και μια εφαρμογή του «LastBlock« ορίζεται στο «Lib2`. Σημειώστε ότι είναι δυνατό να περάσει οποιαδήποτε εναλλακτική υλοποίηση του `LastBlock[A](σκέψου ένεση εξάρτησης).

`useLastBlock` είναι η κόλλα μεταξύ της αναπαράστασης (το πραγματικό Α) και της συμπεριφοράς της. Τα δεδομένα και η συμπεριφορά διαχωρίζονται, κάτι που πρεσβεύει ο λειτουργικός προγραμματισμός.

Ερωτήσεις - Συζήτηση

Ας ανακεφαλαιώσουμε τους κανόνες του προβλήματος έκφρασης:

  • Κανόνας 1: Επιτρέψτε την εφαρμογή του υπάρχουσες συμπεριφορές  να εφαρμοστεί σε νέες τάξεις
  • Κανόνας 2:  Να επιτρέπεται η εφαρμογή του νέες συμπεριφορές να εφαρμοστεί σε υπάρχουσες τάξεις
  • Κανόνας 3: Δεν πρέπει να θέτει σε κίνδυνο την ασφάλεια τύπου
  • Κανόνας 4: Δεν πρέπει να απαιτείται εκ νέου μεταγλώττιση υπάρχον κώδικα

Ο κανόνας 1 μπορεί να λυθεί εκτός πλαισίου με τον υποτύπο πολυμορφισμό.

Το μοτίβο TC που μόλις παρουσιάστηκε (δείτε το προηγούμενο στιγμιότυπο οθόνης) επιλύει τον κανόνα 2. Είναι τύπου safe (κανόνας 3) και δεν αγγίξαμε ποτέ `Lib1(κανόνας 4). 

Ωστόσο, δεν είναι πρακτικό να το χρησιμοποιήσετε για διάφορους λόγους:

  • Γραμμές 33-34 πρέπει να περάσουμε ρητά τη συμπεριφορά κατά μήκος της παρουσίας της. Αυτό είναι ένα επιπλέον κόστος. Θα πρέπει απλώς να γράψουμε `useLastBlock(Bitcoin())`.
  • Γραμμή 31 η σύνταξη είναι ασυνήθιστη. Θα προτιμούσαμε να γράψουμε ένα συνοπτικό και πιο αντικειμενοστρεφές  `instance.lastBlock()` δήλωση.

Ας επισημάνουμε ορισμένες δυνατότητες του Scala για πρακτική χρήση TC. 

Βελτιωμένη εμπειρία προγραμματιστή

Το Scala έχει ένα μοναδικό σύνολο χαρακτηριστικών και συντακτικών γλυκών που κάνουν το TC μια πραγματικά απολαυστική εμπειρία για προγραμματιστές.

Υπονοούμενα

Το σιωπηρό εύρος είναι ένα ειδικό εύρος που επιλύεται κατά το χρόνο μεταγλώττισης όπου μπορεί να υπάρχει μόνο μία παρουσία ενός δεδομένου τύπου. 

Ένα πρόγραμμα τοποθετεί ένα παράδειγμα στο σιωπηρό πεδίο με το «given` λέξη-κλειδί. Εναλλακτικά, ένα πρόγραμμα μπορεί να ανακτήσει ένα στιγμιότυπο από το σιωπηρό πεδίο με τη λέξη-κλειδί `using`.

Το σιωπηρό εύρος επιλύεται κατά το χρόνο μεταγλώττισης, υπάρχει γνωστός τρόπος να το αλλάξετε δυναμικά κατά το χρόνο εκτέλεσης. Εάν το πρόγραμμα μεταγλωττιστεί, επιλύεται το έμμεσο εύρος. Κατά το χρόνο εκτέλεσης, δεν είναι δυνατό να λείπουν έμμεσα στιγμιότυπα όπου χρησιμοποιούνται. Η μόνη πιθανή σύγχυση μπορεί να προέρχεται από τη χρήση της λανθασμένης σιωπηρής παρουσίας, αλλά αυτό το ζήτημα αφήνεται για το πλάσμα μεταξύ της καρέκλας και του πληκτρολογίου.

Διαφέρει από μια παγκόσμια εμβέλεια επειδή: 

  1. Επιλύεται με βάση τα συμφραζόμενα. Δύο τοποθεσίες ενός προγράμματος μπορούν να χρησιμοποιούν μια παρουσία του ίδιου δεδομένου τύπου σε έμμεση εμβέλεια, αλλά αυτές οι δύο περιπτώσεις μπορεί να είναι διαφορετικές.
  2. Πίσω από τη σκηνή ο κώδικας μεταδίδει τη συνάρτηση σιωπηρών ορισμάτων για να λειτουργήσει μέχρι να επιτευχθεί η σιωπηρή χρήση. Δεν χρησιμοποιεί έναν παγκόσμιο χώρο μνήμης.

Επιστρέφοντας στην κατηγορία τύπου! Ας πάρουμε ακριβώς το ίδιο παράδειγμα.

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` είναι ο ίδιος μη τροποποιημένος κώδικας που ορίσαμε προηγουμένως. 

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

Γραμμή 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.

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

Γραμμές 22 και 25, εάν το όνομα του στιγμιότυπου δεν χρησιμοποιείται, μπορεί να παραλειφθεί.

Scala


  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λέξη-κλειδί.

Scala

given LastBlock[Ethereum] = _.lastBlock

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

Επειδή χρησιμοποιούμε ένα εκφυλισμένο χαρακτηριστικό με μία μόνο συνάρτηση σε αυτό, το IDE μπορεί να προτείνει την απλοποίηση του κώδικα με μια έκφραση SAM. Αν και είναι σωστό, δεν νομίζω ότι είναι σωστή χρήση του SAM, εκτός και αν κάνετε γκολφ με κώδικα.

Η Scala προσφέρει συντακτικά σάκχαρα για τον εξορθολογισμό της σύνταξης, αφαιρώντας την περιττή ονομασία, δήλωση και πλεονασμό τύπων.

Επέκταση

Χρησιμοποιείται σοφά, το `extensionΟ μηχανισμός μπορεί να απλοποιήσει τη σύνταξη για τη χρήση μιας κλάσης τύπου.

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)

Γραμμές 28-29 μια γενική μέθοδος επέκτασης `lastBlockΤο ` ορίζεται για οποιοδήποτε`A`με ένα`LastBlock` Παράμετρος TC σε σιωπηρό εύρος.

Οι γραμμές 33-34 η επέκταση αξιοποιεί μια αντικειμενοστραφή σύνταξη για χρήση 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)

Γραμμή 28, η παράμετρος TC μπορεί επίσης να οριστεί για ολόκληρη την επέκταση για να αποφευχθεί η επανάληψη. Γραμμή 30 επαναχρησιμοποιούμε το TC στην επέκταση για να ορίσουμε το `penultimateBlock` (παρόλο που θα μπορούσε να εφαρμοστεί στις `LastBlock` χαρακτηριστικό απευθείας)

Η μαγεία συμβαίνει όταν χρησιμοποιείται το TC. Η έκφραση είναι πολύ πιο φυσική, δίνοντας την ψευδαίσθηση ότι η συμπεριφορά ».lastBlock` συγχέεται με το παράδειγμα.

Γενικός τύπος με 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))

Γραμμή 34 η συνάρτηση χρησιμοποιεί ένα σιωπηρό TC. Σημειώστε ότι το TC δεν χρειάζεται να ονομαστεί εάν αυτό το όνομα δεν είναι απαραίτητο.

Το μοτίβο TC χρησιμοποιείται τόσο ευρέως που υπάρχει μια γενικού τύπου σύνταξη για να εκφράσει «έναν τύπο με σιωπηρή συμπεριφορά». Η γραμμή 36 η σύνταξη είναι μια πιο συνοπτική εναλλακτική από την προηγούμενη (γραμμή 34). Αποφεύγει να δηλώνει συγκεκριμένα την ανώνυμη σιωπηρή παράμετρο TC.

Αυτό ολοκληρώνει την ενότητα εμπειρία προγραμματιστή. Είδαμε πώς οι επεκτάσεις, τα σιωπηρά και κάποια συντακτική ζάχαρη μπορούν να παρέχουν μια λιγότερο ακατάστατη σύνταξη όταν χρησιμοποιείται και ορίζεται το TC.

Αυτόματη παραγωγή

Πολλές βιβλιοθήκες Scala χρησιμοποιούν TC, αφήνοντας τον προγραμματιστή να τις εφαρμόσει στη βάση κώδικα τους.

Για παράδειγμα, το Circe (μια βιβλιοθήκη αποσειριοποίησης json) χρησιμοποιεί το TC `Encoder[T]`και`Decoder[T]` για προγραμματιστές να εφαρμόσουν στη βάση κώδικα τους. Μόλις εφαρμοστεί, μπορεί να χρησιμοποιηθεί ολόκληρο το εύρος της βιβλιοθήκης. 

Αυτές οι υλοποιήσεις του TC είναι περισσότερες από συχνά χαρτογράφους προσανατολισμένους στα δεδομένα. Δεν χρειάζονται καμία επιχειρηματική λογική, είναι βαρετό να γράφουν και ένα βάρος για να διατηρούνται σε συγχρονισμό με τις κατηγορίες περιπτώσεων.

Σε μια τέτοια κατάσταση, αυτές οι βιβλιοθήκες προσφέρουν αυτό που ονομάζεται αυτόματο παράγωγο ή ημιαυτόματος παραγωγή. Δείτε για παράδειγμα την Κίρκη αυτόματο και ημιαυτόματος παραγωγή. Με την ημιαυτόματη παραγωγή, ο προγραμματιστής μπορεί να δηλώσει μια παρουσία μιας κλάσης τύπου με κάποια δευτερεύουσα σύνταξη, ενώ η αυτόματη παραγωγή δεν απαιτεί καμία τροποποίηση κώδικα εκτός από μια εισαγωγή.

Κάτω από την κουκούλα, την ώρα της μεταγλώττισης, γενική ενδοσκόπηση μακροεντολών τύποι ως καθαρή δομή δεδομένων και δημιουργούν ένα TC[T] για τους χρήστες της βιβλιοθήκης. 

Η γενική εξαγωγή ενός TC είναι πολύ συνηθισμένη, επομένως η Scala παρουσίασε μια πλήρη εργαλειοθήκη για αυτόν τον σκοπό. Αυτή η μέθοδος δεν διαφημίζεται πάντα από τεκμηρίωση βιβλιοθήκης, αν και είναι ο τρόπος χρήσης της προέλευσης της Scala 3.

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)

Γραμμή 18 ένα νέο TC `Named` εισάγεται. Αυτό το TC δεν σχετίζεται με την επιχείρηση blockchain, αυστηρά. Ο σκοπός του είναι να ονομάσει το blockchain με βάση το όνομα της κατηγορίας περίπτωσης.

Πρώτα εστίαση στις γραμμές ορισμών 36-38. Υπάρχουν 2 συντάξεις για την παραγωγή ενός TC:

  1. Γραμμή 36 το στιγμιότυπο TC μπορεί να οριστεί απευθείας στην κατηγορία case με το `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 μπορούν να χρησιμοποιήσουν μετα-προγραμματισμό για να παρέχουν συναρτήσεις που δημιουργούν γενικά παρουσίες TC δεδομένου ενός τύπου. Οι προγραμματιστές μπορούν να χρησιμοποιήσουν το αποκλειστικό API βιβλιοθήκης ή τα εργαλεία εξαγωγής Scala για να δημιουργήσουν στιγμιότυπα για τον κώδικά τους.

Είτε χρειάζεστε γενικό είτε συγκεκριμένο κώδικα για την υλοποίηση ενός TC, υπάρχει λύση για κάθε περίπτωση. 

Περίληψη όλων των πλεονεκτημάτων

  • Λύνει το πρόβλημα έκφρασης
    • Οι νέοι τύποι μπορούν να εφαρμόσουν την υπάρχουσα συμπεριφορά μέσω της παραδοσιακής κληρονομικότητας χαρακτηριστικών
    • Μπορούν να εφαρμοστούν νέες συμπεριφορές σε υπάρχοντες τύπους
  • Διαχωρισμός ανησυχίας
    • Ο κώδικας δεν παραμορφώνεται και διαγράφεται εύκολα. Ένα TC διαχωρίζει δεδομένα και συμπεριφορά, το οποίο είναι ένα σύνθημα λειτουργικού προγραμματισμού.
  • Είναι ασφαλές
    • Είναι ασφαλές γιατί δεν βασίζεται στην ενδοσκόπηση. Αποφεύγει την αντιστοίχιση μεγάλων μοτίβων που αφορούν τύπους. Εάν συναντήσετε τον εαυτό σας να γράφει τέτοιο κώδικα, μπορεί να εντοπίσετε μια περίπτωση όπου το μοτίβο TC θα ταιριάζει απόλυτα.
    • Ο σιωπηρός μηχανισμός είναι compile safe! Εάν λείπει ένα στιγμιότυπο κατά τη στιγμή της μεταγλώττισης, ο κώδικας δεν θα μεταγλωττιστεί. Καμία έκπληξη στο χρόνο εκτέλεσης.
  • Φέρνει ad-hoc πολυμορφισμό
    • Ο ad hoc πολυμορφισμός συνήθως λείπει στον παραδοσιακό αντικειμενοστραφή προγραμματισμό.
    • Με τον ad-hoc πολυμορφισμό, οι προγραμματιστές μπορούν να εφαρμόσουν την ίδια συμπεριφορά για διάφορους άσχετους τύπους χωρίς τη χρήση παραδοσιακής δευτερεύουσας πληκτρολόγησης (η οποία συνδυάζει τον κώδικα)
  • Η έγχυση εξάρτησης έγινε εύκολη
    • Μια περίπτωση TC μπορεί να αλλάξει σε σχέση με την αρχή αντικατάστασης Liskov. 
    • Όταν ένα εξάρτημα έχει μια εξάρτηση από ένα TC, μπορεί εύκολα να εγχυθεί ένας κοροϊδευόμενος TC για σκοπούς δοκιμής. 

Ενδείξεις μετρητών

Κάθε σφυρί έχει σχεδιαστεί για μια σειρά προβλημάτων.

Οι κατηγορίες τύπων προορίζονται για προβλήματα συμπεριφοράς και δεν πρέπει να χρησιμοποιούνται για κληρονομικότητα δεδομένων. Χρησιμοποιήστε σύνθεση για αυτό το σκοπό.

Η συνήθης υποτυπογραφία είναι πιο απλή. Εάν διαθέτετε τη βάση κώδικα και δεν στοχεύετε σε επεκτασιμότητα, οι κατηγορίες τύπων μπορεί να είναι υπερβολικές.

Για παράδειγμα, στον πυρήνα της Scala, υπάρχει ένα `Numeric` κατηγορία τύπου:

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

Είναι πραγματικά λογικό να χρησιμοποιείται μια τέτοια κατηγορία τύπων επειδή όχι μόνο επιτρέπει την επαναχρησιμοποίηση αλγεβρικών αλγορίθμων σε τύπους που είναι ενσωματωμένοι στο Scala (Int, BigInt,…), αλλά και σε τύπους που ορίζονται από το χρήστη (a `ComplexNumber«για παράδειγμα).

Από την άλλη πλευρά, η υλοποίηση των συλλογών Scala χρησιμοποιεί ως επί το πλείστον υποτυποποίηση αντί για κατηγορία τύπου. Αυτός ο σχεδιασμός έχει νόημα για διάφορους λόγους:

  • Το API συλλογής υποτίθεται ότι είναι πλήρες και σταθερό. Εκθέτει την κοινή συμπεριφορά μέσω χαρακτηριστικών που κληρονομούνται από υλοποιήσεις. Το να είναι κανείς εξαιρετικά επεκτάσιμο δεν είναι ένας ιδιαίτερος στόχος εδώ.
  • Πρέπει να είναι απλό στη χρήση. Το TC προσθέτει μια νοητική επιβάρυνση στον προγραμματιστή τελικού χρήστη.
  • Το TC μπορεί επίσης να έχει μικρό γενικό κόστος στην απόδοση. Αυτό μπορεί να είναι κρίσιμο για ένα API συλλογής.
  • Ωστόσο, το API συλλογής εξακολουθεί να είναι επεκτάσιμο μέσω νέων TC που ορίζονται από βιβλιοθήκες τρίτων.

Συμπέρασμα

Είδαμε ότι το TC είναι ένα απλό μοτίβο που λύνει ένα μεγάλο πρόβλημα. Χάρη στην πλούσια σύνταξη Scala, το μοτίβο TC μπορεί να εφαρμοστεί και να χρησιμοποιηθεί με πολλούς τρόπους. Το μοτίβο TC είναι σύμφωνο με το πρότυπο λειτουργικού προγραμματισμού και είναι ένα υπέροχο εργαλείο για μια καθαρή αρχιτεκτονική. Δεν υπάρχει ασημί κουκκίδα και το σχέδιο TC πρέπει να εφαρμόζεται όταν ταιριάζει.

Ελπίζω να αποκτήσατε γνώσεις διαβάζοντας αυτό το έγγραφο. 

Ο κωδικός είναι διαθέσιμος στο https://github.com/jprudent/type-class-article. Επικοινωνήστε μαζί μου αν έχετε οποιεσδήποτε ερωτήσεις ή παρατηρήσεις. Μπορείτε να χρησιμοποιήσετε προβλήματα ή σχόλια κώδικα στο αποθετήριο, αν θέλετε.


Ιερώνυμος ΣΥΝΕΤΟΣ

Μηχανικός Λογισμικού

Σφραγίδα ώρας:

Περισσότερα από Καθολικό