Tyyppitunnit Scala3:ssa: Aloittelijan opas | Ledger

Tyyppitunnit Scala3:ssa: Aloittelijan opas | Ledger

Lähdesolmu: 3028113

Tämä asiakirja on tarkoitettu aloittelevalle Scala3-kehittäjälle, joka on jo perehtynyt Scala-proseen, mutta on ymmällään kaikista `implicits` ja parametroidut ominaisuudet koodissa.

Tämä asiakirja selittää miksi, miten, missä ja milloin Tyyppiluokat (TC).

Tämän asiakirjan luettuaan aloitteleva Scala3-kehittäjä saa vankkaa tietämystä käytettäväksi ja sukeltaa lähdekoodiin paljon Scala-kirjastoista ja ala kirjoittaa idiomaattista Scala-koodia.

Aloitetaan siitä, miksi…

Ilmaisuongelma

Vuonna 1998, Philip Wadler totesi että "ilmaisuongelma on vanhan ongelman uusi nimi". Se on ohjelmiston laajennettavuuden ongelma. Herra Wadlerin kirjoittamisen mukaan ilmaisuongelman ratkaisun tulee noudattaa seuraavia sääntöjä:

  • Sääntö 1: Salli täytäntöönpano olemassa olevista käytöksistä (Ajattele Scala-ominaisuutta), johon voidaan soveltaa uusia esityksiä (ajattele tapausluokkaa)
  • Sääntö 2: Salli toteutus uusia käytöstapoja sovelletaan olemassa olevia edustajia
  • Sääntö 3: Se ei saa vaarantaa tyyppi turvallisuus
  • Sääntö 4: Se ei saa edellyttää uudelleenkääntämistä olemassa oleva koodi

Tämän ongelman ratkaiseminen on tämän artikkelin hopealanka.

Sääntö 1: olemassa olevan käyttäytymisen toteuttaminen uudessa esityksessä

Jokaisella oliopohjaisella kielellä on valmiina ratkaisu säännölle 1 alatyypin polymorfismi. Voit turvallisesti toteuttaa minkä tahansa `trait` määritelty riippuvuudessa `class` omassa koodissasi kääntämättä riippuvuutta uudelleen. Katsotaanpa sitä toiminnassa:

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

Tässä kuvitteellisessa esimerkissä kirjasto `Lib1` (rivi 5) määrittää ominaisuuden`Blockchain` (rivi 6) ja sen kaksi toteutusta (rivit 2 ja 9). `Lib1` pysyy samana KAIKESSA tässä asiakirjassa (säännön 4 täytäntöönpano).

`Lib2` (rivi 15) toteuttaa olemassa olevan toiminnan `Blockchain`uudella luokalla`Polkadot` (sääntö 1) tyyppiturvallisella (sääntö 3) tavalla ilman uudelleenkääntämistäLib1` (sääntö 4). 

Sääntö 2: uusien käyttäytymismallien käyttöönotto olemassa oleviin esityksiin

Kuvitellaan sisään `Lib2"haluamme uuden käyttäytymisen".lastBlock` toteutetaan erikseen kullekin `Blockchain`.

Ensimmäinen asia, joka tulee mieleen, on suuren kytkimen luominen parametrin tyypin perusteella.

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

Tämä ratkaisu on heikko uudelleentoteutus tyyppipohjaisesta polymorfismista, joka on jo valmiiksi kirjoitettu!

`Lib1` jätetään koskemattomaksi (muista, että sääntö 4 on voimassa kaikkialla tässä asiakirjassa). 

Ratkaisu, joka toteutettiin `Lib2` on okei kunnes vielä yksi lohkoketju otetaan käyttöön `Lib3`. Se rikkoo tyyppiturvallisuussääntöä (sääntö 3), koska tämä koodi epäonnistuu ajon aikana rivillä 37. Ja muuttamalla `Lib2` rikkoisi sääntöä 4.

Toinen ratkaisu on käyttää `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` jätetään koskemattomaksi (säännön 4 täytäntöönpano koko asiakirjassa). 

`Lib2` määrittää tyypin käytöksen (rivi 21) ja `laajennus` olemassa oleville tyypeille (rivit 23 ja 25).

Rivit 28-30, uutta käyttäytymistä voidaan käyttää jokaisessa luokassa. 

Mutta tätä uutta käyttäytymistä ei voida kutsua polymorfiseksi (rivi 32). Kaikki yritykset tehdä niin johtavat käännösvirheisiin (rivi 33) tai tyyppipohjaisiin kytkimiin. 

Tämä sääntö nro 2 on hankala. Yritimme toteuttaa sen omalla polymorfismin määritelmällämme ja "laajennustemppullamme". Ja se oli outoa.

Puuttuu pala ns ad hoc -polymorfismi: kyky lähettää turvallisesti käyttäytymisen toteutus tyypin mukaan, missä tahansa käyttäytyminen ja tyyppi määritellään. Syötä Tyyppi Luokka kuvio.

Tyyppiluokan malli

Tyyppiluokan (lyhennettynä TC) kuvioreseptissä on 3 vaihetta. 

  1. Määrittele uusi käyttäytyminen
  2. Toteuta käyttäytyminen
  3. Käytä käyttäytymistä

Seuraavassa osiossa toteutan TC-mallin yksinkertaisimmalla tavalla. Se on monisanainen, kömpelö ja epäkäytännöllinen. Mutta odota, nämä varoitukset korjataan askel askeleelta pidemmälle asiakirjassa.

1. Määrittele uusi käyttäytyminen
Scala

object Lib2:
  import Lib1.*

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

`Lib1` on jälleen kerran jätetty koskemattomaksi.

Uusi käytös is TC toteutui ominaisuuden avulla. Ominaisuudessa määritellyt toiminnot ovat tapa soveltaa joitain tämän käyttäytymisen puolia.

Parametri `A` edustaa tyyppiä, johon haluamme soveltaa käyttäytymistä, jotka ovat `:n alatyyppejäBlockchain` meidän tapauksessamme.

Muutama huomio:

  • Tarvittaessa parametroitu tyyppi `A` voidaan edelleen rajoittaa Scala-tyyppisellä järjestelmällä. Voisimme esimerkiksi pakottaa `A` olla `Blockchain`. 
  • Lisäksi TC:ssä voisi olla monia muita toimintoja.
  • Lopuksi jokaisella funktiolla voi olla paljon enemmän mielivaltaisia ​​parametreja.

Mutta pidetään asiat yksinkertaisina luettavuuden vuoksi.

2. Toteuta käyttäytyminen
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")

Jokaiselle tyypille uusi `LastBlock` käyttäytymistä odotetaan, on olemassa erityinen esimerkki siitä käytöksestä. 

``Ethereum` toteutusrivi 22 lasketaan `eth` ilmentymä välitetty parametrina. 

`LastBlock`for `Bitcoin` rivi 25 on toteutettu hallitsemattomalla IO:lla, eikä se käytä sen parametria.

Joten, `Lib2`toteuttaa uutta toimintaa`LastBlock`for `Lib1` luokat.

3. Käytä käyttäytymistä
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))

Rivi 30 `useLastBlock` käyttää esiintymää `A` ja `LastBlock` tälle tapaukselle määritetty käyttäytyminen.

Rivi 33 `useLastBlock` kutsutaan esiintymän ` kanssaEthereum` ja `:n toteutusLastBlock` määritelty `Lib2`. Huomaa, että on mahdollista ohittaa mikä tahansa vaihtoehtoinen `-toteutusLastBlock[A]` (ajattele riippuvuuden injektio).

`useLastBlock` on liima esityksen (todellinen A) ja sen käyttäytymisen välillä. Data ja käyttäytyminen erotetaan toisistaan, mitä toiminnallinen ohjelmointi kannattaa.

Keskustelu

Kerrataan lauseongelman säännöt:

  • Sääntö 1: Salli täytäntöönpano olemassa olevista käytöksistä  sovelletaan uusia luokkia
  • Sääntö 2: Salli toteutus uusia käytöstapoja sovelletaan olemassa olevia luokkia
  • Sääntö 3: Se ei saa vaarantaa tyyppi turvallisuus
  • Sääntö 4: Se ei saa edellyttää uudelleenkääntämistä olemassa oleva koodi

Sääntö 1 voidaan ratkaista suoraan alatyypin polymorfismilla.

Juuri esitetty TC-malli (katso edellinen kuvakaappaus) ratkaisee säännön 2. Se on tyyppiturvallinen (sääntö 3), emmekä koskaan koskettaneet `Lib1` (sääntö 4). 

Sen käyttö on kuitenkin epäkäytännöllistä useista syistä:

  • Rivit 33-34 meidän on välitettävä käyttäytyminen eksplisiittisesti sen esiintymän mukaan. Tämä on ylimääräinen yleiskulut. Meidän pitäisi vain kirjoittaa `useLastBlock(Bitcoin())`.
  • Rivin 31 syntaksi on harvinainen. Haluaisimme mieluummin kirjoittaa tiiviin ja oliokeskeisemmän  `instance.lastBlock()` lausunto.

Korostetaan joitain Scala-ominaisuuksia käytännönläheistä TC-käyttöä varten. 

Parannettu kehittäjäkokemus

Scalassa on ainutlaatuinen joukko ominaisuuksia ja syntaktisia sokereita, jotka tekevät TC:stä todella nautinnollisen kokemuksen kehittäjille.

Implisiittiset

Implisiittinen laajuus on erityinen laajuus, joka ratkaistaan ​​käännöshetkellä ja jossa voi olla vain yksi tietyn tyyppinen esiintymä. 

Ohjelma asettaa ilmentymän implisiittiseen soveltamisalaan `given` avainsana. Vaihtoehtoisesti ohjelma voi hakea ilmentymän implisiittisestä laajuudesta avainsanalla `using`.

Implisiittinen laajuus ratkaistaan ​​käännösaikana, ja sitä voidaan muuttaa dynaamisesti ajon aikana. Jos ohjelma kääntää, implisiittinen laajuus on ratkaistu. Suorituksen aikana ei ole mahdollista puuttua implisiittisiä ilmentymiä, joissa niitä käytetään. Ainoa mahdollinen hämmennys voi johtua väärän implisiittisen ilmentymän käytöstä, mutta tämä ongelma jätetään tuolin ja näppäimistön väliin.

Se eroaa maailmanlaajuisesta laajuudesta, koska: 

  1. Se on ratkaistu kontekstuaalisesti. Ohjelman kaksi sijaintia voivat käyttää samantyyppistä esiintymää implisiittisessä laajuudessa, mutta nämä kaksi esiintymää voivat olla erilaisia.
  2. Kulissien takana koodi välittää implisiittisiä argumentteja toimimaan, kunnes implisiittinen käyttö saavutetaan. Se ei käytä globaalia muistitilaa.

Takaisin tyyppiluokkaan! Otetaan täsmälleen sama esimerkki.

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` on sama muokkaamaton koodi, jonka määritimme aiemmin. 

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

Rivi 19 uusi toimintatapa `LastBlock` on määritelty, aivan kuten teimme aiemmin.

Rivi 22 ja rivi 25, `val` korvataan `given`. Molemmat toteutukset `LastBlock` asetetaan implisiittiseen soveltamisalaan.

Rivi 31 `useLastBlock` ilmoittaa käyttäytymisestä`LastBlock` implisiittisenä parametrina. Kääntäjä ratkaisee `:n sopivan esiintymänLastBlock` implisiittisestä laajuudesta kontekstualisoituna soittajan sijainnista (rivit 33 ja 34). Rivi 28 tuo kaiken kohteesta `Lib2", mukaan lukien implisiittinen soveltamisala. Joten kääntäjä välittää ilmentymien määrittämät rivit 22 ja 25 viimeisenä parametrina `useLastBlock`. 

Kirjaston käyttäjänä tyyppiluokan käyttö on helpompaa kuin ennen. Rivit 34 ja 35 kehittäjän on vain varmistettava, että käyttäytymisen esiintymä lisätään implisiittiseen soveltamisalaan (ja tämä voi olla pelkkä "import`). Jos implisiittinen ei ole `given` missä koodi on `using` se, kääntäjä kertoo hänelle.

Scalan implisiittiset ominaisuudet helpottavat luokkaesiintymien välittämistä niiden käyttäytymistapausten kanssa.

Implisiittiset sokerit

Edellisen koodin rivejä 22 ja 25 voidaan edelleen parantaa! Toistellaanpa TC-toteutuksia.

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

Rivit 22 ja 25, jos ilmentymän nimi on käyttämätön, se voidaan jättää pois.

Scala


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

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

Riveillä 22 ja 25 tyypin toisto voidaan korvata merkillä `with` avainsana.

Scala

given LastBlock[Ethereum] = _.lastBlock

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

Koska käytämme rappeutunutta ominaisuutta, jossa on yksi funktio, IDE voi ehdottaa koodin yksinkertaistamista SAM-lausekkeella. Vaikka se on oikein, en usko, että se on SAM:n oikea käyttö, ellet koodaa golfia satunnaisesti.

Scala tarjoaa syntaktisia sokereita, jotka virtaviivaistavat syntaksia ja poistavat tarpeettoman nimeämisen, määrittelyn ja tyyppien redundanssin.

Laajentaminen

Viisaasti käytettynä `extension` -mekanismi voi yksinkertaistaa tyyppiluokan käytön syntaksia.

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)

Rivit 28-29 yleinen laajennusmenetelmä `lastBlock` on määritelty mille tahansa `A` ja `LastBlock` TC-parametri implisiittisessä laajuudessa.

Rivit 33-34 laajennus hyödyntää oliosuuntautunutta syntaksia käyttääkseen TC:tä.

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)

Rivillä 28 TC-parametri voidaan määrittää myös koko laajennukselle toiston välttämiseksi. Rivillä 30 käytämme uudelleen laajennuksen TC:tä määrittämään `penultimateBlock` (vaikka se voitaisiin ottaa käyttöön `LastBlock` ominaisuus suoraan)

Taika tapahtuu, kun TC:tä käytetään. Ilmaisu tuntuu paljon luonnollisemmalta, mikä antaa illuusion siitä käytöksestälastBlock` sekoitetaan ilmentymään.

Yleinen tyyppi TC:llä
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))

Rivi 34 funktio käyttää implisiittistä TC:tä. Huomaa, että TC:tä ei tarvitse nimetä, jos se on tarpeeton.

TC-mallia käytetään niin laajasti, että on olemassa yleinen tyyppisyntaksi, joka ilmaisee "tyyppiä, jolla on epäsuora käyttäytyminen". Rivin 36 syntaksi on ytimekkäämpi vaihtoehto edelliselle (rivi 34). Se välttää nimeämättömän implisiittisen TC-parametrin ilmoittamisen.

Tämä päättää kehittäjäkokemusosion. Olemme nähneet, kuinka laajennukset, implisiitit ja jotkut syntaktiset sokerit voivat tarjota vähemmän sekalaisen syntaksin, kun TC:tä käytetään ja määritellään.

Automaattinen johtaminen

Monet Scala-kirjastot käyttävät TC:tä, joten ohjelmoijan on otettava ne käyttöön koodikantaansa.

Esimerkiksi Circe (json-sarjanpoistokirjasto) käyttää TC:tä `Encoder[T]` ja `Decoder[T]` ohjelmoijien käyttöönotettaviksi koodikantassaan. Kun se on toteutettu, kirjaston koko aluetta voidaan käyttää. 

Näitä TC:n toteutuksia on enemmän kuin usein datasuuntautuneita kartoittajia. Ne eivät tarvitse liiketoimintalogiikkaa, ne ovat tylsiä kirjoittaa ja taakka synkronoida tapausluokkien kanssa.

Tällaisessa tilanteessa nuo kirjastot tarjoavat ns automaattinen johdannainen tai puoliautomaattinen johtaminen. Katso esimerkiksi Circe automaattinen ja puoliautomaattinen johtaminen. Puoliautomaattisella johtamisella ohjelmoija voi ilmoittaa tyyppiluokan esiintymän pienellä syntaksilla, kun taas automaattinen johtaminen ei vaadi koodin muuttamista paitsi tuontia.

Konepellin alla, käännösaikana, yleisiä makroja itsetutkiskelu tyypit puhtaana tietorakenteena ja luoda TC[T] kirjaston käyttäjille. 

Yleisesti TC:n johtaminen on hyvin yleistä, joten Scala esitteli täydellisen työkalulaatikon tätä tarkoitusta varten. Tätä menetelmää ei aina mainosteta kirjaston dokumentaatioissa, vaikka se on Scala 3:n tapa käyttää johtamista.

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)

Rivi 18 uusi TC `Named` esitellään. Tämä TC ei liity varsinaisesti blockchain-liiketoimintaan. Sen tarkoitus on nimetä lohkoketju tapausluokan nimen perusteella.

Keskity ensin määritelmäriville 36-38. TC:n johtamiseen on kaksi syntaksia:

  1. Rivillä 36 TC-ilmentymä voidaan määrittää suoraan tapausluokassa `derives` avainsana. Kannen alla kääntäjä luo tietyn `Named` esimerkki kohteessa `Polkadot` kumppaniobjekti.
  2. Rivit 37 ja 38, tyyppiluokkien esiintymät annetaan jo olemassa oleville luokille, joissa on `TC.derived

Rivi 31 määritellään yleinen laajennus (katso edelliset osat) ja `blockchainName` käytetään luonnollisesti.  

``derives` avainsana odottaa menetelmän muodossa `inline def derived[T](using Mirror.Of[T]): TC[T] = ???` joka on määritelty rivillä 24. En selitä perusteellisesti, mitä koodi tekee. Pääpiirteissään:

  • `inline def` määrittää makron
  • `Mirror` on osa työkalupakkia tyyppien itsetutkiskeluun. Peilejä on erilaisia, ja rivillä 26 koodi keskittyy `Product` peilit (koteloluokka on tuote). Rivi 27, jos ohjelmoijat yrittävät johtaa jotain, joka ei ole `Product`, koodia ei käännetä.
  • `Mirror` sisältää muita tyyppejä. Yksi niistä, `MirrorLabel`, on merkkijono, joka sisältää tyypin nimen. Tätä arvoa käytetään `:n toteutuksessa rivillä 29Named` TC.

TC-tekijät voivat käyttää metaohjelmointia tarjotakseen toimintoja, jotka generoivat yleisesti tietyn tyypin TC-esiintymiä. Ohjelmoijat voivat käyttää omaa kirjastosovellusliittymää tai Scala-johdannaistyökaluja luodakseen esiintymiä koodilleen.

Tarvitsetpa yleistä tai erityistä koodia TC:n toteuttamiseen, jokaiseen tilanteeseen on ratkaisu. 

Yhteenveto kaikista eduista

  • Se ratkaisee ilmaisuongelman
    • Uudet tyypit voivat toteuttaa olemassa olevaa käyttäytymistä perinteisen piirteen periytymisen kautta
    • Uusia käyttäytymismalleja voidaan toteuttaa olemassa oleviin tyyppeihin
  • Huolenaiheen erottaminen
    • Koodia ei ole sekoitettu ja se on helposti poistettavissa. TC erottaa datan ja käyttäytymisen, mikä on toiminnallinen ohjelmoinnin motto.
  • Se on turvallista
    • Se on tyyppiturvallinen, koska se ei ole riippuvainen itsetutkiskelusta. Se välttää suuria kuvioiden yhteensopivuutta, johon liittyy tyyppejä. jos kohtaat itsesi kirjoittamassa tällaista koodia, saatat havaita tapauksen, jossa TC-kuvio sopii täydellisesti.
    • Implisiittinen mekanismi on käännetty turvallisesti! Jos esiintymä puuttuu käännöshetkellä, koodia ei käännetä. Ei yllätys ajon aikana.
  • Se tuo ad hoc -polymorfismin
    • Ad hoc -polymorfismi puuttuu yleensä perinteisestä olioohjelmoinnista.
    • Ad-hoc-polymorfismin avulla kehittäjät voivat toteuttaa saman käyttäytymisen useille toisiinsa liittymättömille tyypeille ilman perinteistä alatyyppiä (joka yhdistää koodin)
  • Riippuvuusinjektio tehty helpoksi
    • TC-instanssia voidaan muuttaa Liskov-korvausperiaatteen mukaisesti. 
    • Kun komponentti on riippuvainen TC:stä, pilkattu TC voidaan helposti ruiskuttaa testaustarkoituksiin. 

Vastailmaisimet

Jokainen vasara on suunniteltu monenlaisiin ongelmiin.

Tyyppiluokat on tarkoitettu käyttäytymisongelmiin, eikä niitä saa käyttää tietojen periytymiseen. Käytä koostumusta tähän tarkoitukseen.

Tavallinen alatyyppi on yksinkertaisempi. Jos omistat koodikannan etkä tavoittele laajennettavuutta, tyyppiluokat voivat olla ylivoimaisia.

Esimerkiksi Scala-ytimessä on `Numeric` tyyppiluokka:

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

On todella järkevää käyttää tällaista tyyppiluokkaa, koska se ei salli ainoastaan ​​algebrallisten algoritmien uudelleenkäyttöä tyypeissä, jotka on upotettu Scalaan (Int, BigInt, …), vaan myös käyttäjän määrittämissä tyypeissä (a `ComplexNumber` esimerkiksi).

Toisaalta Scala-kokoelmien toteutuksessa käytetään enimmäkseen alatyyppiä tyyppiluokan sijaan. Tämä muotoilu on järkevä useista syistä:

  • Kokoelman APIn oletetaan olevan täydellinen ja vakaa. Se paljastaa yleisen käyttäytymisen toteutusten perimien ominaisuuksien kautta. Erittäin laajennettavissa oleminen ei ole tässä erityinen tavoite.
  • Sen on oltava helppokäyttöinen. TC lisää henkistä lisäkustannusta loppukäyttäjän ohjelmoijalle.
  • TC saattaa myös aiheuttaa pieniä yleiskustannuksia suorituskyvyssä. Tämä voi olla kriittistä kokoelma-API:lle.
  • Kokoelman sovellusliittymä on kuitenkin edelleen laajennettavissa kolmannen osapuolen kirjastojen määrittelemän uuden TC:n kautta.

Yhteenveto

Olemme nähneet, että TC on yksinkertainen malli, joka ratkaisee suuren ongelman. Scala-rikkaan syntaksin ansiosta TC-kuvio voidaan toteuttaa ja käyttää monin tavoin. TC-malli on toiminnallisen ohjelmoinnin paradigman mukainen ja on upea työkalu puhtaaseen arkkitehtuuriin. Hopealuotia ei ole ja TC-kuvio on käytettävä, kun se sopii.

Toivottavasti sait tietoa lukiessasi tätä asiakirjaa. 

Koodi on saatavilla osoitteessa https://github.com/jprudent/type-class-article. Ota minuun yhteyttä, jos sinulla on kysyttävää tai huomautuksia. Voit halutessasi käyttää ongelmia tai koodikommentteja arkistossa.


Jerome PRUDENT

Software Engineer

Aikaleima:

Lisää aiheesta pääkirja