Classi di tipo in Scala3: una guida per principianti | Registro

Classi di tipo in Scala3: una guida per principianti | Registro

Classi di tipo in Scala3: una guida per principianti | Ledger PlatoBlockchain Data Intelligence. Ricerca verticale. Ai.

Questo documento è destinato allo sviluppatore Scala3 principiante che è già esperto nella prosa di Scala, ma è perplesso su tutti i `implicits` e tratti parametrizzati nel codice.

Questo documento spiega il perché, come, dove e quando Classi di tipo (TC).

Dopo aver letto questo documento, lo sviluppatore principiante di Scala3 acquisirà solide conoscenze da utilizzare e si immergerà nel codice sorgente Un sacco delle librerie Scala e iniziare a scrivere codice Scala idiomatico.

Partiamo dal perché…

Il problema dell'espressione

Nel 1998, Ha dichiarato Philip Wadler che “il problema dell’espressione è un nuovo nome per un vecchio problema”. È il problema dell’estensibilità del software. Secondo la scrittura del signor Wadler, la soluzione del problema dell'espressione deve rispettare le seguenti regole:

  • Regola 1: Consentire l'implementazione di comportamenti esistenti (si pensi al tratto Scala) a cui applicare nuove rappresentazioni (pensa a una classe di casi)
  • Regola 2: consentire l'implementazione di nuovi comportamenti da applicare a rappresentazioni esistenti
  • Regola 3: Non deve mettere a repentaglio la tipo di sicurezza
  • Regola 4: Non deve essere necessaria la ricompilazione codice esistente

Risolvere questo problema sarà il filo conduttore di questo articolo.

Regola 1: implementazione dei comportamenti esistenti sulla nuova rappresentanza

Qualsiasi linguaggio orientato agli oggetti ha una soluzione integrata per la regola 1 con polimorfismo del sottotipo. Puoi implementare in sicurezza qualsiasi file `trait` definito in una dipendenza da un `class` nel tuo codice, senza ricompilare la dipendenza. Vediamolo in azione:

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

In questo esempio fittizio, la libreria `Lib1` (riga 5) definisce un tratto `Blockchain` (riga 6) con 2 implementazioni (righe 9 e 12). `Lib1` rimarrà lo stesso in TUTTO questo documento (applicazione della regola 4).

`Lib2` (riga 15) implementa il comportamento esistente `Blockchain"in una nuova classe".Polkadot` (regola 1) in modo sicuro (regola 3), senza ricompilare `Lib1"(regola 4). 

Regola 2: implementazione di nuovi comportamenti da applicare alle rappresentazioni esistenti

Immaginiamo in `Lib2`Vogliamo un nuovo comportamento`lastBlock"da attuare specificatamente per ciascuno di essi".Blockchain`.

La prima cosa che mi viene in mente è creare un grande interruttore in base al tipo di parametro.

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

Questa soluzione è una debole reimplementazione del polimorfismo basato sul tipo che è già integrato nel linguaggio!

`Lib1` rimane intatto (ricordate, vige la regola 4 in tutto questo documento). 

La soluzione implementata in `Lib2"è". okay fino a quando non verrà introdotta un'altra blockchain in `Lib3`. Viola la regola di sicurezza del tipo (regola 3) perché questo codice fallisce in fase di esecuzione alla riga 37. E modificando `Lib2" violerebbe la regola 4.

Un'altra soluzione è utilizzare un file `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` resta intatta (applicazione della regola 4 in tutto il documento). 

`Lib2` definisce il comportamento per il suo tipo (riga 21) e le `estensioni per i tipi esistenti (righe 23 e 25).

Righe 28-30, il nuovo comportamento può essere utilizzato in ogni classe. 

Ma non c'è modo di chiamare questo nuovo comportamento in modo polimorfico (riga 32). Qualsiasi tentativo di farlo porta a errori di compilazione (riga 33) o a cambiamenti basati sul tipo. 

Questa regola n°2 è complicata. Abbiamo provato a implementarlo con la nostra definizione di polimorfismo e il trucco dell'"estensione". E questo era strano.

C'è un pezzo mancante chiamato polimorfismo ad hoc: la capacità di inviare in modo sicuro un'implementazione del comportamento in base a un tipo, ovunque siano definiti il ​​comportamento e il tipo. Inserisci il Tipo Classe pattern.

Il modello Type Class

La ricetta del pattern Type Class (TC in breve) prevede 3 passaggi. 

  1. Definire un nuovo comportamento
  2. Implementare il comportamento
  3. Usa il comportamento

Nella sezione seguente implementerò il modello TC nel modo più semplice. È prolisso, goffo e poco pratico. Ma aspetta un attimo, questi avvertimenti verranno fissati passo dopo passo nel documento.

1. Definire un nuovo comportamento
Scala

object Lib2:
 import Lib1.*

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

`Lib1` è, ancora una volta, lasciato intatto.

Il nuovo comportamento is il TC materializzato dal tratto. Le funzioni definite nel tratto sono un modo per applicare alcuni aspetti di quel comportamento.

Il parametro `A` rappresenta il tipo a cui vogliamo applicare il comportamento, che sono sottotipi di `Blockchain`nel nostro caso.

Alcune osservazioni:

  • Se necessario, il tipo parametrizzato `A` può essere ulteriormente vincolato dal sistema di tipo Scala. Ad esempio, potremmo imporre `A"essere un"Blockchain`. 
  • Inoltre, il TC potrebbe avere molte più funzioni dichiarate al suo interno.
  • Infine, ciascuna funzione può avere molti più parametri arbitrari.

Ma manteniamo le cose semplici per motivi di leggibilità.

2. Implementare il comportamento
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")

Per ogni tipo il nuovo `LastBlock`è previsto un comportamento, esiste un esempio specifico di quel comportamento. 

Il `Ethereum`la riga di implementazione 22 viene calcolata dal file `eth` istanza passata come parametro. 

L'implementazione di `LastBlock"per"Bitcoin` la riga 25 è implementata con un IO non gestito e non utilizza il relativo parametro.

Quindi, `Lib2` implementa un nuovo comportamento `LastBlock"per"Lib1`classi.

3. Usa il comportamento
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))

Linea 30`useLastBlock` utilizza un'istanza di `A"e il"LastBlock` comportamento definito per quell'istanza.

Linea 33`useLastBlock` viene chiamato con un'istanza di `Ethereum" e un'implementazione di "LastBlock"definito in "Lib2`. Tieni presente che è possibile passare qualsiasi implementazione alternativa di `LastBlock[A]"(pensa a iniezione di dipendenza).

`useLastBlock` è il collante tra la rappresentazione (l'attuale A) e il suo comportamento. Dati e comportamento sono separati, che è ciò che sostiene la programmazione funzionale.

Discussione

Ricapitoliamo le regole del problema dell’espressione:

  • Regola 1: Consentire l'implementazione di comportamenti esistenti  da applicare a nuove classi
  • Regola 2: consentire l'implementazione di nuovi comportamenti da applicare a classi esistenti
  • Regola 3: Non deve mettere a repentaglio la tipo di sicurezza
  • Regola 4: Non deve essere necessaria la ricompilazione codice esistente

La regola 1 può essere risolta immediatamente con il polimorfismo del sottotipo.

Il modello TC appena presentato (vedi screenshot precedente) risolve la regola 2. È sicuro (regola 3) e non abbiamo mai toccato `Lib1"(regola 4). 

Tuttavia non è pratico da utilizzare per diversi motivi:

  • Alle righe 33-34 dobbiamo passare esplicitamente il comportamento lungo la sua istanza. Questo è un sovraccarico aggiuntivo. Dovremmo semplicemente scrivere `useLastBlock(Bitcoin())`.
  • Alla riga 31 la sintassi non è comune. Preferiamo piuttosto scrivere un testo conciso e più orientato agli oggettiinstance.lastBlock()"dichiarazione.

Evidenziamo alcune funzionalità di Scala per l'utilizzo pratico di TC. 

Esperienza degli sviluppatori migliorata

Scala ha un insieme unico di funzionalità e zuccheri sintattici che rendono TC un'esperienza davvero piacevole per gli sviluppatori.

Impliciti

L'ambito implicito è un ambito speciale risolto in fase di compilazione in cui può esistere solo un'istanza di un determinato tipo. 

Un programma inserisce un'istanza nell'ambito implicito con l'estensione `given"parola chiave. In alternativa un programma può recuperare un'istanza dall'ambito implicito con la parola chiave `using`.

L'ambito implicito viene risolto in fase di compilazione, esiste un modo noto per modificarlo dinamicamente in fase di esecuzione. Se il programma viene compilato, l'ambito implicito viene risolto. In fase di esecuzione, non è possibile che manchino istanze implicite in cui vengono utilizzate. L'unica confusione possibile potrebbe derivare dall'uso dell'istanza implicita sbagliata, ma questo problema è lasciato alla creatura tra la sedia e la tastiera.

È diverso da un ambito globale perché: 

  1. Si risolve contestualmente. Due posizioni di un programma possono utilizzare un'istanza dello stesso tipo specificato nell'ambito implicito, ma queste due istanze potrebbero essere diverse.
  2. Dietro le quinte il codice passa gli argomenti impliciti da funzione a funzione finché non viene raggiunto l'utilizzo implicito. Non utilizza uno spazio di memoria globale.

Tornando alla lezione di tipo! Prendiamo lo stesso identico esempio.

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` è lo stesso codice non modificato che abbiamo definito in precedenza. 

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

Linea 19 un nuovo comportamento `LastBlock` è definito, esattamente come abbiamo fatto in precedenza.

Linea 22 e linea 25, `val" è sostituito da "given`. Entrambe le implementazioni di `LastBlock` vengono inseriti nell'ambito implicito.

Linea 31`useLastBlock` dichiara il comportamento `LastBlock` come parametro implicito. Il compilatore risolve l'istanza appropriata di `LastBlock` dall'ambito implicito contestualizzato dalle posizioni del chiamante (righe 33 e 34). La riga 28 importa tutto da `Lib2", compreso l'ambito implicito. Pertanto, il compilatore passa le istanze definite alle righe 22 e 25 come ultimo parametro di `useLastBlock`. 

Come utente della libreria, utilizzare una classe di tipo è più semplice di prima. Righe 34 e 35 uno sviluppatore deve solo assicurarsi che un'istanza del comportamento sia inserita nell'ambito implicito (e questo può essere un semplice `import`). Se un implicito non è `given` dove si trova il codice `using`, gli dice il compilatore.

L’implicito Scala facilita il compito di passare istanze di classe insieme ad istanze dei loro comportamenti.

Zuccheri impliciti

Le righe 22 e 25 del codice precedente possono essere ulteriormente migliorate! Iteriamo sulle implementazioni 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")

Righe 22 e 25, se il nome dell'istanza non è utilizzato, può essere omesso.

Scala


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

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

Righe 22 e 25, la ripetizione del tipo può essere sostituita con `with"parola chiave.

Scala

given LastBlock[Ethereum] = _.lastBlock

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

Poiché utilizziamo un tratto degenerato con una singola funzione al suo interno, l'IDE potrebbe suggerire di semplificare il codice con un'espressione SAM. Anche se corretto, non penso che sia un uso corretto di SAM, a meno che tu non stia codificando casualmente il golf.

Scala offre strumenti sintattici per semplificare la sintassi, rimuovendo nomi, dichiarazioni e ridondanza di tipi non necessari.

Estensione

Usato saggiamente, il `extension`il meccanismo può semplificare la sintassi per l'utilizzo di una classe di tipo.

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)

Le righe 28-29 un metodo di estensione generico `lastBlock` è definito per qualsiasi `A"con un"LastBlock` Parametro TC nell'ambito implicito.

Righe 33-34 l'estensione sfrutta una sintassi orientata agli oggetti per utilizzare 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)

Riga 28, il parametro TC può essere definito anche per l'intera estensione per evitare ripetizioni. Alla riga 30 riutilizziamo il TC nell'estensione per definire `penultimateBlock` (anche se potrebbe essere implementato su `LastBlock` tratto direttamente)

La magia accade quando viene utilizzato il TC. L'espressione sembra molto più naturale, dando l'illusione di quel comportamentolastBlock` viene confuso con l'istanza.

Tipo generico con 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))

Riga 34 la funzione utilizza un TC implicito. Tieni presente che non è necessario nominare il TC se tale nome non è necessario.

Il modello TC è così ampiamente utilizzato che esiste una sintassi di tipo generica per esprimere “un tipo con un comportamento implicito”. Riga 36 la sintassi è un'alternativa più concisa alla precedente (riga 34). Evita di dichiarare specificamente il parametro TC implicito senza nome.

Questo conclude la sezione sull'esperienza dello sviluppatore. Abbiamo visto come estensioni, impliciti e un po' di zucchero sintattico possano fornire una sintassi meno ingombrante quando il TC viene utilizzato e definito.

Derivazione automatica

Molte librerie Scala utilizzano TC, lasciando al programmatore il compito di implementarle nel proprio codice base.

Ad esempio Circe (una libreria di deserializzazione json) utilizza TC `Encoder[T]` e `Decoder[T]` che i programmatori possono implementare nella loro base di codice. Una volta implementato, è possibile utilizzare l'intero ambito della libreria. 

Tali implementazioni di TC sono più che frequenti mappatori orientati ai dati. Non necessitano di alcuna logica aziendale, sono noiosi da scrivere e un onere da mantenere sincronizzati con le classi di casi.

In una situazione del genere, quelle biblioteche offrono ciò che viene chiamato automaticamente in Sistemi derivazione o semiautomatico derivazione. Vedi ad esempio Circe automaticamente in Sistemi ed semiautomatico derivazione. Con la derivazione semiautomatica il programmatore può dichiarare un'istanza di una classe di tipo con una sintassi minore, mentre la derivazione automatica non richiede alcuna modifica del codice tranne un'importazione.

Sotto il cofano, in fase di compilazione, le macro generiche eseguono l'introspezione Tipi di come pura struttura dati e generare un TC[T] per gli utenti della biblioteca. 

Derivare genericamente un TC è molto comune, quindi Scala ha introdotto una cassetta degli attrezzi completa a tale scopo. Questo metodo non è sempre pubblicizzato dalla documentazione delle biblioteche, sebbene sia il modo in cui Scala 3 utilizza la derivazione.

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)

Linea 18 un nuovo TC `Namedviene introdotto. Questo TC non è correlato al business blockchain in senso stretto. Il suo scopo è nominare la blockchain in base al nome della classe case.

Innanzitutto concentrati sulle righe di definizione 36-38. Esistono 2 sintassi per derivare un TC:

  1. Alla riga 36 l'istanza TC può essere definita direttamente nella classe case con il comando `derives"parola chiave. Dietro le quinte il compilatore genera un dato file `Named` istanza in `Polkadot` oggetto associato.
  2. Righe 37 e 38, le istanze delle classi di tipo sono fornite su classi preesistenti con `TC.derived

Alla riga 31 viene definita un'estensione generica (vedi paragrafi precedenti) e `blockchainName` è usato naturalmente.  

Il `derivesLa parola chiave ` prevede un metodo con la forma `inline def derived[T](using Mirror.Of[T]): TC[T] = ???` che è definita dalla riga 24. Non spiegherò in dettaglio cosa fa il codice. A grandi linee:

  • `inline def` definisce una macro
  • `Mirror`fa parte della cassetta degli attrezzi per analizzare i tipi. Esistono diversi tipi di mirror e la riga 26 del codice si concentra su `Product` mirror (una classe case è un prodotto). La riga 27, se i programmatori tentano di ricavare qualcosa che non sia un file `Product`, il codice non verrà compilato.
  • il `Mirror` contiene altri tipi. Uno di loro, `MirrorLabel`, è una stringa che contiene il nome del tipo. Questo valore viene utilizzato nell'implementazione, riga 29, del `Named"TC.

Gli autori di TC possono utilizzare la metaprogrammazione per fornire funzioni che generano genericamente istanze di TC dato un tipo. I programmatori possono utilizzare l'API della libreria dedicata o gli strumenti di derivazione Scala per creare istanze per il proprio codice.

Che tu abbia bisogno di codice generico o specifico per implementare un TC, esiste una soluzione per ogni situazione. 

Riepilogo di tutti i vantaggi

  • Risolve il problema dell'espressione
    • I nuovi tipi possono implementare il comportamento esistente attraverso l'ereditarietà dei tratti tradizionali
    • È possibile implementare nuovi comportamenti sui tipi esistenti
  • Separazione delle preoccupazioni
    • Il codice non è alterato ed è facilmente cancellabile. Un TC separa dati e comportamento, che è un motto di programmazione funzionale.
  • É sicuro
    • È sicuro perché non si basa sull’introspezione. Evita la corrispondenza di modelli di grandi dimensioni che coinvolgono i tipi. se ti trovi a scrivere tale codice, potresti rilevare un caso in cui il modello TC si adatterà perfettamente.
    • Il meccanismo implicito è compilabile in modo sicuro! Se manca un'istanza in fase di compilazione, il codice non verrà compilato. Nessuna sorpresa in fase di esecuzione.
  • Porta polimorfismo ad hoc
    • Il polimorfismo ad hoc di solito manca nella tradizionale programmazione orientata agli oggetti.
    • Con il polimorfismo ad hoc, gli sviluppatori possono implementare lo stesso comportamento per vari tipi non correlati senza utilizzare la sottotipizzazione tradizionale (che accoppia il codice)
  • L'inserimento delle dipendenze è reso semplice
    • Un'istanza TC può essere modificata rispettando il principio di sostituzione di Liskov. 
    • Quando un componente ha una dipendenza da un TC, un TC simulato può essere facilmente iniettato a scopo di test. 

Controindicazioni

Ogni martello è progettato per una serie di problemi.

Le classi di tipo servono per problemi comportamentali e non devono essere utilizzate per l'ereditarietà dei dati. Usa la composizione per quello scopo.

La consueta sottotipizzazione è più semplice. Se possiedi la base di codice e non miri all'estensibilità, le classi di tipo potrebbero essere eccessive.

Ad esempio, nel core Scala è presente un file `Numeric` digitare classe:

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

Ha davvero senso utilizzare una classe di tipo di questo tipo perché non solo consente il riutilizzo di algoritmi algebrici su tipi incorporati in Scala (Int, BigInt, ...), ma anche su tipi definiti dall'utente (a `ComplexNumber`ad esempio).

D'altra parte, l'implementazione delle raccolte Scala utilizza principalmente il sottotipo invece della classe di tipo. Questo design ha senso per diversi motivi:

  • L'API di raccolta dovrebbe essere completa e stabile. Espone il comportamento comune attraverso i tratti ereditati dalle implementazioni. Essere altamente estensibile non è un obiettivo particolare qui.
  • Deve essere semplice da usare. TC aggiunge un sovraccarico mentale al programmatore dell'utente finale.
  • TC potrebbe anche comportare un leggero sovraccarico in termini di prestazioni. Questo potrebbe essere fondamentale per un'API di raccolta.
  • Tuttavia, l'API di raccolta è ancora estensibile tramite il nuovo TC definito da librerie di terze parti.

Conclusione

Abbiamo visto che TC è un modello semplice che risolve un grosso problema. Grazie alla ricca sintassi di Scala, il modello TC può essere implementato e utilizzato in molti modi. Il modello TC è in linea con il paradigma di programmazione funzionale ed è uno strumento favoloso per un'architettura pulita. Non esiste una soluzione miracolosa e il modello TC deve essere applicato quando si adatta.

Spero che tu abbia acquisito conoscenza leggendo questo documento. 

Il codice è disponibile su https://github.com/jprudent/type-class-article. Per favore contattami se hai qualsiasi tipo di domanda o commento. Se lo desideri, puoi utilizzare problemi o commenti di codice nel repository.


Girolamo PRUDENTE

Software Engineer

Timestamp:

Di più da Ledger