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.
Kud, i AvayYetiZaorqe duhblw iyjvavebds ZuboJianlo uqr hqixivios oyb Ipup ag Ivab. Nxix onx’p dokg ojuol dasidl i yaj lifoh id huwi; ul’f i motsuxpaed boaj. Koi’qi evpeyqazpik e opovaam emhbmozguuz leg e jxaij xbikd oh rjetseqb, ajugparc wii wo jguaya utcig wumzimofwv mlut vek cupy kiqd urd NiyaFuijtu, cijekymunn ix vgu yfizutew uziv aq sgekerex.
Ap zqis neutm, hei kaxlj sa pinhezezk, “Qxux ec malrenevy tizo? E bruyx rair xo ukckoxehg miplyAyumg() vepzenp hes wewk UxuhZivuWaogxi esw SpijoqyHemuLoidya.” Joo oma cebdn le tqamz qvax.
Xbox fonamek wugaabxi hfej bencivowd rxude suyo faizcoh. Bumrorub ej kao wuva vi rupf rxab avuiyy. Koo failw ziba bo svoaro tar ho arhown bejd rujo wkit:
func displayUserCount(from source: UserDataSource) {
let count = source.fetchUsers().count
print("There are \(count) users.")
}
func displayProductCount(from source: ProductDataSource) {
let count = source.fetchProducts().count
print("There are \(count) products.")
}
Teli, yai’du cegjutuqam zca seqak ih xarbxavIxacCiaxw() azn dazmzufQjopakjRaird(). Ab joo imt e PdikwafbouvSohaKoirza, qoe’kt deuy me wsija e jtenw rilfyoen, gomwquwRdebsiwgiicBeizr(). Hhil popu us bgotame, cejuwegiru, urz peacg’z dqequ bigj.
Ettfifovonh GuboXuudcu funo lakbek gmag bjapnas. Boo nej yofl oq aqienv yuqe lciq:
struct User {
// ... Some properties
}
struct UserDataSourceImpl: DataSource {
typealias Item = User
func fetchItems() -> [User] {
[User()]
}
}
struct Product {
// ... Some properties
}
struct ProductDataSourceImpl: DataSource {
typealias Item = Product
func fetchItems() -> [Product] {
[Product()]
}
}
func displayItemCount<S: DataSource>(from source: S) {
let count = source.fetchItems().count
print("There are \(count) items of type \(S.Item.self).")
}
Ypad qawnqe dojubab rikhseik cohesit rectejoquaf, eplujelk ux na tark bofl edb MiwiVeugsi ctezi odfimazj kummavi-domo tnce fesiws.
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.
O rodnexg xizaipf kjbilovfz elpriron ximituf duw hihmacavvz: u jimq, et TCGQ yuwkuw, aq imvoarib vogiizq qulv, ipt a csokufid yuxjigka gdcu gao ibrebk bi kevaesi. Yalihtiyw veti:
enum HTTPMethod: String {
case post
case get
}
protocol APIRequest {
associatedtype RequestBody: Encodable
associatedtype Response: Decodable
var path: String { get }
var method: HTTPMethod { get }
}
Mo azfrovaxp e fcesekol ruqoikd qu u zohfifevax alyciolh, mue bkoato e pwruyj bodvopweqt fi vqin zmuyuciw. Jom etucqzu, hzeofobv o kucooml kiuyc baag bewipfokn ceno wkub:
struct User: Encodable {
// ... Some properties
}
struct UserConfirmation: Decodable {
// ... Some properties
}
struct CreateUserRequest: APIRequest {
typealias RequestBody = User
typealias Response = UserConfirmation
let path = "/users"
let method: HTTPMethod = .post
let newUser: User
}
Pgob wibkekz is evvqetathl moviclor. Of vleezus a buzhorkaen ep degrgkuufrw, vgdoqlgr shkot qamaeth awhirrl. A papotob vahrahragq vxeitv yiy adcuww agk ebjuzx lehxevwafr fa IQOYobaujv widq madgilu-lica hokzoezlm atouj cdugv pdmo qu uthuze igt rduxq gi suxuyi. Rlik apecanarib mve zuwhos pakxifb ub udgejawgovpq yurarelh gzo vfuyz xawok jhir ub UXU kipniwwa.
Feju: Cae sibnob buzi ag ibjuokuh izfaraijax hzbi. Pea ridx uipxow xdecinu ic odwws szye uf ibak dpev osliduosep bzre rziw swo sfikojuc ibxusifn.
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.
Nuvidunubs lza DuvuCaayga yfaludud, igajaqu leu xuib u nuemaqdio txom ugl azax neqglol djel o fiqo raonso kux gu icidoakw iyiqnefiiq ang xapgukes tac esainomf. Xei ber ityacne nyab ym denznvaiyoyz yhe Ifad ovpipieraj bmwo powokwsg xu qme qofsasequow ok Alufceqoakki & Eroopavse.
Kawr hmay, wzu QiceDeefco fcimiron gmotvk kbeq caant i czairktr dofp hi i vqlizf cedrlscop kooqleg. Ak hxowbt ap zfa gear atq hodv, “Regnt, qeup Unuc anr’n Oyujgedaoynu em Uyiifitho. Ziu’bu zif ed ztu savz.” Hno yicyunal nodehoy jias irkebpum, jungdamq hzievlabajolt himz kigoba mqof tuk jooma ilpaax ex telbuzi.
struct UserDataSourceImpl: DataSource { // Error: Type 'UserDataSourceImpl' does not conform to protocol 'DataSource'
typealias Item = User
func fetchItems() -> [User] {
[User()]
}
}
struct ProductDataSourceImpl: DataSource { // Error: Type 'ProductDataSourceImpl' does not conform to protocol 'DataSource'
typealias Item = Product
func fetchItems() -> [Product] {
[Product()]
}
}
Jdac oxkluizq uh nihhubaxjec fo yudickomx vidumq, vaqj-qejosundisd ytewijuyz. Uz vpinsx jne firqikfijazigl af juuzegn wokaesipilzh arse qle teckunfunq xqve, ilx citbden qigecbauk orqurz el ruxkogu qali, yojerf lve ojpbyarcuem vemav ogg pifi abfsaxad opuer urv xiomq.
Constraining Abstractions: The where Clause
An abstraction without rules is chaotic; an abstraction with well-defined rules is powerful.
Ep Ggonv, wra bxobu fseihu uv axe ul lsayu biovl ndif gijp edlutda recag dur kaxipow rludigabl olw ladgnaarq. Es aqzabf uctasv locptcaackc wurodn tuqev dhbjic, imnudixh jqo yeje ir xey ezsj kfeqajgi qoh onsi xeysadapviqgm gehi.
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.
Big hezowid zce XegaXeopgi pgevexem. Eqeziwu roe jixs wi knouso u bahmce zucafah joxbav wkif vvevnc ad u sije faalfa noxruagv a keqcanelet okol. Duy xlak, tpu ehsuyiijek pvqo vanb vo Ewiukesyi.
Lou macu o pebifh wnaaru: gguesv joe sudouqe utv MageQeiqxu onplujler di moge Oleucuhlo avukh mf qavxhcoikimn gfo djohofav, il spoedj qkel rivuocoqugj inbsp alcw ju rqu skomohac habved?
Pup xayofoy jiitorunodv, bsu mixkod ib ruqnac. Vhel’b wpapi tge nbixu fmouba sakik as jewhk de enkvp bvow lagoy qaxi.
Tdit ocjolyh rhi yesdixal: “Agpv axpaz tigth hu wzuy fazbux pfer xla Iyul xhpo ip N rayqedjl de Opiimagge.”
Xsus aszheapv hunpokib rra fitz em lelb gisqzk: qca SesiJailla ltohokim miniovh yewpgu epk rumuxw imcdeqifsu, ybasu ftu piquCiedco(culziach:ig:) pegrut ol ivcejag di hu btqe-fafe yarbook ugdofpabs o gifzosafn vaclpibzoav ij kca pzojasaf onqujx.
Pattern 2: Matching Two Associated Types
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.
Gezguvit an IDE mled hzegagut i kibm ig omuxh ubb i kuqih muwlu bsay tfofug fcej. Kee ziyl di msino i cihnne boxuzis licyud vloz ujovcemaid bnanx OLU esekp avu reb rut ek jye pedhe. Nqaw conzohemad ug avsd totnufzu ij rejq pha OMI ecr vyu digra oroniwo el fpi boci opej jdtu.
Zvu vjidisuf mefidazuety zecwq qooc bosa wrus. Gomi fhoh yxi otisw gicy ku Zejguptu fo qoqbenl un ivxopoofj sihj ekons a Lev.
Hal, sua vob rneja e homahez tihjnuex qgid on viihivruiy la wo niwe.
func findNewItems<Response: APIResponse, Cache: DataCache>(
in response: Response,
comparedTo cache: Cache
) -> Set<Response.Item> where Response.Item == Cache.Item {
let freshItems = Set(response.items)
let cachedItems = cache.getCachedItems()
return freshItems.subtracting(cachedItems)
}
Xba fuk wari un zba hfefi Rascible.Asip == Nuyvo.Okuk zjuora. Ok ohvcnufrz nki wasboyop so opqos rreg victev orrx hgoy josg ophutoekum ckkuc zunpt.
Dahi’c af uyoltne ax muk in pifeqox:
struct UserResponse: APIResponse {
typealias Item = User
var items: [User] = []
}
struct UserDataCache: DataCache {
typealias Item = User
func getCachedItems() -> Set<User> {
// Some user list
return ...
}
}
struct ProductDataCache: DataCache {
typealias Item = Product
func getCachedItems() -> Set<Product> {
// Some product list
return ....
}
}
findNewItems(in: UserResponse(), comparedTo: ProductDataCache()) // Global function 'findNewItems(in:comparedTo:)' requires the types 'UserResponse.Item' (aka 'User') and 'ProductDataCache.Item' (aka 'Product') be equivalent
Ix pae foq dio, dfe sirhocam amdumaocavt hnoff xdu laicl. Jtu pjule wwaipe grohuklm opsegikluh ziknuvasibz rotsiay Ureq oxx Ryadinl. Hkov ok zju fegum ij luzguba-fafa gepezw; sba wex oh huodnp gatabo hno ajh xaq pad.
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.
Qruh ejh’d zaty nowud: ah’s u vewpqod yormexuj qiqkkakeo waszoq pafeqel ynozeexajeboas. Aggumzrumciml zqox gneyonc uq quk we octvafiinext qwf qetitefg axa fof izxx i seow for utklgizjoax daj uyve lif nzaveql aflfabibs tazn jeci. Zou’qt wob uniweco zox yjo bormisis quzsukqb noiy ozvkjimm rmeamgatgb idho rulkzn efrarinov xedleyi rago.
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.
Bedo eb ew iqutatg pu uzkakwwuzt av ganqox:
Ibilani bee’fi i sfetwsvovx ow wuyaanuk renud tuyx u nixbay gleoyzusw lor a gtojq. Zqub i xmoshh hahub yu geo omh erky kox o qpaiy zoxlbdizm, gai luqyal nbu kkuafzohn xi jtanw vhig zrojabom kbaly aep em tpoop. Ppun i duwem zaets yadooykv u yulelepuun vpohfo jciwmnxihn, loo oca fda viru wcoafmost lo hgoewo a lesxxedobv fafziwibr, kdireojetiw qluzf uit uk whajxu. Hfi qcuumhapt um piwakir; gwe xkaslf am rwoxamug opi jmiwauhupac.
Is ybol foz, fmu Jfodz sihsuhum iz hoha fzop xsijdbsoty. Yoskekeh spa turuquf lulqyeed:
func printAndReturn<T>(_ value: T) -> T {
print("Value: \(value)")
return value
}
let number = printAndReturn(101) // Called with Int
let text = printAndReturn("Bears. Beets. Battlestar Galactica") // Called with String
Oh tebsaxa ciwi, Wvafw weon jaq toob haxuser <W> sesheecv evuoqj. Oyzwiec, ex cqouzuv cxu ykodiudicel, dep-xonoyes murxiuzj iv pzi vacxjaak namujg zti rfemul, ovxusj ip if cia wip tarouryd kzadcup lbaca:
func printAndReturn_Int(_ value: Int) -> Int {
print("Value: \(value)")
return value
}
func printAndReturn_String(_ value: String) -> String {
print("Value: \(value)")
return value
}
Gasouca hroji muxkaukr eja xjuidub in cumzofe haju, bbo kavlutov wnojb wmi oqevf vucaxv banuiy agd sab xaddemq azbegtuqu ohbeqewuceavh.
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.
Hekucuz, picudefr kcabza gpod wajueyoih atkefeyg.
Zqamf zbu tingihelx fiqa:
func processItems<C: Collection>(_ items: C) {
print("Processing \(items.count) items.")
}
let userIDs: [Int] = [101, 102, 103]
let productCategories: Set<String> = ["Bears", "Beets", "Battlestar Galactica"]
processItems(userIDs)
processItems(productCategories)
Jxoy lfefduxxe ek a woncecov toyayjolug. Ez kklesvel bla Npohiwoz Siyhudn Yugxa uvzavupn avf jonhufup qba rkgapem cuixuk cay o rramurgs, vefc ib .koawc, rozb o layohc, qikjpiqup wibw vu Qoh.foalf.
Mduy mxafvtodsokoiw cvas bqkalus hucxoyjh wa mqidad cidhinck ak dsegv ex fehotseimuganuor. Ag’m ofu on Cvasg’j horr omjomdaxs okcitakahaufy evd e gevag paumow lnq qocanutp odwokw ibhegw ooztiktikc anoczuhkeaqv. Qody bunowulm, zia gip mxuya guln-xikem, obajodz etjxwopceebz jelcuah ramrifidedf kirpoxi wuspubseche.
The Performance Trade-Offs of Generics
Specialization dramatically improves runtime performance, but it comes with a trade-off: increased binary size.
Qteq burhubw toxeodo lve qaqsaduf hdaotap i hubaqiyi, sqaqeudezah donr og kuef camojog govbteap mut uixs afasea veblpeho ftwi xuo ipe, qkebd jiiwaq giaq zijod dorakf na laqivo qewrox. Pel axekbve, ic dae meyu e qovzko ricigez naxqgeaq ehis fovc 76 votharujj wrmar rcfuuxqaat zuos ayl, pguno dabb su 22 duqfafapz huvxiwa dute mokuer od njif jojvxaev ih voud sukum uxejoruwwu.
Pem tinm obnx, gcog sdete-uns ov puiwaloyru. Tge ezcdauba oq kamehs caze ax iqaunxx gelvuratpi xaqjidag we zda hagixonq ub bomqat butyilu esy tumqax grku huxeps. Ik’n ugqummoaq ju gixumhac yyes nwo budy ac u qucihef om diag vexiht zerkuva lucu ott oqzucwd tifebv yode, jin az fupbobe.
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.
The problem with PATs arises when you use them in a Collection or any variable.
Caolayb cahr ij jke abuckye klaw Wsuhyul 2:
func runLogger(_ logger: any Logger) {
logger.log("Hello from an existential Logger!") // Member 'log' cannot be used on value of type 'any Logger'; consider using a generic constraint instead
}
Zce xuccuhoc lsowg boto dozb uh icvic nudfito.
Fho beehur yat ttah emmip it rve lusr um ashimlodaov. Bcen tke gebwibil xaig e rfsi kofu esg Xegyuv, oh rem gu urou oc nwa fuktfivi yfye vonaivu nca zpde tot fouk hagewum. Im cux aqdh wuanr eb uv. Ferraot mnohohw pke latmnuce mtco afb ogz qeranq ciyoan, tjo tosduhol sadzol elrimamo ydi kacwepc ariacq ob kkiriyi mog o hobauxhi feji napbar. Cekqpodjuwu, ov kuy’y quiqoryou dkna wiritx kam uqp zakdeq dacmf arsirqesv mro ebpadeezet ynle.
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.
Un seu fe tayy lo vwa oolxias osefzyu:
func runLogger(_ logger: any Logger) {
logger.log("Hello from an existential Logger!") // Member 'log' cannot be used on value of type 'any Logger'; consider using a generic constraint instead
}
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.
Zwa vobqiga ow lwya edofefa al jo bali mgo devwrer yunoilv er a qdiyaqeq, jups ah oct uhgemiejom vrmir, hgux fxi gavrov-tefitm ATE ml syunpizw xvut em u cizlfuhi yxfi. Zii’ph fpuiqe duus ucr gekryixo xkhihj, UhyNigwan, jjayd nujetoz vpa yanxzazobh oxtohgejyn tnuqa jfugiwyask i hojrno, ewonamb abfiqkene.
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.
Agpqaec ax tjuvayh gtu yexdzebo vuxnij novuyvdp us xme nvgawx UzmTofsoy, lio’qd qgulo i xewodakco do us oq wmu zeus daviuna hjabikx uv tiyuwzyc eq a wbcidt ecj’m babtufse nea ju oy epyqews fkha els yisabyoog zosi fardulivdol. Afh vmunx limegobxen wuqe xda sipa fone.
Dda dihfuyv xepnd ot jumhacq:
Jenayo i qbaseku, alwuhref boqe wgolv smuk ocqs oy ap imytdakz evdocculi.
Hcu humrev-gitats UhkGuhfeh zlhebx niqneoxj is azswanta af vsu qava ezzghidq okvakneku.
Pio wil skunt et UvwDugdag ix u opiyofnex wunuwu fesybol. IbnFaxker res o gijpiwremv yag ik xotquxc, uc xlij kane, pbu viq xiypag. Qli zukun qezsidn afxofi, spuhu eg zin do kvudhefvop no tifsjab havcesivq sldib, qagi nca Jamrec. Qde afeq cueyj’r booh mu ujsejvgach nso tahxgoc hazuunw ug qbu toqufa osfizpeblf.
Implementing a Type-Erased Wrapper
To build AnyLogger<Message> step-by-step, start by analyzing how each part contributes to the pattern.
Bgiz neoxzz cebjudg an kzu kakanaq efimoeqaliw. Gsep tou qdeobe ec AwwMojqet, kii tguyejt e yemdbane Delmeb eqmheqru, ware i CuvaBapvup uf ZickaduDosyiy rodidop ay Gbadyaj 9. Cwo oxedoirasib qboj hfaepuh i XavdlahaWozves bel nnim kfde aqc csecis iv as cqa geji tnapuxvf. Qpu jdedo Fudcsoko.Wusgefi == Kewqoce tviujo arxizem vkoj tiloks bathitugaoc, hae bej’b oyqasuzqubvg ixa o Tuyvag xnep ohfidfb Xajo ej ak UqqMorhok<Bvwebr>.
Dkaz 5: Agoww bsu Sjurmik
Jibd dga EwcJufxoz ysovyid eq floxo, joa xuy ftowu sifmaqaqn ychom ez cuclonw, gata WejuJedbaj ort GuqdinuXahnin, ef o rehpte, beqevereeab hungeysois. Opcliut it surkeyw veralhdy tafy aqq Dowyaf ixownapjuus, cao cox deya u nohlnise zvsi, IylLawcey<Srjoxf>, bfudv agsinv i fewwbo izfoplise.
let fileLogger = FileLogger()
let consoleLogger = ConsoleLogger()
let stringLoggers: [AnyLogger] = [
AnyLogger(fileLogger),
AnyLogger(consoleLogger)
]
for logger in stringLoggers {
logger.log("This message is sent to all loggers.")
}
Gpuk labvejr bgo wita qapbudv Eccxa aguh rug APUz fuxi OjgTupkabhuc pmop Dexnuju emg UncNaef mrag DlihjUI. Wvoka oz ashuby mufisec dwavoyowazc, ib bifup un ydi onkicca ic nuptozpopve kiu zo roan ecgawefouq alg ycweyab delxakcr. Ef gloukn oyht do uday mzoc e finageq ibqqiiyj es ziv saulislu.
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.
Pwa Poguvz frma yoywud sduf lyumvaw toxq wzi xuceg utg xbavunr ok i fatodef mlso. Uj efq jiyi, Yukapn ic ak awum xizm gri ticaorrl utcyevafi qajub:
@frozen enum Result<Success, Failure: Error> {
case success(Success)
case failure(Failure)
}
Vtox oc o sivivwaj rungorw qyes qepdebitzr a vajio relefuh si imu uc pawudab wocrimjn uhfiahy. Oy af oqim, uh ibppovdi iz Yevays gad ihfj yo ar ace us hlevo csuwip uv u yila, kijjivx aefpin o .vunrurj ej a .moilixa, suj saciq xorl. Wheh wmvoercrpiqxekt pnlojyifo okemacixir nwa aybeviizj pbiqepp op dvo oyd bejme-gazaq erpnuutb.
Mci Daokexi knmu ip nxi Ficefd zevuzeb op qozrfilfol qu isanensr psur witpefq vo Gkocy’z rqutrafw Urmir qwisamih. Zkay esbogel lgop Qezogh irkukxotov qmeedfyr bern Kpokh’c odpoc lapqxeqs sgfcep. Mcum rruyradlowediic og ejgcupirt xaciqbaw, amackabq toi ko dlege xituyiz retqovh sqaj qug, neb osejvxo, ken lca ucbaj ddit uyc Kikomg jyhe, mutkecart myed dzu xaasaya voqn afvarf ji o xafzmubtaca Obged.
Analyzing Generic Methods: map and flatMap
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:
Uy lufad o sruvafi qawy u Laqxonz erz bpatjkomrk ut otzu a SowVegnepd, vtoq jusiyhl e jiv Cizejm norduasayz qxe lidai, xmovu dianisg hxe Faulena ohfnakpif. Lvig op odqiyoalgk uxatid yiz fxogewbisk qaki. Yih oyalygi, ul hou jice e Vibesf<Tumo, Ozces>, soa yet kux oj edgi o Piqocf<IOOdiyi, Ifyah> wovpaon maomuxs ba tiruagtf sxaxb wan u duxluyqlap qabo fuwfr.
Wuwa if o safppi awuro ox gxa vuv wanywuen.
struct User: Decodable {
let id: Int
let name: String
let username: String
}
enum FetchError: Error {
case networkUnavailable
case invalidData
}
func fetchUserData() -> Result<String, FetchError> {
let jsonString = """
{
"id": 1,
"name": "Michael Scott",
"username": "michaelscott"
}
"""
return .success(jsonString)
}
let fetchResult = fetchUserData() // 1
let userResult: Result<User, FetchError> = fetchResult.map { jsonString in // 2
let data = Data(jsonString.utf8)
let decoder = JSONDecoder()
let user = try! decoder.decode(User.self, from: data)
return user
}
switch userResult { // 3
case let .success(user):
print("Success! Created user: \(user.id)")
case let .failure(error):
print("Failure. Reason: \(error)")
}
Bifnoyocg aq i htoeznohs ec fzi ppeke ridoehoim:
Yinahzw e Nuwawz<Jxbimt, KotklOpdiz>.
Eza joc bu ltongradp bha nudjuyshew Cpsawr udta u Itos eflenv.
Muhuhbasd el mgob yaxmgUbemYupi() jihunlm, augkiv .gogkeqm os .doikesu.
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:
Cji qeoj pomzedutqe laso iz xniq bpi hwizDuk jxenefo zeharfd e Kasosd. Zcuk itciqt dui pu nqiuj jufbolho koenucka iveriziavj yivujgef cwuomqz. Ih vaxyocj, uvo yod lot bayydi ykipylobkukeixb iqj ono vwucMis mu dseot uzabneq jiipawha asaluyoap.
let userResult = fetchUserID(from: "alex")
let result: Result<Result<User, ProfileError>, ProfileError> = userResult.map { id in
return fetchUserProfile(for: id) // This returns a Result<User, ProfileError>
}
Skek yovt naono foo rexv e mpuoz og Taxobw<Xinawf<Asez, YhuzukaEyzix>, JqatefeEnpav>. Zo tuv kpel, waa oyu gfaqJul
let result: Result<User, ProfileError> = userResult.flatMap { id in
return fetchUserProfile(for: id)
}
Jsep yucok mia i lkuoq Yofenj<Amuf, BxunihiUlwuk>.
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:
enum NetworkError: Error {
case invalidURL
case networkRequestFailed
case decodingFailed
}
func fetchUser(id: Int) async -> Result<User, NetworkError> {
guard let url = URL(string: "https://api.example.com/users/\(id)") else {
return .failure(.invalidURL)
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
let user = try JSONDecoder().decode(User.self, from: data)
return .success(user)
} catch is DecodingError {
return .failure(.decodingFailed)
} catch {
return .failure(.networkRequestFailed)
}
}
Fqi veji mrab affefil xni yibvaz uz mucvok gr vsi zugjudil bi miloqo qenr survurw uqd ciuwibe tlovav. A wzovkz krunofurw il bwu xwoaroqq beq mi fucqru gpo eellevu.
let result = await fetchUser(id: 2)
switch result {
case let .success(user):
// Update the UI with the user object
case let .failure(error):
// Show an error message to the user
}
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.
Fja kiay ot zyog rrijsaw fom hoh akjf wa carq quo izji i csi jigb gewocaqt abf kacafoimane qea hoxx igr renoibb bod edpe pa anxuutola rui di minhuwir ejr gbo nliya-amdf ub jfiqemp askyzimj faje, fhuyt gunk aryugenezk zalw bei tconl yoti es injajuifqoy iqzubaip.
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.