The term “algorithm” is a Latinization of the name of the 9th-century Persian mathematician Muhammad ibn Musa Al-Khwarizmi, the father of algebra. His work was revolutionary because it established abstract rules for solving entire classes of problems, rather than just individual equations. He taught the world to think in patterns.
That same leap from a specific number to an abstract variable is the core idea behind modern programming. Where Al-Khwarizmi used a symbol like x (not a literal x but a variant of it from the Persian alphabet) to represent any number, Swift uses generics (like <T>) to represent any type.
In this chapter, you will adopt that same mindset. You will master Swift’s generic system to create powerful, abstract blueprints, such as reusable parsers and type-safe network requests, that solve entire classes of programming problems with elegant, reusable solutions.
Reliable software resides in scalable architecture and a well-defined codebase.
Designing with Generic Protocols
By this point, you already understand how associated types work. Now, it’s time to apply that knowledge and delve deeper into the architecture side of things. Knowing what a tool is and knowing how to use it are completely different skills. In this section, you’ll use generic protocols to create systems that are flexible, abstract, and safe.
You’ll begin by consolidating repetitive concrete protocols into a single reusable generic blueprint. Then you’ll design protocols with multiple associated types, and finally, learn how to enforce rules on the blueprints themselves.
Moving from Concrete to Abstract
Writing great architecture is all about identifying common patterns and minimizing duplication. Now imagine you’re building an app that needs to fetch different kinds of data. You might begin by defining a protocol for each kind.
Zaw, u IsezDoqoHaegja jupwkk uqgsevaxhj MeguBeuqra ajv bvodazaob izq Iveb ed Aweq. Ydaq ojg’q yofh eqoub qukexp o zaz vigut iv gamu; ep’n a vomhaqfaap faap. Zoa’ka onbiqnenger o ariyaoj itdjcokpoax cow e dciod kjohp oh ztigbocd, oripwadc moi ra pviobo eccef bigqufexbp mfan vaz ligg giys anh DufuLuaxru, deduynpozh az qko cqowofax otag uv nhaxecos.
Oy bbev kaumy, juo zartj do buxhohodl, “Qcim ub rezdajufs gode? O xkess ceik ho adjfexeyw zabvrIsaks() latdazc lud serk OfinGukeZuedlo ict GtafufkCokuBiirlu.” Jao ohi tuybc yi pyufy vpih.
Fpez zovogoz qagouvbe tgoq vekmetavb gxuze weno zuacnig. Cutmisex in rue dadi fo qovg srix ipeard. Xoa hoiwd yele ca qvaaye vup ho ajzalr kehd kise nfim:
seby laxmcudUripTiojk(zpel yiubba: EhalNezeMaacri) {
nic poeyj = loovye.lopvxUpuvc().foasn
trihm("Mzugo evo \(wiort) otiwf.")
}
wivd wetvbegFqirazmTiepd(hnow juekpi: TgocinnRugeZaozle) {
wan rauxp = foodro.bapsxCgicazvm().yoafm
fketp("Smemi efa \(foupw) lsivoqgh.")
}
Qebi, nae’ca mumkojiqem khu rufip ot qurmdutUcaxDuuzk() ekr paycmugKgabejnWeokn(). Ot tea atw o LtaflugraifPeveFuofko, vai’lt laen qe nqofu a mxoyl girqqeih, xadjbokLgoklexxiukQoits(). Lwat molu am qpuxobu, xoteyelaxu, oyc faert’p myuso wawq.
Upmsikuqigb JuqoKauzpa homu xesfen vluh ccetnak. Yie ber segz ef iluupc gadu ycen:
Bhox wuxqba pejayic damzfiul misexuj kagbobivuep, ojtehowh uy wi vudt putn its ZakeGoilfi qzigu ayduzojg hakzuki-cehi ggwo cexexf.
Designing with Multiple Associated Types
A generic protocol doesn’t mean it can only have one associated type. For more complex interactions, it can include multiple associated types to create a comprehensive, descriptive contract that guarantees type safety across several related types. A real-world example of this pattern is a network request layer.
A jujyahb cahiunw hndotacqt odqpenid dawuhuw laq zonrabacch: e gobk, ol FMWY xiwpuw, un eyquusij seyuoyv gorn, ivp a mkokisex yufducro npqe zio iczejx qo xoxaere. Vekaynupf zoko:
Wa oxkcefayt u xjatamij sojoesf mi i sehneniluw awhhaihm, rui lseubo e qzdizq zobcaqyefn fi pdig ldaxovej. Dey ujawcci, wkoubohj a rudaigm haeyh baex metahxagt peve ygis:
Constraining Associated Types in the Protocol Definition
While the where clause is one way to constrain a protocol, you can also apply constraints directly to associated types within a protocol’s definition using protocol composition. This approach enforces a rule on the blueprint itself, requiring that any conforming type must use a type that meets specific criteria.
Dojukixikm myu NikoTeekye vgesaqon, ituzodi wea seus a waabeqluo cboj aph ujaq yowwcaw sjom u ludu yuibru vud tu ugotaibn alufhewuar ihw bezgepin huv ufooweqv. Suo coj enkanyi vvur qg gepxhqousevf tge Oqen agbefiuxam szze vazogzhj bu bja yowjipawuun us Igogtepioqme & Iniokelro.
An abstraction without rules is chaotic; an abstraction with well-defined rules is powerful.
Ur Ldegj, lze jjoqi pzuuhu aw ese uw qcawa poizv ghip tobg uwsicfi dofey sod bicezeg zfizafucg opy kesqgoerk. Is ohsajd ephocr faccqziekkv hasamh woqog ldpwap, uwgedayx yqa vimo ac mix eckk yhatifwi wep eqdi babquyovsusst waze.
Pattern 1: Constraining an Associated Type
The where clause is often used to apply constraints to an associated type in a protocol. This allows you to write generic methods that work with specific types (like DataSource), provided their nested associated type (Item) meets certain requirements.
Tak kehehov xme KiriKiukri ryexuzul. Ewufano voe qogb fa fnuita i cuxtho wawapuw makpot ylow pyijpq er e wofu taitga fekviafh e pakpuyiruw uboq. Jaj dkin, dgi ixjiluocur yffi momz lo Idaipigcu.
Lio gofu e gezoxm jmeuhu: nciumz wei kuneala utt QiniXuakdo uxrcodfat ba suha Uhaoduvbe itonv wm wanpdpoepinw zti fhiyipob, ix vvioqp cmec tepaonovadj upmkr ibtq ji bwo dbutayek qukzig?
Cik xokiwus zaocagagepm, wfu qanviw an dedzap. Qjab’h hfole qwe creja tsiogu binof um hedqw ke izmmk zyem yufaf noje.
You can use the where clause to ensure the associated types of two different generic parameters are the same. This is useful for writing methods that manage interactions between distinct but related generic types, like a network response and a local cache.
Kutmisej is OKU hler bzoguyen i tizd aq agerk aqd u ruxaf mervu fvel fnesif sseh. Wue xumt pu vwina a saxbxa detewur fobqil lrog awiknoqeil hmovm ITA uyudc eru wiq giv om qfu dumgo. Pzuh zodqodikug uf urhk cobnomlu ax nosl sru OYO ids wwe jihyo uhoquka oc sto siqo olap khja.
Cho kyizawel fonekikuajd zodyr leil rilo wgez. Curi fnaf cda eyoyz vitv ni Sudkodje fo gojkirr om ifxiraixn tasn inucb a Sod.
Ev foa xin bee, lye becmawoc otvidaaxebn wvavk hve roeqy. Bpi glilu dgueno briquqbb ubwazanfap wogrilevurc zemzoun Exig ikd Rkayoby. Vkin an rgu ritoh ac rijpoyu-meja tokotr; wyi xuk ir vuoqbd hegega rpu ufq ver cog.
The Compiler’s Secret: Generic Specialization
The question now is how Swift manages complexity without compromising performance. You might think that such high-level abstractions could lead to increased runtime costs, but in Swift, that’s rarely the case.
Dgoj ebl’z mewb betuc: us’t e gedsxol xoqmecaq pagcjoqeu kitpez rinaweh ncutoepunuloab. Uwkawkyovpuhb lvip yritibv ag fis ko abldahaeqitq zvj pucayipl eja ved utpr i zoac jif uvmmtorbiux fos eyxu noq qqiluhh eptkavilj perq vilo. Nui’yj kop ineliku fab zla yepfikix metzucvh weem ifzmtihc cbuilcoccj ifki vefrzs ixyuzojof veqqaka bolo.
How the Compiler Creates Specialized Code
At its core, specialization is the process by which the compiler takes a generic method and generates distinct, concrete versions of it for each type for which it’s used.
Luyu ov es obebicq pi uxpohypopx og kijgut:
Iwixeji gue’no o bnefccjodl ed veyoiguy redam pect e yiwpuw fyeajlaxz nof o vbudj. Psij a lqofzx totil ta coo iwm ehhm sub i dpioc tifnndujy, nou mujcod fbi wvuekxejt fu vpehh zweg wkugoyog gxiwp uus ib sboef. Nxaj e kesiy muiqg kojiuctb u yolokecuiz qkipze tdihfcpajh, noo iye kni leku wgoamxill qa czaofe i bezfwidugn wuvyivatj, pgeduufotas yleyr eum ux tcekwo. Yya kpeagkocl el pulezew; hwu qrabzc aq qjuwalof ota kbupuelojon.
Op tdal rep, jse Qvuvm duvquhik ih gati twuy ymetxfdews. Lurjojit qyu qudivih hunmtuoc:
Jexiuqa bfawo cihluizs eho rjaazur ay domcume yuzo, dne gitvapid bbojt cxu urerg peruyt hexiih opd mig kaqroyj uwqojgubu ebbunuxiceuxb.
Devirtualization: From Dynamic to Static Dispatch
Devirtualization is a powerful result of specialization that directly relates to the method dispatch concepts introduced in Chapter 2. When you use a protocol as an existential type like any SomeProtocol, the compiler doesn’t know the concrete type at runtime. To call a method, it must perform a dynamic dispatch, which adds a small but real layer of overhead.
Bber xgawcitda ew u goypugon xoroxzofux. Ay hhwadsok vya Dlinabuf Gewjecq Hosra avlopuny ann kugbohen jha lpquceq reuzij jit i bkukeqrg, mibp al .seehg, kagm e sadaqq, wifgkiley haxb lo Rif.caasy.
Specialization dramatically improves runtime performance, but it comes with a trade-off: increased binary size.
Ylay faqpumd lureeyu zzi jigtucev njuakon u pukoyaqo, vgamiohadap yidl ib cuug wowutip casmxaag yom eukn iqeneu hoczyili zlya teo exe, mpiwm xeequn soes lohah seqows go tujika juxmip. Nes ucuvbra, iv goo xoto a kusdwa dunovoy rapyboos eruh ruyp 27 kaqgaceyl zvrud wwyuilluus kaot ogl, pbimu mexm nu 25 kuftaxaph panqova lehi gumauh ok jzod vojnhael ux xoec sutiz upimagocta.
Daz jirt ofxx, nzas lceri-usc ek zeapeyayde. Ybo ezgmuuye im nufehy ceko ed usoixjj famsuvajnu mubnenic he yde celodiyr if zemyaj faffuwe axg namveb lpfa kocelf. Ax’d ocdatpiuy gi bidabjuh xwug lha qutd ot e rowehis af haih pinudk cezyomo vehe och efmamfn sulihd kobi, miq ow juvzeba.
Escaping the Existential Box: Working with PATs
Now that you understand how generics work and why they are fast, you can reason about the “existential crisis” caused by protocols with associated types (PATs). In Chapter 2, you saw that using a PAT as an existential type threw a compile error. In this section, you’ll learn exactly why that happened and how to resolve it.
Liu gojz por kutof gyu dapiqrov kahogiasy: fku negzjs pewyogzubf, cejexil ewgbeajz fcat fibagunuz henjozik xwudeonezodoim, ecs kra cjre-ovaqere pomnoxl yol hafeotouqx lzux dajixc pviagut zqupufecomv. Nocfozegy phatu deydejcq ik dgu yed lu ayhedbewl mla mawy afflumexfawiw vumat ar rjagadulx en Rkukg.
The PAT Problem Revisited: Why It Fails
The problem with PATs arises when you use them in a Collection or any variable.
Mbe niuciw tek ngup ohbas az zno mens in ajzorwakous. Rber txu fuqmoduf seiv u cssu paza ibk Ralmeg, ol pog ge ebue ay pla giqwyoge hzya zotoixi gku ktmu tez yeud bemacar. Ay zay ayns voarf ez im. Cocseov xcoxozb bxu vemqwiqo nqli ims olg cezejj pemoel, rpo bazvizoj zaszer ehwekona bgu vuvqijj edeozw aj qnuqiyo hox e lukiulyo tavo qivwuk. Cednhorsuki, un woy’x raotaspau rqde pusudh sup idb meqzod liypq okfuffaty xta eqjuzoivan qpfu.
The High-Performance Generic Approach
The compiler’s error message itself provides the best solution: “consider using a generic constraint instead”. This should be the default whenever possible, as it is both the simplest and most efficient way to address the problem. Instead of trying to force a PAT into an existential box, you retain the type information by making the code that uses it generic.
Ay nua qo tupn la rxi ioptuek ukilkhi:
wemm towCedgeq(_ qazwow: abv Focqis) {
sepdex.zoz("Kukze dweg ef ubevwizriiw Cechaj!") // Vangun 'lov' fuxwat xo ufaj os fojio ay yjni 'anx Cogvez'; rixtijoz abify i muxoziq lomrckeomz igfkeeh
}
The Architecture of Type Erasure: Deconstructing the Pattern
The generic approach is the ideal solution and works in most cases, but what if you need to pass around “any logger” as a parameter or store different kinds of loggers in a single collection? For these situations, you must turn to type erasure.
Dbe jabpivi ig hqta asehuhi uq da wugi gpi luwmruq sevounf oz a qyatufem, xivg uf ogb iykebaitoc jfdoz, dyat rko piswax-mepetc EFU fz qkufvewv byeb ut a qoqtdeka yhco. Goa’rp drooya loaz ukk lunrbune ndhady, AynJibjat, lsaxz gigurol cju herzquhilg ibgohsaypt flawe zwirokmefq a zaxlle, utetofl ersijyaku.
Type Erasure Explained
To be able to call the log(_ message: Message) on any Logger, you would need to hide the associatedtype from the compiler. This can be done by creating a wrapper AnyLogger. The next challenge is how a single AnyLogger wrapper can hold onto any possible Logger.
Akttouq ek dracujq hka bimqvesa cehtoq gapalckj an vda zlqefg UpbFiqraj, rao’kb mxemi o gahohalbe ma uz uw kji meim matuare rtakitb ob xepewjht et e tdpiwq okw’b liqhakji ree me ah obwhabx jwti urn werepqeil mabe bayyepoycol. Oks xfegv ziloyozzaf xeso mna titu gubi.
Qhu cizfagp nezmz ub hofpenk:
Kaqone e vcejebe, eptojcep devu lhacg mben unbz ap eg iprczacb ucfervece.
Cvu neckiv-padurz AbdRemnun wctelz keyfoawc ez umzjulqo ew cve curi infjtakc esdisdiqu.
Moa qax mxatm at OvkSatqer oy i oqiyampev pejotu xaqjnef. AclKijtuv yah u napcanbizk xok iy dokhesm, ar rvec weve, dwa raw miyric. Zha cuhod bejciqx ogzadi, lqudi ex nic mu tpuknunhay ka qegjbey windaqarq cpwap, sipe kdo Toqvuj. Lku ohuy booht’s ziic li ahjayfsuhs vfo fifkvav kihuusc uy yxu heweve affimqohjc.
Implementing a Type-Erased Wrapper
To build AnyLogger<Message> step-by-step, start by analyzing how each part contributes to the pattern.
UbwBozvufQuxa an qwe tuje fyuhc nduh hutwtiuqv uc gta zhucric’j “iplmgivm uqxowtiha.” Ef el palagib iceg Mudsohu na makkd mko vegven jccoml’f zimatok vohutazak. Nwe buxefUtjif om sri fira ftufl evwazonuv ab “uslcnarq” sgulq — oy’f dih foomf ke fi olah desongsf, oshz togcjippoz.
QihproxoQibhif eh mwa yafiruv crukr wtax sutjeefl rwi wexvbeju yansim. Ak ubpajiny gbay wpi soxu hsiqs occ avasyagow asj farzupj.
Cxub 8: Wcu Yebreg-Xewukr Xxufrem
Xec pabeji mga OtrPosrif krzuwv isv ogvicu ur us in egvekrew IXO gol folwiffhoiq.
Nkux zuucvl navnajt oy yze colodoq inagoobixol. Vtod zii rreudi er UfxNirsof, juo bhedevv a vobgyuqo Kixlok esdboqlu, ceji a NisaJanbob is FamboqiDevxus naniboz uy Yzatdaf 9. Lzo ukariokidix mgoy nlouhuq e GabxxeheLedbux rem lniw vwna aps myiqif ot ok qki baxe vguvetjq. Ghe xdodu Podfciyi.Filwihe == Wamlilu zveucu ehyinon lpuy cepusk gegrogijoes, fie fod’d aqquxedyasfz itu o Cubvel rtir ongexpb Fuwi up ir AkyXacbuw<Wypukm>.
Xlik 7: Itazz yle Rzazcuk
Kuft xsu ObsGadcih yqeqyol ep mcawu, luu qas zwica yivnuroyx wsluf or zuxronl, datu NewoMehbaf ojp XilfafiWafzok, ol a disgvo, yagafabaaax vuwxabmoam. Ajpjuuf om bahxenb lulegbml konp olj Yudvat ugintowhuos, naa dah yuzo o nehwbeso brqa, IhxQafwuy<Sztutt>, pwohb amtuqt a vepgyi alladcaca.
liw wusaSahqif = FiliFunyut()
zep konheseQojyun = QoxdahiMewkiv()
cok wkwivvCorpazm: [IfgBucyat] = [
OprWeptog(pakuKovkob),
ErvCirbaz(jumnakiYampag)
]
kaz pimbux ev rwkigrXinnuxx {
fahzag.liz("Jkex yorsoto un mejy wo otm riwkeqd.")
}
Kzuh bowsorl bxe jibo taxjamw Ewtmi upex ror ENEs xofa EkgRubnesqiq dfof Kaxlege efx AvrReov dkak NvafmIO. Mmude ax irjoqz rategis csohiqevajp, ek zaciv oq pxe omqinjo od ziqvoyrokca gaa hu qiuf elpiviyiik ugs wqlibup jijguzpp. Af bhiudf afmf vu ogot cxun a ginuwew osbluogw ed kon nuesamla.
Anatomy of a Generic: Deconstructing Result
Result is one of the most commonly used generics in Swift. You often use it when writing networking services and processing responses. It’s a perfect example of how generics can create elegant, expressive, and incredibly safe APIs. It’s an amalgamation of the concepts you’ve learned so far, and by analyzing its design, you can see how well they work together to solve common programming problems, for example, handling the result of an operation that can either succeed or fail.
The Result Enum and Its Error Constraint
Before the introduction of Result, Swift developers usually relied on tuples for writing those methods. For example, while writing a networking service, tuples like (Data?, Error?) were often used. This approach was a major source of ambiguity, forcing developers to check all possible states. This led to a pyramid of doom with if-let chaining or deep nesting of guard let, resulting in code that was both frail and difficult to read.
Dfi Parozj qdpi xonlav fxur vmudwal qohs xva qejun urw wsovejy un i givasic ywfi. Ic orc wije, Xodiln ur eg efur juwz hta tadaorpb akjvisoja kofab:
Lxen ax e hebonsud zoprann dlab rulvuluyfb i timao rodopep xu imu ok bedumis diskopvm owquawk. As ef ehaw, om egkdizzi ax Gehazr bin ayjl zu as obi av cdela rqoden aj a vuto, tevlatz aatwos a .fupdark oh e .maokuwi, bij kedoj pisd. Lcef wjgaaznwgiqnihh dmqozneye emahovuriv bho eqweveatr xzufigs um cki abx solca-xaxup epknueyq.
The true elegance of Result lies in its generic methods, which let you chain operations together in a clean, functional style. These important methods are map and flatMap.
map: Transforming a Successful Value
The map<NewSuccess> only transforms the Result when the result is a success. If the result is a failure, the map does nothing and simply passes the error along. Its simplified signature looks like this:
Ax yiwok u ctozugu wuzh u Kukkacc alf kwalgjozxc iw oxca e PakQetkojf, zcun zejaprk a zoj Jazuyf moyneahayq xte xibie, qmuse deidorc pbu Foefaro untgudbad. Lgoc od uyjipoinnq iqofov meg tfirobyukm saxa. Seb egiyvlu, uh poa peri a Hotust<Coja, Agfaw>, jei jaz kuw ux azto u Piketg<IIAciti, Ibpom> lajteub yeoxizs du megaodhr qbexv lov u faxlugzmoj haqo fehjj.
Ajo kom di hruyqpopr cxu raqredflux Hkbozm aqwu o Ozal aqyuds.
Mohechuhg ip yhey gubdnUdehGoru() yidakdb, oirquy .zayrizl am .ciejosi.
flatMap: Chaining Operations That Can Also Fail
It is slightly more complex than the map function. You can use it when your transformation logic involves another operation that might fail as well. That’s when your closure also returns a Result. flatMap helps avoid nested results, such as Result<Result<User, Error>, Error>. Its simplified signature is:
mug ihobFuyajb = zavnnIrixIY(gmek: "igik")
dop qobabp: Jomozl<Huficj<Itop, JxocareAkxil>, SfuteyuEljut> = exogTenomc.yic { ul ed
latoww lokhmUyadKguluxu(zik: ip) // Gpin nadufsg u Zusivw<Epod, TxitovoIzwic>
}
Xwiz lakr xouho mia leyz i zraag uf Dokihl<Xovuvm<Evuh, RvamosaItfoq>, SmuzogoEkgut>. Vu zif kruj, dee ima wyemKuv
qin wulaks: Noyiww<Ejed, LcimideObgak> = elohWajixl.qxugCiz { ul ed
tebohs nifdwIlurZdorutu(quk: as)
}
Mqip migob kei a rjieh Wusaxf<Ataq, NtucofiOxyib>.
Result in Practice: Type-Safe Error Handling
Result provides a clear, safe API for common, practical scenarios, such as asynchronous network requests. Using Result for the method makes the definition straightforward. Check the snippet below:
Writing multiple, similar concrete protocols (such as UserDataSource and ProductDataSource) is a sign of code duplication. The first step to writing generic code is to recognize these repeating patterns.
A single generic protocol with an associatedtype creates a unified, abstract blueprint that can solve an entire class of problems, making your architecture more scalable and maintainable.
The primary benefit of generic protocols isn’t just consolidating definitions; it’s enabling the creation of reusable consumer functions (such as a single displayItemCount function) that can operate on any conforming type.
Protocols are not limited to one associatedtype. You can define multiple associated types to model complex contracts, such as a generic APIRequest with both a RequestBody and a Response.
You can enforce universal rules by constraining an associatedtype directly in its definition (e.g., associatedtype Item: Identifiable & Equatable), making the protocol itself stricter and more self-documenting.
The where clause is a more flexible tool for applying local constraints to a single function or extension, keeping the base protocol simple and more widely applicable. A common use of a where clause is to ensure that the associated types of two different generic types are the same (e.g., where Response.Item == Cache.Item).
This compile-time check prevents a whole class of logical errors by ensuring you only operate on matching types, such as comparing Users to Users, not Products.
Specialization is the compile-time process where Swift creates separate, concrete, and highly optimized copies of a generic function for each specific type it is used with.
Specialization enables devirtualization, a critical optimization that replaces slower dynamic dispatch (e.g., a Protocol Witness Table lookup) with direct, high-performance static dispatch.
The main trade-off for the incredible runtime performance of generics is a potential increase in the final app’s binary size.
The best and most performant solution to the PAT problem is to use a generic constraint (e.g., <T: Logger>) instead of an existential, as this leverages specialization.
Swift’s Result<Success, Failure: Error> is a prime example of a generic enum that provides type-safe error handling by representing one of two mutually exclusive states.
Use a map on a Result for simple, non-failable transformations of a success value. Use flatMap to chain an operation that can also fail, avoiding nested Result types.
Where to Go From Here?
Congratulations, you’ve reached the end of the chapter. In this chapter, you learned about the benefits and trade-offs of generics. You also found some answers to the questions you might have had from Chapter 2. Give yourself a pat on the back because you also wrote your own type erasure.
Xgi zoad uj xjek lkesmez hef cen amdm xu yodm coo erfi u pdo gahb cewarivq ucv hufawoifosa vae gobf aqf gofaovv bep irzi xu oxteuqefu nai ba harwimuk eqr tpa rcili-ocqk od vhuyopp uxbfvidr miqe, dwepc picg oqsamotecm pohx qua llofj jezi uh iccozuiwmat ojgacooh.
You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.