Klasy typów w Scala3: Przewodnik dla początkujących | Księga główna

Klasy typów w Scala3: Przewodnik dla początkujących | Księga główna

Klasy typów w Scala3: Przewodnik dla początkujących | Inteligencja danych Ledger PlatoBlockchain. Wyszukiwanie pionowe. AI.

Ten dokument jest przeznaczony dla początkującego programisty Scala3, który jest już zaznajomiony z prozą Scala, ale jest zaintrygowany wszystkimi `implicits` i sparametryzowane cechy w kodzie.

W tym dokumencie wyjaśniono dlaczego, jak, gdzie i kiedy Klasy typów (TC).

Po przeczytaniu tego dokumentu początkujący programista Scala3 zdobędzie solidną wiedzę, którą będzie mógł wykorzystać i zagłębić się w kod źródłowy dużo bibliotek Scala i zacznij pisać idiomatyczny kod Scala.

Zacznijmy od dlaczego…

Problem ekspresji

W 1998, stwierdził Philip Wadler że „problem wyrażeń to nowa nazwa starego problemu”. Jest to problem rozszerzalności oprogramowania. Zdaniem pana Wadlera rozwiązanie problemu wyrazowego musi spełniać następujące zasady:

  • Zasada 1: Pozwól na wdrożenie istniejące zachowania (pomyśl o cesze Scala), do której chcesz zastosować nowe reprezentacje (pomyśl o klasie przypadków)
  • Zasada 2:  Zezwól na wdrożenie nowe zachowania do zastosowania istniejących reprezentacji
  • Zasada 3: Nie może zagrażać bezpieczeństwo typu
  • Zasada 4: Nie może wymagać ponownej kompilacji istniejący kod

Rozwiązanie tego problemu będzie srebrnym wątkiem tego artykułu.

Zasada 1: wdrożenie istniejącego zachowania na nową reprezentację

Każdy język zorientowany obiektowo ma wbudowane rozwiązanie dla reguły 1 polimorfizm podtypu. Możesz bezpiecznie zaimplementować dowolne pliki `trait` zdefiniowane w zależności od `class` we własnym kodzie, bez ponownej kompilacji zależności. Zobaczmy to w akcji:

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

W tym fikcyjnym przykładzie biblioteka `Lib1` (linia 5) definiuje cechę `Blockchain` (linia 6) z 2 jego implementacjami (linie 9 i 12). `Lib1` pozostanie takie samo we CAŁYM dokumencie (egzekwowanie zasady 4).

`Lib2` (linia 15) implementuje istniejące zachowanie `Blockchain`na nowych zajęciach`Polkadot` (reguła 1) w sposób bezpieczny dla typu (reguła 3), bez ponownej kompilacji `Lib1` (zasada 4). 

Zasada 2: wdrażanie nowych zachowań do zastosowania w istniejących reprezentacjach

Wyobraźmy sobie w `Lib2` chcemy nowego zachowania `lastBlock` do wdrożenia specjalnie dla każdego `Blockchain`.

Pierwszą rzeczą, która przychodzi na myśl, jest utworzenie dużego przełącznika w oparciu o typ parametru.

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

To rozwiązanie jest słabą reimplementacją polimorfizmu opartego na typach, który jest już wbudowany w język!

`Lib1` pozostaje niezmienione (pamiętaj, zasada 4 jest egzekwowana w całym dokumencie). 

Rozwiązanie zaimplementowane w `Lib2jest w porządku do czasu wprowadzenia kolejnego blockchainu w `Lib3`. Narusza to regułę bezpieczeństwa typu (reguła 3), ponieważ ten kod zawodzi w czasie wykonywania w linii 37. I modyfikacja `Lib2` naruszyłoby zasadę 4.

Innym rozwiązaniem jest użycie rozszerzenia `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` pozostawia się bez zmian (egzekwowanie zasady 4 w całym dokumencie). 

`Lib2` definiuje zachowanie dla swojego typu (linia 21) i `rozszerzenie dla istniejących typów (linie 23 i 25).

W wierszach 28-30 nowe zachowanie można zastosować w każdej klasie. 

Ale nie ma sposobu, aby nazwać to nowe zachowanie polimorficznie (wiersz 32). Każda próba zrobienia tego prowadzi do błędów kompilacji (linia 33) lub do przełączników opartych na typach. 

Ta zasada nr 2 jest trudna. Próbowaliśmy to zaimplementować, stosując naszą własną definicję polimorfizmu i sztuczkę z „rozszerzaniem”. I to było dziwne.

Brakujący element to tzw polimorfizm ad hoc: możliwość bezpiecznego wysyłania implementacji zachowania zgodnie z typem, gdziekolwiek zdefiniowano zachowanie i typ. Wejdz do Wpisz klasę wzór.

Wzorzec klasy typu

Receptura wzorca klasy typu (w skrócie TC) składa się z 3 kroków. 

  1. Zdefiniuj nowe zachowanie
  2. Zaimplementuj zachowanie
  3. Wykorzystaj zachowanie

W poniższej sekcji implementuję wzorzec najlepszego współtwórcy w najprostszy sposób. Jest rozwlekły, nieporadny i niepraktyczny. Ale poczekaj, te zastrzeżenia zostaną poprawione krok po kroku w dalszej części dokumentu.

1. Zdefiniuj nowe zachowanie
Scala

object Lib2:
 import Lib1.*

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

`Lib1`, po raz kolejny pozostało nietknięte.

Nowe zachowanie is TC zmaterializował się poprzez cechę. Funkcje zdefiniowane w cesze są sposobem na zastosowanie niektórych aspektów tego zachowania.

Parametr `A` reprezentuje typ, do którego chcemy zastosować zachowanie, który jest podtypem `Blockchain`w naszym przypadku.

Kilka uwag:

  • W razie potrzeby sparametryzowany typ `A` może być dodatkowo ograniczone przez system typów Scala. Na przykład moglibyśmy wymusić `A`być `Blockchain`. 
  • Ponadto TC może mieć zadeklarowanych znacznie więcej funkcji.
  • Wreszcie każda funkcja może mieć znacznie więcej dowolnych parametrów.

Ale zachowajmy prostotę ze względu na czytelność.

2. Zaimplementuj zachowanie
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")

Dla każdego typu nowy plik `LastBlock` oczekuje się zachowania, istnieje konkretny przypadek takiego zachowania. 

`Ethereum` linia implementacyjna 22 jest obliczana z `eth` instancja przekazana jako parametr. 

Implementacja `LastBlock`dla`Bitcoin` linia 25 jest zaimplementowana z niezarządzanym IO i nie używa jego parametrów.

Więc `Lib2` wdraża nowe zachowanie `LastBlock`dla`Lib1` zajęcia.

3. Wykorzystaj to zachowanie
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))

Linia 30”.useLastBlock` używa instancji `A` i `LastBlock` zachowanie zdefiniowane dla tej instancji.

Linia 33”.useLastBlock` jest wywoływane z instancją `Ethereum` i implementacja `LastBlock`zdefiniowane w `Lib2`. Zauważ, że możliwe jest przekazanie dowolnej alternatywnej implementacji `LastBlock[A]` (pomyśl wstrzykiwanie zależności).

`useLastBlock` jest spoiwem pomiędzy reprezentacją (rzeczywistym A) i jego zachowaniem. Dane i zachowanie są oddzielone, za czym opowiada się programowanie funkcjonalne.

Dyskusja

Przypomnijmy zasady problemu wyrażeń:

  • Zasada 1: Pozwól na wdrożenie istniejące zachowania  do zastosowania nowe zajęcia
  • Zasada 2:  Zezwól na wdrożenie nowe zachowania do zastosowania istniejących klas
  • Zasada 3: Nie może zagrażać bezpieczeństwo typu
  • Zasada 4: Nie może wymagać ponownej kompilacji istniejący kod

Regułę 1 można rozwiązać od razu za pomocą polimorfizmu podtypu.

Właśnie zaprezentowany wzór TC (patrz poprzedni zrzut ekranu) rozwiązuje regułę 2. Jest typu bezpieczny (reguła 3) i nigdy nie dotykaliśmy `Lib1` (zasada 4). 

Jednak korzystanie z niego jest niepraktyczne z kilku powodów:

  • W wierszach 33-34 musimy jawnie przekazać zachowanie wzdłuż jego instancji. Jest to dodatkowe obciążenie. Powinniśmy po prostu napisać `useLastBlock(Bitcoin())`.
  • Linia 31 składnia jest rzadka. Wolelibyśmy napisać zwięzły i bardziej obiektowy ``instance.lastBlock()` oświadczenie.

Podkreślmy niektóre funkcje Scali pod kątem praktycznego wykorzystania TC. 

Ulepszone doświadczenie programisty

Scala posiada unikalny zestaw funkcji i cukrów syntaktycznych, dzięki którym TC jest naprawdę przyjemnym doświadczeniem dla programistów.

Implicyta

Zakres niejawny to specjalny zakres rozpoznawany w czasie kompilacji, w którym może istnieć tylko jedna instancja danego typu. 

Program umieszcza instancję w zakresie niejawnym z rozszerzeniem `given` słowo kluczowe. Alternatywnie program może pobrać instancję z ukrytego zakresu za pomocą słowa kluczowego `using`.

Niejawny zakres jest rozwiązywany w czasie kompilacji, istnieje sposób na dynamiczną zmianę go w czasie wykonywania. Jeśli program się skompiluje, ukryty zakres zostanie rozwiązany. W czasie wykonywania nie jest możliwe, aby brakowało ukrytych instancji, w których są one używane. Jedyne możliwe zamieszanie może wynikać z użycia niewłaściwej ukrytej instancji, ale tę kwestię pozostawia się istocie znajdującej się między krzesłem a klawiaturą.

Różni się od zakresu globalnego, ponieważ: 

  1. Zostało to rozwiązane kontekstowo. Dwie lokalizacje programu mogą używać instancji tego samego typu w zakresie niejawnym, ale te dwie instancje mogą być różne.
  2. Za kulisami kod przekazuje funkcję ukrytych argumentów do działania, aż do osiągnięcia ukrytego użycia. Nie wykorzystuje globalnej przestrzeni pamięci.

Wracamy do klasy typów! Weźmy dokładnie ten sam przykład.

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` to ten sam niezmodyfikowany kod, który zdefiniowaliśmy wcześniej. 

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

Linia 19 nowe zachowanie”.LastBlock` jest zdefiniowane dokładnie tak, jak zrobiliśmy to wcześniej.

Linia 22 i linia 25, `val` zastępuje się `given`. Obie implementacje `LastBlock` są umieszczane w zakresie ukrytym.

Linia 31”.useLastBlock` deklaruje zachowanie `LastBlock` jako ukryty parametr. Kompilator rozpoznaje odpowiednią instancję `LastBlock` z zakresu ukrytego kontekstualizowanego na podstawie lokalizacji osób wywołujących (linie 33 i 34). Linia 28 importuje wszystko z `Lib2`, łącznie z zakresem ukrytym. Zatem kompilator przekazuje zdefiniowane linie 22 i 25 jako ostatni parametr `useLastBlock`. 

Jako użytkownik biblioteki korzystanie z klasy typów jest łatwiejsze niż wcześniej. Linie 34 i 35 programista musi jedynie upewnić się, że instancja zachowania została wstrzyknięta w zakresie niejawnym (może to być zwykłe `import`). Jeśli dorozumiana wartość nie jest `given` gdzie kod to `using` to, mówi mu kompilator.

Ukryte w Scali ułatwienie przekazywania instancji klas wraz z instancjami ich zachowań.

Niejawne cukry

Linie 22 i 25 poprzedniego kodu można jeszcze ulepszyć! Przejdźmy do implementacji najlepszych współtwórców.

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

Linie 22 i 25, jeśli nazwa instancji nie została użyta, można ją pominąć.

Scala


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

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

W wierszach 22 i 25 powtórzenie typu można zastąpić znakiem `with` słowo kluczowe.

Scala

given LastBlock[Ethereum] = _.lastBlock

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

Ponieważ używamy zdegenerowanej cechy z pojedynczą funkcją, IDE może sugerować uproszczenie kodu za pomocą wyrażenia SAM. Chociaż jest to poprawne, nie sądzę, że jest to właściwe użycie SAM, chyba że od niechcenia grasz w golfa w kodzie.

Scala oferuje cukry składniowe, które usprawniają składnię, eliminując niepotrzebne nazewnictwo, deklaracje i nadmiarowość typów.

Rozbudowa

Używane mądrze, `extension` może uprościć składnię używania klasy typu.

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)

Linie 28-29 to ogólna metoda rozszerzenia `lastBlock` jest zdefiniowane dla dowolnego `A`z `LastBlock` Parametr TC w zakresie niejawnym.

W wierszach 33-34 rozszerzenie wykorzystuje składnię obiektową do korzystania z 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)

W linii 28 parametr TC można również zdefiniować dla całego rozszerzenia, aby uniknąć powtórzeń. W wierszu 30 ponownie używamy TC w rozszerzeniu, aby zdefiniować `penultimateBlock` (mimo że można to zaimplementować w `LastBlock` cecha bezpośrednio)

Magia dzieje się, gdy używany jest TC. Wyrażenie wydaje się dużo bardziej naturalne, dając złudzenie, że zachowanie `lastBlock` jest powiązane z instancją.

Typ ogólny z 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))

W linii 34 funkcja używa ukrytego TC. Pamiętaj, że najlepsi współtwórcy nie muszą podawać nazwy, jeśli jest ona niepotrzebna.

Wzorzec TC jest tak powszechnie stosowany, że istnieje ogólna składnia typu pozwalająca wyrazić „typ z ukrytym zachowaniem”. Linia 36 składnia jest bardziej zwięzłą alternatywą dla poprzedniej (linia 34). Pozwala uniknąć deklarowania konkretnego nienazwanego ukrytego parametru TC.

Na tym kończy się część dotycząca doświadczeń programistów. Widzieliśmy, jak rozszerzenia, implicyty i trochę cukru syntaktycznego mogą zapewnić mniej zaśmieconą składnię, gdy używany i definiowany jest TC.

Automatyczne wyprowadzenie

Wiele bibliotek Scala korzysta z TC, pozostawiając programiście implementację ich w swojej bazie kodu.

Na przykład Circe (biblioteka deserializacji json) używa TC `Encoder[T]` i `Decoder[T]` dla programistów do wdrożenia w ich bazie kodu. Po zaimplementowaniu można korzystać z całego zakresu biblioteki. 

Takie wdrożenia najlepszego współtwórcy zdarzają się częściej maperzy zorientowani na dane. Nie potrzebują żadnej logiki biznesowej, są nudne w pisaniu i kłopotliwe w utrzymaniu synchronizacji z klasami przypadków.

W takiej sytuacji biblioteki te oferują tzw automatyczny wyprowadzenie lub półautomatyczny pochodzenie. Zobacz na przykład Circe automatyczny i półautomatyczny pochodzenie. W przypadku derywacji półautomatycznej programista może zadeklarować instancję klasy typu z niewielką składnią, podczas gdy derywacja automatyczna nie wymaga żadnej modyfikacji kodu z wyjątkiem importu.

Pod maską, w czasie kompilacji, introspekcja ogólnych makr typy jako czystą strukturę danych i wygenerować TC[T] dla użytkowników biblioteki. 

Ogólne wyprowadzanie TC jest bardzo powszechne, więc Scala wprowadziła do tego celu kompletny zestaw narzędzi. Ta metoda nie zawsze jest reklamowana w dokumentacjach bibliotek, chociaż jest to sposób wykorzystania wyprowadzania w Scali 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)

Linia 18 nowy TC `Named` zostaje wprowadzony. Ten najaktywniejszy współtwórca nie jest ściśle związany z branżą blockchain. Jego celem jest nadanie nazwy blockchainowi na podstawie nazwy klasy przypadku.

Najpierw skup się na liniach definicji 36-38. Istnieją 2 składnie wyznaczania najlepszego współtwórcy:

  1. W linii 36 instancję TC można zdefiniować bezpośrednio w klasie przypadku za pomocą `derives` słowo kluczowe. Pod maską kompilator generuje dany plik `Named` instancja w `Polkadot` obiekt towarzyszący.
  2. W wierszach 37 i 38 instancje klas typów są podawane w istniejących klasach za pomocą `TC.derived

W linii 31 zdefiniowano rozszerzenie ogólne (patrz poprzednie sekcje) i `blockchainName` jest używane naturalnie.  

`derivesSłowo kluczowe ` oczekuje metody w postaci `inline def derived[T](using Mirror.Of[T]): TC[T] = ???`, który jest zdefiniowany w linii 24. Nie będę szczegółowo wyjaśniał, co robi kod. W ogólnym zarysie:

  • `inline def` definiuje makro
  • `Mirror` jest częścią zestawu narzędzi do introspekcji typów. Istnieją różne rodzaje serwerów lustrzanych, a w linii 26 kod skupia się na `Product` lustra (klasa przypadków jest produktem). Linia 27, jeśli programiści próbują wyprowadzić coś, co nie jest plikiem `Product`, kod nie zostanie skompilowany.
  • `Mirror` zawiera inne typy. Jeden z nich, `MirrorLabel`, to ciąg znaków zawierający nazwę typu. Ta wartość jest używana w implementacji, linia 29, `Named`TC.

Autorzy NW mogą używać metaprogramowania do udostępniania funkcji, które ogólnie generują instancje NW danego typu. Programiści mogą używać dedykowanego API biblioteki lub narzędzi do wyprowadzania Scala do tworzenia instancji dla swojego kodu.

Niezależnie od tego, czy do wdrożenia najlepszego współtwórcy potrzebny jest kod ogólny, czy konkretny, w każdej sytuacji znajdziesz rozwiązanie. 

Podsumowanie wszystkich korzyści

  • Rozwiązuje problem ekspresji
    • Nowe typy mogą implementować istniejące zachowanie poprzez tradycyjne dziedziczenie cech
    • Nowe zachowania można zaimplementować na istniejących typach
  • Oddzielenie troski
    • Kod nie jest zniekształcony i można go łatwo usunąć. Najlepszy współtwórca oddziela dane od zachowania, co jest motto programowania funkcjonalnego.
  • To jest bezpieczne
    • Jest bezpieczny pod względem typu, ponieważ nie opiera się na introspekcji. Pozwala uniknąć dopasowywania dużych wzorców obejmujących typy. jeśli natkniesz się na pisanie takiego kodu, możesz wykryć przypadek, w którym wzór TC będzie idealnie pasował.
    • Ukryty mechanizm jest bezpieczny przy kompilacji! Jeśli w czasie kompilacji brakuje instancji, kod nie zostanie skompilowany. Nic dziwnego w czasie wykonywania.
  • Wprowadza polimorfizm ad hoc
    • W tradycyjnym programowaniu obiektowym zwykle brakuje polimorfizmu ad hoc.
    • Dzięki polimorfizmowi ad hoc programiści mogą zaimplementować to samo zachowanie dla różnych niepowiązanych typów bez używania tradycyjnego podtypowania (które łączy kod)
  • Wstrzykiwanie zależności stało się proste
    • Instancję TC można zmienić w odniesieniu do zasady podstawienia Liskova. 
    • Jeśli komponent jest zależny od najlepszego współtwórcy, można łatwo wstrzyknąć próbnego współtwórcę do celów testowych. 

Przeciwwskazania

Każdy młotek jest przeznaczony do rozwiązywania różnych problemów.

Klasy typów służą do rozwiązywania problemów behawioralnych i nie można ich używać do dziedziczenia danych. Wykorzystaj w tym celu kompozycję.

Zwykłe podtypy są prostsze. Jeśli posiadasz bazę kodu i nie dążysz do rozszerzalności, klasy typów mogą być przesadą.

Na przykład w rdzeniu Scala znajduje się plik `Numeric` wpisz klasę:

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

Używanie takiej klasy typów naprawdę ma sens, ponieważ pozwala nie tylko na ponowne użycie algorytmów algebraicznych na typach osadzonych w Scali (Int, BigInt,…), ale także na typach zdefiniowanych przez użytkownika (a `ComplexNumber` na przykład).

Z drugiej strony implementacja kolekcji Scala używa głównie podtypów zamiast klasy typów. Ten projekt ma sens z kilku powodów:

  • Interfejs API kolekcji powinien być kompletny i stabilny. Ujawnia typowe zachowania poprzez cechy dziedziczone przez implementacje. Wysoka rozszerzalność nie jest tutaj szczególnym celem.
  • Musi być prosty w obsłudze. TC zwiększa obciążenie psychiczne programisty użytkownika końcowego.
  • Najlepszy Współtwórca może również wiązać się z niewielkimi kosztami wydajności. Może to mieć kluczowe znaczenie dla interfejsu API kolekcji.
  • Jednak interfejs API kolekcji jest nadal rozszerzalny za pomocą nowego TC zdefiniowanego w bibliotekach stron trzecich.

Wnioski

Widzieliśmy, że TC to prosty wzór, który rozwiązuje duży problem. Dzięki bogatej składni Scali wzorzec TC można zaimplementować i wykorzystać na wiele sposobów. Wzorzec TC jest zgodny z paradygmatem programowania funkcjonalnego i jest fantastycznym narzędziem do czystej architektury. Nie ma złotego środka i należy zastosować wzór TC, kiedy pasuje.

Mam nadzieję, że zdobyłeś wiedzę czytając ten dokument. 

Kod dostępny jest pod adresem https://github.com/jprudent/type-class-article. Jeśli masz jakiekolwiek pytania lub uwagi, skontaktuj się ze mną. Jeśli chcesz, możesz użyć problemów lub komentarzy do kodu w repozytorium.


Hieronim PRUDENT

Software Engineer

Znak czasu:

Więcej z Księga główna