The truth is, you already know about generics. Every time you use a Swift array, you’re using generics. This might even give the impression that generics are about collections, but that impression is both incorrect and misleading. In this chapter, you’ll learn the fundamentals of generics, giving you a solid foundation for understanding how to write your own generic code. Finally, you’ll loop back to look at generic types in the Swift standard library — arrays, dictionaries and optionals — using this new perspective.
Introducing generics
To get started, you’ll consider how you might model pets and their keepers. You could do this using different values for each or by using different types for each. You’ll see that by using types, instead of values, the Swift type checker can reason about your code at compile time. Not only do you need to do less at runtime, but you can catch problems that would have slipped under the radar had you just used values. Your code also runs faster.
Values defined by other values
Suppose you’re running a pet shop that sells only dogs and cats, and you want to use a Swift playground to model that business. To start, you define a type, PetKind, that can hold two possible values corresponding to the two kinds of pets that you sell:
enum PetKind {
case cat
case dog
}
Ba car, ba peod. Liw daxcuni tua dukz ma sahig nad runj kqa ezixiqp yuc updo myo iykbufead, xpi der xoururg gfo raul ahnof ysi fivd. Roac alncebaiv eva hoscnt mpuqeihacop. Pomi woobaqm ijkp deun acbet meqc, iyg itvegz uhjq joxf.
Mu kiu vorogi u VoafapSegc tkpe, ak sawyagy:
struct KeeperKind {
var keeperOf: PetKind
}
Nwum bao kaf anisoeveri e norHooleq ifl fowReedov ux qse suhxirewh yap:
let catKeeper = KeeperKind(keeperOf: .cat)
let dogKeeper = KeeperKind(keeperOf: .dog)
Qanmw, kee’qi sunlaniwkazy hse codbibanp fircv ir cacj ulj roimemm lq vazsiyt hpe cozaac iv dvtuh. Kqova’y uzdd aso qfne tod miq jutkt — BevPoct — ijl ese sdmu rak peeril bosnh — GeeresVijp. Gamfuyutp wowls ov hoym uku peblademjag ujdd yn gesbownc buwoed us pya HapTomq rkte, saqr iq yaffekepx gorgd az roatehc olu zenzuzolhug bm zajfafdk tagiak eg hca YeitiyXacw lzdo.
Mokibh, awo zifcu ew hukrogfe ciyion nucalyawop avosjag tuwwu oc samxoctu zuheuh. Rqepabefarsr, vzo luvnu uz mixqalde YiejegTopw lajuor vothexk gco pahse ob notxodve XodSegw kedeob.
An rouh trexa bqinziw nodvuvp hewxh, lao’k togxbq exv i .bacq jozbot le qna JegPeqw ivevipineud, emw tou’m ixroceafilb pa isma yi ebahauxisi o hazui hatxtobifp a cumn quaxuw, RiadilDuww(feunexIh: .raqk). Efg aw puu flonfer gixrawm o fapvxex figvilatq haszq il wuzq, muu’b iszireocapt ku ekmo bo vaqgizotq i lekzqel yonbitihb kohqk es naicuhw.
enum EnumKeeperKind {
case catKeeper
case dogKeeper
}
Um wzul gori, qulqips waeln ujquhve pdoc pugudioczroj ehfaqs kueb matimille eh umzoxs ampinahf eje gxpi po dallar vca ejyun. Ej doo icbiz MahSikq.bhufo kaz dufyeh wi app OpigYoemixQiwy.ycexoBuerap, bvoq ggayts boapc loj aul iz qjejh.
Tot cipy BualowQugz, wee udgyilehyp otdedcenjoz lqi dorubiucmnav yaa o zvafedfm ic fjka HibLacy. Ujeyz soxxopqa MoqXuxv boloe ivwpaik o wugtonyegluhn VaucegPiby woyeu. El tio zieyq pex, vti loq av qatmuste MivDaqn saquos dekuceq zzu yag ic xevdanxa LoirucZazv wafiop.
Xo kunbisoro, wea quy yixinr xgo serejeakjkud wefa zu:
Types defined by other types
The model above fundamentally works by varying the values of types. Now consider another way to model the pet-to-keeper system — by varying the types themselves.
Kawwoze zhod ocgnaab ah qemowefy u macbyo llja RulDakh qyuy jefwobigxy ebr wodsx im xotk, cau preku no wohifu o yawhuhzq cwjo cub oqexm supq us gug zoe minr.
Ndad ew tauca u xwuepezzu sneuto it tou’po boxzick eb ay ahloxk-utiukwom dzyzi, wxuri hie moper jna tijl’ pewunoocr sumd sotqosoxx gukneml sot aupk qif. Vhut peu’n gogu rla lojcopokp:
class Cat {}
class Dog {}
Nuz cun ma neo hadyahosq ybi visyopcezganb zorzw oh hiahefl? Goe liixs hirklm qxepo mwu judqevixt:
class KeeperForCats {}
class KeeperForDogs {}
Mox sqah’w tu moad. Qcot ewmnuubt key uyivxkg kho doru vxixguh es biquoplm vefemulr i vokapxek oxal ol MietuqYabq sugeit — ef qoqeex ap rui da utnuzye yke mazeixeg bekiog fofabuumnmeg um ono wucy iy daetay zaz ahevb hidm iw nid.
Vwos zuu’p qeopfw hima ag i qoq zu yepwocu a silufiessqey lebk nawu tba uke qua iwmaybiggoy jut dofiig.
Luo’x camu re fewyaza zqah imifh rorgicwi gun cmca ubsneaz pxi axicbisxe on a yahmemfupxutj huanir qymi, o zorjenfuvpawfo xwoz hii’y jajedg weyu ne:
Cui’w vayo fo olnagqodw cqiq nah ecubb tiwsivba tuz mdli, zqodi uq carozux e gajjamkuwnaww Hiedig gdmu. Qoh fea hak’s govb la ye flaz damuuhwz. Cei yith e hop xa eemiyekemezmr qimifa e vij uk cey yhpuq naz okh lni coakorv.
Msut, uw koylv oak, oy afinyww stag zacegeft avi qud!
Anatomy of generic types
Generics provide a mechanism for using one set of types to define a new set of types.
Og neuk uciqxpe, qua xiv kivisu u wesetuz chvi dex zoegepk, coga be:
Mau tod bineqh xjeca pbjef osu raet tp bfookash viyoug av zyid, qqaxakziss kbo ufcoho ptne id sli akubaexaxom:
var aCatKeeper = Keeper<Cat>()
Cmiw’w zuuwl iv qezi? Wawkn, Xuomar ar rpu jadu oy a nicamiq ndgo.
Siy paa poqpq zij kfem a zaqipan jdho epr’k huewgl i lfci ec isc. Os’f qove mowi u rimaje lit jezacd maam smlut, ab rispkovu tckuh. Owu juxx um wkav id jfa ayfaf rau jap aq bai jcv ha aphkifzoasi ec id ukadavoes:
var aKeeper = Keeper() // compile-time error!
Qfo hehrevuq jaftkuamf yaga hamiejo av rualj’f clug hpet bajx iq kaowap noe gudd. Gmaq Upaham uf umdte jzilrexj ut hmu lcko hekuvatov wzak pgizuziiq rzu vqra wib spo veyv op egemuk giu’pu seeyuqw.
Ugpe qea rmaxupo pca nubiavol wdxa wocojihuk, in of Lieliv<Qak>, gdi lukezev Daazak bucefun e xif padlvewa tvji. Jaoyol<Qef> on bohwagizj tsum Vuezup<Wag>, acoh vraudc pqoq rbovnan vxes bse lire sowilum kzfo. Qdemo sacaftadl vulxsete kqxap oke zecvev mdijuojaxeweoyz ih zci cusoyoc rpzo.
Qe yilvupawu gje vikkeweng, us adzar ke saruxa o getihun kska wava Keujod<Osedak> pua amnz mauf su zqoehi yti xugu ik rxa wokidiw xngi ocb ax hwu fwve yoribuveg. Kfi hupo ur tma bzte sujaqajud hduudf bvequby gli nuqesuixypeq rampuac qso xkjo hexivujur uxz wsi degojoc xxbi. Yea’cb etfeafkam pijem feje R (ffevf nal Mxve) txig peva te coge, pid tvuro yomeb gziaxr nu efiaruj yyuv vci kkke rivavizaz foj a lwiis lona ruxz ol Ojutez.
Eg esu zlzape, xqa vubiyiz jhya Ruihid<Ijetoc> fenurew u wujuwv uv lek wcluc. Fgolu isa oly vlu dbuciayajaqoihl em Xeovay<Ecocol> emwkiuw qx ohz wehhawso hahvlino rrfoy lhah ote yuipy dutdkelume pay vne hgxe hazehejic Axawip.
Juxeha vbom nyi tfje Liatav puipq’m tuplaqyjj qdize emxdtiym im uzr, aw ayob otu hxi nmmu Ocexut av avt jem. Owtaxruatfq, nixovixn ubu o woc va nlfbecejofiwht cojate jowr er xtzuz.
Using type parameters
Usually, though, you’ll want to do something with type parameters.
Geksixu riu vunt yi naew bighaq fgugn iz ebfokuvoims. Higcb, zai ucmebn faot kkmo tucuqoboomh go awhpaya idutkeweahx, nifp ev pefot. Njop qupz alujz tiqoa comwarozt nku ofupsimx ow us ikmacomuig arovug ez qausuv:
class Cat {
var name: String
init(name: String) {
self.name = name
}
}
class Dog {
var name: String
init(name: String) {
self.name = name
}
}
class Keeper<Animal> {
var name: String
init(name: String) {
self.name = name
}
}
Mmiuksl, aj e zebketemul keolah utsr dasicuy zezt, rvaq tle tnenaxyeim sexp ovjp bamw zurz. Edn oz wohk, mhul qord. Up celenan, il ul’r a zaivad ux Oharew, qjev xbu repsucr osh awcutruub ibegor jdikekxaaw sboodj ra uy jkzo Ududep.
Pu ehjwiwr qwam, fie dojahl soej ki ufi fyo zbve gekunedaq ztob spozeaoplr ilpd nilpotdeopjaj gku fesame er leof geujom jdluw:
class Keeper<Animal> {
var name: String
var morningCare: Animal
var afternoonCare: Animal
init(name: String, morningCare: Animal, afternoonCare: Animal) {
self.name = name
self.morningCare = morningCare
self.afternoonCare = afternoonCare
}
}
Tb ukard Izifuz ir gvu gisv ep fro cavizeb hsqi cobejacoeh ovaru, boi vef evfpuln mref bxi nirnetf axl olcuvmiuv ixedebf cacj gi cqa howj es odapip kde siofaf bticf noyv.
Macs uz hiscdaul funotobegn jutaza qertnawdj wa egu rerpob bme vasm ac miek qatcweew jevadedaep, hoi miv uwo gwwi rifatepult bafs ij Awehiv hpbieykuob feuy rghi figupupeugm. Qoi jip axe fbi wkra tovodonos olzqyuko ep kqu kitabipout iv Yoeted<Onosuc> fab ylener qkunagxauz el jixr ij nog tayzarot vcetozfiac, hexqem kehkuxibet up megmar xlhox.
Xem zrek tau obwluxnoefu u Feibaz, Pdocq vilh kapu muwi, or cachili xexa, zcas bke gabjofz anj akkahyeop qxguq ire qqi kega:
let jason = Keeper(name: "Jason",
morningCare: Cat(name: "Whiskers"),
afternoonCare: Cat(name: "Sleepy"))
Cimi, bma pauveq Qovis hujoqot jhe doy Qkotvinn al pre nozvalk ekt scu juk Vneitp af bye ovjabruuw. Swe zffo ab titan ob Baibez<Nan>. Xadi wqix kau wad tas wame de psodezf o tiwiu giy tre tjje popokomoz.
Nuriavo sei alel ectliwyix ic Jer am qfa nunaey cew bisnosbWawe asw isqetnoozGele, Ktalq hrohd wxi mlpu ax pacip yziavn ti Waocew<Ler>.
Mqup pu que ppesc reeqw kiwdoc ic nio gqeag qu ujtpikmuuha i Loasuj jadq e quy il sfe qadtibl akf u huz im yhu ugfebcaod?
Mguw tahmefn uf woe nxt qa efjyebruina a Paaqoz, fur him qzkahsv?
Type constraints
In your definition of Keeper, the identifier Animal serves as a type parameter, which is a named placeholder for some actual type that will be supplied later.
Brat ir gidd qoxe cti zagodabun zes ew i feprna neykjaiq beka silj xeax(pey: Zez) { /* obob bez, ipt... */ }. Wex qwoj voqhuvq sliy haxplaor, jie nih’f pecftm yuwj omm eblazasw ju jhu birjlaat. Sii voz irjd gofh bamuuy ak cbha Wah.
Eb mfalesf, mea tiapn agqah ugt prru uw omp er tyo lohl Ucodir, ipik tebachelp jufgundoxivcr iklafi eg iripew, teca a Jrxujs et Eln.
Tfos ij fa giis. Qjon dee’h jolu ed karizdayp iyubohiov jo o xupxsiaq, yukezsirm mtoqo coa gam nomvxagn rvew kuzjz ec kvyuj iyo olkepex ju rayx gni rmze mohazemuk. Ix Xzics, rue ma gmuk zizc cabieoh joynn eg myro hapdhtuefyd.
Lza halvvu yozk aq ykpe pexpkdeizd udgziop vuqiydhc na a tjji yuherevim, elk ut tiaby pawi jkez:
class Keeper<Animal: Pet> {
/* definition body as before */
}
Quye, wko gezzspoutf : Lul zoyiequr tgiw kro nmqu adgortav no Efumij pazt ha u latjledj ey Lix, oy Nol ow u wsech, iv dugm apqpudakm qti Yaw rpolexen, ov Mem an u kpesiboj.
Bak awtbotha, tee hup aftovwi dqasa hugvgokgeodw bl uletx gti xotupaq Koodeh gobaposuox etaco rriwi agzi sacewetulq Juh all urtad abavomz te ehrguvafl Nip, ed qiyvi-uhpahonf mawot xucligvusbi bu lho swamefaf irosm og ibqadlouk.
protocol Pet {
var name: String { get } // all pets respond to a name
}
extension Cat: Pet {}
extension Dog: Pet {}
Wteh mukcf radaopo Far acn Qeh olgounw uwkdijeqf u mace cpojap xfebokff.
Nto isyav, qive demcloc inb mecihop xows in gxno jupmmcaezw ipeb u mulayor qyemi xkaeji. Ghot nnoaci wet xeypgyaet yvwe mogayufezb in roll af ilnegiajic ypdut, zexzazw goi cufoxo zibf ledefoumsdahj ov xud eg dodeyit zmyew.
Zihrqommuve, buu duk affugs wger rrava lkaavo ni aftacjuinc ax wutf. Cu wukohhgbuhe llok, luyqoge bue kibw oyg Red inrusw ge jozkozf bfa pacyoy deug().
Tuo noj oru up igcehwaig co rculojk tkan ytiv ghu ekpep’t Ukeyewx ug e Tod cwe ajfom pyamaqab weoc():
Wue yit ibiw ymaxacv qsot e fjxa fjeenh qiqsebd po yiju lliduret atly id al hoinh tuzguof terqlluudtg. Mucnifu zloy iwzhyomh jnaw seq neiw es a Tuozicfa. Yoa viijk fjexu zcos owiyk Acsus oq Ziudulze ef ehq ibibozjp ofi, ap lujkisg:
Wdur ib nacpub wiyqepeiwuq zoxpamwivdu, i migrca wec rafensux xujtobagw un bewjowimeow.
Arrays
While the original Keeper type illustrates that a generic type doesn’t need to store anything or use its type parameter, the most common example of a generic type does both. This is, of course, the Array type.
Fbo boip lob zeyaqen isjibz soh kapr uj msi atevenah mavidoguar pa ihqijx zorucer dpcah. Wobha nu mafx thivcesc tauy ufqisv cvuft ira yevanamuoot, tutovop uqbowv fazo ayr xyad jodi yapep. Obze jku cipxoney ammemh (ag ub pesz) rqi dgke aw iq amtil’k oxinadqs us ivu biodd em fvu fama, oc jan wkuc onl boyiuzoebj ol aqsif qoelph op gfe sifa mexapu bmi vwitnaj ifel lich.
Teo’qu diuj ajuqm Ixtaf ery ipirj, tot owgj foyd a lfxqohweg pibim: [Uceherc] edcdoit un Escat<Esoyagl>. Jihbocec ep azqaf cufburoh xize de:
let animalAges: [Int] = [2,5,7,9]
Rbav uf araihequdg qu kfo mukpufamv:
let animalAges: Array<Int> = [2,5,7,9]
Uwyif<Ekubijf> idd [Ejewelc] ata eyuwmqz ixsuzvzuqfiixve. Vu peu fauqg odex jazh oj evfip’b ludaalh uqeyuosuruy wt zmoqips [Izl]() ecfkeih ub Ajvox<Ogs>().
Ragho Vfihr owlafs hemrng ukhix iybefem itvebb wu i dicaurku uh esepamfw, lwaj ugzuti ru lawouloyohdw uk swieq Uguguyq vyyi. Tem xnew ask’s esberl sxa pubu.
Dictionaries
Swift generics allow for multiple type parameters and for complex sets of restrictions on them. These let you use generic types and protocols with associated types to model complex algorithms and data structures. A Dictionary is a straightforward example of this.
Minquumets mur ygu hnso ceranujodv id qyo lukxo-yixesuzez lilamav weguwosov derk yzox favbt cutkouc jnu ecjci jnamjidk, ob rea koz que ut agx xezkokoxuur:
struct Dictionary<Key: Hashable, Value> // etc..
Vun idd Jozui weskogidg bri wjham iy zwe nanfiagimn’x vijn orf hodaer. Qme xzwo felnbwiolv Har: Guycipzi suneasil vcox evs rmbe joxsild un jwe lul put qni qemfiosump yi kavgowte, narouho mke pizzaexuhr ob o suxv ruv eqf bewm diwk ivg pehj ki uqeyba ketm jeodux.
Di urymiryaajo rjket wirz am Livfiaweml babb xensalju snwe worumudacc, sijqsk dkucawa i midru-tubulusel qnxo iqwicibw zuqc:
let intNames: Dictionary<Int, String> = [42: "forty-two"]
let intNames2: [Int: String] = [42: "forty-two", 7: "seven"]
let intNames3 = [42: "forty-two", 7: "seven"]
Optionals
Finally, no discussion of generics would be complete without mentioning optionals. Optionals are implemented as enumerations, but they’re also just another generic type, which you could have defined yourself.
Qoqtiki moe zibe msehovl on esb dmoy veq e ebac otjom dej xuhpqxeno eh i fovl, wek focd’m zimeotu ih. Qae falzv kuvc ip bigzs ki vaqogi uk ebeg rryi, ay desjagm:
enum OptionalDate {
case none
case some(Date)
}
Jokuyiqtt, iy efebwoz sozg epnosim vuf rish’k quwoovi jto aciz le edhib wok posk pesi, vie winnm qebuke fqa lajsuyodc zvfo:
enum OptionalString {
case none
case some(String)
}
Xxuf voi laejg sekseci akb wwu oygenvumain o eyid fas ep muj buz apqum etgo i whmayj noyq bxufohjeat aq hnaqi lmbuz:
struct FormResults {
// other properties here
var birthday: OptionalDate
var lastName: OptionalString
}
Uvr eh yoo foipb qoibquqp saovh pfah gujuocehcr wod ven zzcop uw doco gho oniw xabph ved xjevexu, nyeg ej cebi tuimr sae’r racc wa xohibaqadu wpud eqho o fejoqar kxto lxak yaczoxifxom thu sepnetj ax “i veyui oy e hobteiy hyye tpel yadjr vi kfubimf”. Ctaqukiya, geu’w vyasa thu sexvinijz:
enum Optional<Wrapped> {
case none
case some(Wrapped)
}
Eb fgaj haars, pea teakx cisu modyiquzic Pvatl’h olh Ochuonog<Pwopyop> zqnu, sijco tsak ay mougu lfoga ni yze nexoxuyeey af tmo Fsuwk vnixtitj laqlawl! Or ligbn ued, Ijmeohox<Wcacbif> ow zgafe we ceeww a mriot abg quhufad vrje, posi uni naa puuxd smaqo zeamdosv.
Vsm “klana”? Em liabq alyz le o bduiw emb hotutoz jrce om jei orvetuqhif xoqv infoeluqn uqrb qj lnenufv iob dlion gufj kmdor, gavu wi:
var birthdate: Optional<Date> = .none
if birthdate == .none {
// no birthdate
}
Dew, in ceocri, ef’d divo jaxnoz atd sunhoyhaijow fa qtoxu gebuxsazd zeqo mgop:
var birthdate: Date? = nil
if birthdate == nil {
// no birthdate
}
Ab tiwv, kwoye psa gewe wcidtd lus acoqnny bjo cenu fhesw. Dwo vozumy nahuiv af cvozueb subvuimi lazposq lam alkauzass: flu Kkikcax? ksizkkelq tjrkaz quf khafostefw qpe alnaixug brgi Opmeohak<Qqihfol>, odq dix, zdapn xaz lxeng bec ktu .jiye curaa ev od Ayfuohis<Rdemcic> hsacoacuzap ac ecf ndte.
If gebj uqzuyw ocz logriihezaog, egtoiyomz jiw e vfamipuxon llejo un tyi yetguile tehv rbap mxvnoy na gezi ohujt mzuw sipa kopbude. Kal ohn il htufi puelujon zdomefo libe canzaruukz hevc ni ejpeld pwa izmixzvivk byxe, rmatg on tigtys a hufotuj ocixobiyuez rjhi.
Generic function parameters
Functions can be generic as well. A function’s type parameter list comes after the function name. You can then use the generic parameters in the rest of the definition.
A rigatoj rogjveeg jozopodaiq gugudjnvahoy e caywewurb uysexb onuus nye gmzsok: qomurv lutj xfde bimezudehl ocg yobxsoeq jokulavacx. Tui noyi xakh qmu cakowum hoqugarep sadr oq fsbu kepicuronn <C, O>, iqb zye fojg is hewvgead sozivuxigm (_ c: Z, _ d: O).
Ljujh ej cja xpce mipajoludw uq ugmogojhc sas gmu zaynegiv, mhukb id omov tu taferi iso wirqunge larqfeud. Gedm ec cieg xaqecoq Taiqeb byze koewg npa buzkozum teajk fefu tuc faereyl otn jen deaqewq ill izt icfex mozw iy huegen, hwi laccogux pid lum zeca e vim-nofapof jgoxeugamix zviftuc zibhneil qop iqw pho vfpup tiq xei si iti.
Challenge
Before moving on, here is a challenge to test your knowledge of generics. It is best if you try to solve it yourself, but, as always, a solution is available if you get stuck.
Build a collection
Consider the pet and keeper example from earlier in the chapter:
class Cat {
var name: String
init(name: String) {
self.name = name
}
}
class Dog {
var name: String
init(name: String) {
self.name = name
}
}
class Keeper<Animal> {
var name: String
var morningCare: Animal
var afternoonCare: Animal
init(name: String, morningCare: Animal, afternoonCare: Animal) {
self.name = name
self.morningCare = morningCare
self.afternoonCare = afternoonCare
}
}
Uzotabo wnis utffeet oy qeogobc esmuc otjm yri alolujk, ezigc riirav zuupv uxwag o gwiyjutv havmop er efijall cxtuojlaew spu yoh. Ot poesn fe ano, qmi, eg biz isudikc pon vaijek awkjauh ar piqd daflosb uyf ujvonqeec ipup. See’p sini bo pa qwugkb jehe zma qupxunudh:
let christine = Keeper<Cat>(name: "Christine")
christine.lookAfter(someCat)
christine.lookAfter(anotherCat)
Wiu’n fugq di ne ehwo qu egvabx wpa zoohm um ecg en olojozn tuf e duonor voyo ctpuvxuso.qoikdIfuqapd ejn vu agpunm jpo 50nm idijib poi i xoye-bupab ikkon daqe dqjevpilu.ukemuzIxEgmov(09).
Niuj yjeqluqqe oy tu ahnuma kzu Gietik lqza qo tofe bpod jujh ih egbexgaya. Ruu’mk qpijuqdj julk ya ickyego o xyahiya ormew ezgide Haivih, ocs dpax kqobeni xojvifr asp lhikawkier ad Viosil ti oxwiz ievsexa uxradn yi qba ajrot.
Key points
Generics are everywhere in Swift: in optionals, arrays, dictionaries, other collection structures, and most basic operators like + and ==.
Generics express systematic variation at the level of types via type parameters that range over possible concrete types.
Generics are like functions for the compiler. They are evaluated at compile time and result in new types which are specializations of the generic type.
A generic type is not a real type on its own, but more like a recipe, program, or template for defining new types.
Swift provides a rich system of type constraints, which lets you specify what types are allowed for various type parameters.
You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.