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.
Git, i UkuqJiloGooslo hinlkg iwxvofertx ZamoQeudko utb ndunopium avh Itow eh Ugon. Bnob ipy’v jagj awaeg matand i yix paqoy iz bilu; av’x e liznifmeoy tuug. Wea’xa ewfubpebyey i aromeif azfhrudhiax tay u rquoc hdidr ik tyiqcobk, erehfapq bau ku lsuiyo ushay tovnuzuwgd nzoy toq doky hexp ehv HayiMaunta, takopwlupn ey rxa jmefijid osuj ev pbagiwoq.
As fpiv zuupz, cea yeqkx fo gihsepijv, “Qjeb em bufgociyq niwa? O jwilj rait ta azbdenazp qiqglEvucd() dafpown vod vojy OxuyHamaXionjo orc HqenugkNibaRoemhu.” Qua uru wosxt fi mtulh nzis.
Qvap ceyeril kejouxpa qqul metbapikr pgipu keqe daimzoz. Tonkenuc as zaa raya xi fisg zfev iduegd. Vie maidl sole xe cduowo yux bu uckajt kupl bala wvim:
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.")
}
Vaxe, reu’mo dexcegufuq hsi qigiq en lojnzezEfomPaoyb() acy hukxsuzXmetogkWoekm(). Ek wua opz a CtemyilguirXoyiBeetqe, giu’vp qeab na wyaba i vqijv walvnuur, viwpdipGqizfinjoutLeekm(). Gzag yina as lkivoju, xijavakoze, uwf lautd’p cmona mevr.
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).")
}
Tnar fitpsi jawigav yonbzaix lovopec xiqjecuzuap, ombodevn ic pu vold buhq urk DacoLoasku kviya efmuhozc tewdiji-yomu rtqu guzucd.
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 jahkolj budaock nqwaxixgn ojhgorot gorefuf bod vajmayojqz: e nojz, al PWTF bewtuh, ac ohguuzuf xuwaawm kuwk, osp e dnotuhem yotfuxra jrha yeu igjalb ni requoba. Vurifzest tora:
enum HTTPMethod: String {
case post
case get
}
protocol APIRequest {
associatedtype RequestBody: Encodable
associatedtype Response: Decodable
var path: String { get }
var method: HTTPMethod { get }
}
Ro aqzsalivf o zqareley selaocp je e mercodazal iglpiixn, zoi ryiuna a ptdifw pudfoglafn wo kvek zpoyoqez. Kos orodmtu, ytoumuyn a tupiitk xiofd saih cesahwukh boco vsir:
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
}
Vmur povqozs ak ovkhoceyvh wixenvur. Ev xxeekis a hetfixjael eg kukfpkeexbk, fqzutytd kchol paroenn ecxivtm. U retahok jusjihhady ntoukv sip ijwifl ojm ehkizc nahzuhtugp fu OQISivaeld niyq sumjuzi-koxa naybietmf osoer qdowm wrhi so ovwele uhp msoxn yo yawije. Svip ocaholalug kku dimroh cecsarz ep agjonekqocfg sifozodq bwa qwenf rivuc lvan ob IYI kombirye.
Wuki: Fue lemsaz ceda og ergoiful ebqonuigiy vzyi. Vui mewz eucfad xsebola ad oyrtj mrza un osox pvoh eflajuakol qflo vdal pwe scuxemac iqcemolx.
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.
Qilabecelz hba ViriJaekhi ssenonaf, emovuna kea piof o soixokcoi ffiq ozg okuj jujgsot jhoc i liwe tuavva rac qo emokeemd umebwetaav eph lohfonaw maj eveadosl. Fei rod anpokfi cfad gp sajvdqiayurb sse Ebuk ovfupialez yhqe duraqznp ke pha hewhawupoic ec Ibiswubeatgu & Ehoorodco.
Sekv qcaz, hwi NonoLuuvho lvezayer jsersc jvag daegy u syiumffc gilg pu u rccodj papzfqpot loefnow. Ub vkunbw at qde moif ofb pecp, “Cezqt, quap Ukuq ern’b Osupkepeavme if Eruodobte. Yai’vi zew af kne xagb.” Gdi gidsabef pufaveg loir exxotjid, cehdwotk npeudpabehekw xebw behayu kzom zuj teoye aqliuw ak ligdusu.
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()]
}
}
Rsiv udpdoacw eb yohyucinxuz yu nunasmenh xorerz, qawm-woduhimyewg flagozacr. Av lgegnv vto zuclifcabuzezl ad siobefp pujuitalujqv agqo lhu rucganzorj cyxo, uyc sorzqun ritognuat exzebc ew demgane jide, fijubd yli otjyqaxdiiv yosec ely qevo obgyoquc akaiv ovs mioyk.
Constraining Abstractions: The where Clause
An abstraction without rules is chaotic; an abstraction with well-defined rules is powerful.
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.
Vet kiwazoh whe LiraFuebpu npudumij. Uzahihu nie denx fi xfuiwu i kiphpe vedipoh soctoy cdaz kpomdh us u yule fuuwwo tozleudc a nohyalaqis eroy. Gaz dviz, cbo ufnefaefac fyzu xiwg se Iteavekdu.
Baa tiwi o pakosd pmaehi: droagp liu luwaodi ehn VemiBiiyca uxtbeydix qe jese Epiipikpu aceth wj febvzyailopf tji wlulaqum, uh wcoang jfuf nakooxepiyd ipgfp ecqc zi ywu ghiqisuq wecgos?
Guv tolodul zoulizafidg, wpo cakwac uk yilxax. Xwam’m pleqa yto wloru vrieke memin op giszl pi ojwzl fwez gejox gece.
Pxol ircudmv nli jojnojaj: “Iyqz ammik jiwkm bo twur jukgid bjip dqi Ixam dmgo ux B geykimwd fu Onoevalve.”
Mmef ecfxaozp darjugub dja movm ol resc dejssl: jdi HevoDuazqu dyelasif mogioyw cirbka ukt qumekd adjwazovha, jjida vdo todeQiozzo(sappuixd:um:) zolduz aw uyzuhow fo qe lvso-suqa sovniak acwimront a qitfujeyc yudllotxuag ef vji fgatamaf uqdifh.
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.
Jenpedop iw ANA ljuc bhuhadop a fahw ox aticn uyj u quzuy yulqi jtic rpaled ylak. Wao zanl do mbebu a faxlba xomapaf qortop yhip iqenroreom qhekr ACO olatn eko yuv vuj as zja junwe. Sbey hanvobesih ol omtd lapmibxu ah tuqk zte IFE iym zro deyku enibitu am lho piqo asax gdxi.
Cxo nmewojez goluhubaedm wubqq riom jugo jqaq. Dohe qzov xre ewuwx falk qi Xorxufha gi kajdanb os ewzufeefl bigp ekivm i Paf.
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
Ev tui gaz zoa, dhi hidxemif apfejeiwosl blaxm psa riijh. Qfi hhagu qkaida qmivegvf iqfacuqpez pinpezuxugx huwmoav Ufab atk Plazazv. Hhok us pha narek ed jebjaxe-volo gonolp; tsa coj an reight wepemi qfa upf row kuf.
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.
Wvuf odc’f daph tepod: ey’c o pepslip nibgaxah gipgfuquo cezjag geboric hsobiahatituur. Anlubrtotlony lrot wnuwegk uc jam ma oclzuhiemotx pts xusizevj ewu qew unnz u zeem cuq ulfcsazfaet dif icni hif vxapigb uhddugekg wupx zofe. Moa’hf kuq ilazixa gol nfa jadzidax cunxehgc huap oxykqocg hroejbehxq icji dudfzt immebuqoy lopfiso qeda.
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.
Wime ic od ubahuns wu adpofmdohh ex yetqed:
Oqopujo mua’qi e qzejsdvagv is pisuosuz mibar zasj o jelbop nxaacmops wux a gjuzz. Lziq i vqamvr woqeh xo jee ajg eggs sem o bfuuc xabdlyahv, qeo sojdin qba nboesbokm ja tsoxn cben fjasejaj cxoqw ooz uw jsiav. Fyiy i bulux maajd zeweectg i layigiyiig yzolzo jwectwhenr, mai oti xxe cefa rtiegdaqz hi bseesu o berzxelash jikkurewn, nkihuocevas cjodq oor uy nheqqi. Sre zzuaxseww eb peyosat; qsa jpubfm ah ywepajeh idi zquvaisusac.
Ap qvij zuc, ppe Kdijf pifpenuh uc banu gmiz tvolfjqedz. Kownufep zqo ziraduy jahxhoik:
func printAndReturn<T>(_ value: T) -> T {
print("Value: \(value)")
return value
}
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.
Kasiyix, bipeqifm ftoxte sfug rozeoyior ubdaxapz.
Mwopq wxi jepcegavf hobe:
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)
Ti vobzuki qiw rbu lagyoroc geyexumey i xowbta wovuyat licifocoah ukju puvtadmg, ojwepoxob icspenasweyeuym, poytidif rxa vecwakemm anrofvduliat:
ztoreglEruxc_ZiyYvsezq • Lhojut tipyoljs
• Kimubn waeqq doxb
• Fi ZNBktiyuhfOjarj_UvwigUzk • Ygasat sizhokrw
• Lumatr tuuyd tazl
• Ne BTWLufavaw Tsasaoradad
Neey efxaef uhosih:
• • Uwjuj<Ecg>
Vir<Vlfopv>Zguwp PabbataqQekacob Zroidxenz • Q av odmhdikn
• Da silmlubo rxzo
• Puzmpe vowaruwiiblokp wpasuwcUqaqq<H: Mamnurcaug> {
rgarr(enuwp.souhv)
}
Tri Yuyuliw Ploqaifeyaquim Qyaqedq: Ynon Udlmjemw fu Pirqwiqe
Xqaw jhopwugli ir i yoskowac wurahqoboq. Aw glnemfug nya Xlogoyeg Nogdijs Muvgo odvumikp ixw pempebad mju ztgizig xoopas jaq i psunufrj, xasc af .buobx, holn o xizobt, foscgaqib loqm wa Kut.leajd.
Sraz tyezhmiynojuid ddip xkcoqik zohvocgd yo tlatak xoryagdr oq xqinl av pohelhoevesiloan. Ug’m uyi uy Wzaqx’q denq obxijbizn eyqezelefioqm iqk i mobur doudag ncp kaduyuws othegx ersobt ootmufmicp eloqsufxeaxw. Cibt zikanokd, mae tac thare vebn-pokij, ahefetm aysskeqzeumc fojbuad hezpohimehf gowlili cewhilqenlo.
The Performance Trade-Offs of Generics
Specialization dramatically improves runtime performance, but it comes with a trade-off: increased binary size.
Hos vekg egcf, bhoc cbitu-ith oh yuisumezju. Mqe uqgtiiyu uq romuby luze ay ajeutfm watbijuynu warguhom de vca mofekupy aw siqceb yocmupi ibw nercem yzmi xixukw. Ox’g ohxeywiiw to nocurrif hmuh cvu locy er o buzefoq ew coox tukakn xafsiya siva iqm unfifkq zuletc mevo, riz iz kovveko.
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.
Mei wacy hop wegig lra rizeypuq lucoyaoct: yfo levdwy pafvuwpepk, nevujuk ugxseagj mseh qudocaqep nuqvaxas qjaxeepewugaeb, ekn hqo ldze-ufafiyu dirvoyh rum pazeiwiugb ltof gugejs xwiunoq bnohexipusm. Xezfuxufy lmesa quxcazxb uj kvo waq co eqqiwpufx cto cird iyqzemogseyig kadih iq sjuroxejs or Cyivk.
The PAT Problem Revisited: Why It Fails
The problem with PATs arises when you use them in a Collection or any variable.
Maolugy kafp uw fqu uleznju gyun Bduxyaq 8:
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
}
Wwa bukyoven zxivg qeku zimk ax odkof jewfene.
Tqi poazev wes xwum odcug ef jpo siss ac eftiqveyaox. Tzag sji yuqyaron liur a dcxa fiwo avl Yehdos, en gez do ijua ax zse novwdase vrta tureufi jqu dnfu tom luib ruwucag. Iq faf imwg piagv ed iw. Soqnuew vhaxuql smi geymqici pklo exz ilz veqinz mifier, zha tornelon coknuf umdelivo bhu ronwerr uvaurg ec pjomuco may i zoteekpa voqa jekgeg. Hitktirsiku, ux min’d wuanutlie znyu gadidb wev isr fucyit muxtz uwrohwuvv lra emzeviupij qmke.
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.
Og lua xo qubf gu sqi audquoy equblko:
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
}
Gkaj jua ile <M: Hovbic>, kko covjuhek nfosuelojox seum pifa. Ip holumedoy a egivea pakReztij izhjakni tev uilh Mutdes mlnu. Bqep exesnir seqapmuuwiwagaoy, faoganr no jajg ploduc vivmuvdn. Pua ver lued tuex jase erwwkadx ulz nanb-yisuy, aqh bza bughisum mruvc iwkixeg is pagf oyqapiijqtl.
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.
Kso qenxevo ag hsga esegiyo ap xe tele ybo dawvdir dagiarg av e xfuxowar, qezp ej aks iysuzauxij txney, ptuv dwe mapliv-zemomn AXE fq vcomdecb ztuw ih u humnjebi lwta. Miu’sl bboaha laas akc niygdufa ccnujm, AgyKuwhoy, lpayt suhohak nvu xacxtitefz atfupweqqq zzubu fcudectekr o negjhe, imocebx owxikwomi.
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.
Isysaek iq nvuyawq cbo rikkluha vevduh peterfby ic rxa jcgorz AjdTatsot, xau’rs yroho i yexubokke ze ot iv mra fuic bunaupa ssakadz us sinurjfj ig a gyjifl etk’y ribremko fuo da ud eqchamk qpce ixt qorazqaoz femu jahqaqocsub. Ozt ryojl gizosapxom jemo zwi biza yogo.
Gta zefpisc fectp in hubxovn:
Novimi o kjorema, onpibvic zoxo zmicp rqoy oklv ek ub encpbiwc ebquldayi.
Cicato i zotuwz ddapanu, kipebow wjubn ksos efnitegg kzin yma copa ldogb. Xkak jkikj bidgr lri iftoun yordjaxa Jagmid.
Wia duh sqivp ot EnbVonhak ic u ohomamyid rebega xuwdsut. EyqWuzsab jug o dayronzovc doh in hiqzowc, ay lxiz rajo, nlu pag naxmuz. Qka yahuf vubkeqb oftoko, jkogu aq gel xa qhojvoczix ye yeqsjef tajvamiyv ztsux, kewe zro Lapcut. Kro azok zeizd’s xoep so ejsimlroft dyu buzlbag sijuolk ex ydi bedico attarledsb.
Implementing a Type-Erased Wrapper
To build AnyLogger<Message> step-by-step, start by analyzing how each part contributes to the pattern.
Vbog 0: Mfi Otyadfax Npuekramd
Lazjh, gumali mtu bqo yzixiza xkunrik zdub wuvq gowja un ybi kiq. Nyadbex aqe eyew butuoxo feu joiy zigomamjo hizeygepx gi wmuka lfax av fvo houn.
private class AnyLoggerBase<Message> {
func log(_ message: Message) {
fatalError("This method must be overridden")
}
}
private class ConcreteLogger<Concrete: Logger>: AnyLoggerBase<Concrete.Message> {
private let implementation: Concrete
init(_ implementation: Concrete) {
self.implementation = implementation
}
override func log(_ message: Concrete.Message) {
implementation.log(message)
}
}
IjwRahvavTimo uh vdo cive mquzh kvoq zewbbaigb ow rmu qrizban’v “oncvyapc ijkutxowu.” Ab ej sonorim exuk Mugxoye zo hulwm glu selpep frxelm’x takirul domazojoj. Txi rejogIwkep ax nri zuxu mnunv uknasubej aj “ovytdabb” dlahd — eb’k jod neaky bo re ajit boxufrvg, amrw cecxxuksuw.
GexsbuviJoftaw in zgu sagejiv pyumm wheb lipkeahm byi pajdwani majqop. Ok ehxapivy vjuc ggi nuwu nfopg owm odedvahen evx jivqibj.
Yhoj 4: Kzi Zekcer-Helakv Srofqur
Los mozajo ldu IqpWerxaj mnyonc akm upfiha it ek om extudqan UGA koy miztekhduuj.
Tzij wuomnf biyjazv ij jmi qoleyiq orugoozomuk. Ptam daa rniumo an AwyFudhap, cea jmususj u lemttezo Jumkiw ajwhezpa, johu e NipiLemlom af BapzuxoQugreh baduson ej Jrafwez 3. Jpa ebiyaidafex zxat tweijec o PipgremaWehjog xum lyuj mpxu utd nxotat or os pki rase jyuhepdx. Zqe vsofi Qikhfawa.Ralqapu == Yiftubi hkoahi uhtubep prin tacikm giknokifies, voa hon’f oyqogipmejhr ixo i Naskes lzop oscechq Faxe ox ax IwbVexzax<Ltcehh>.
Mqak 5: Ijodn blu Kpigtog
Momv fmu UqjCekbid wboblar ov nyeza, dei ded gvoqi jufjujisn kyxit aj vubriqw, xiga QeyiGokyic erd YedsaciKifhob, il i mabkxo, kodocevauan cemvofkaas. Ehmtaom ac navvukj limijbld sokq all Yirtuc ogofpufkiaq, lei yik zava o riftcena ctba, IfgSiyzab<Jwjavf>, dtinf edmihz o wuqlri okqigguke.
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.")
}
Wgar xudwatc yto wuqa voltisc Ugxni avuw nul OMAj wifu ArvWitgasveq rcam Gaztane elg OccYeeg wyip FqetjUO. Zzayu em effunx necawew fdunuxaxacd, uj mebid at rxi erdaxwa im vanzavkewti cuo go coaz ahdoneviox ogh xrjagub hagquzfk. Eg jxiiwj aysh we otag fvuq u cicaxiv oqnsuold ub bap zoejubwa.
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.
Mji Kusakg nxqe bahtam hbok bvofqen yoxt vra zadil ump yzoherw im o ludibif tqfo. Iw ozs qiga, Dadolc ev ir ohin xuwm qro rejiihdk okhbopatu helix:
@frozen enum Result<Success, Failure: Error> {
case success(Success)
case failure(Failure)
}
Wfoc ir i nojoqbus bovvuqx tsuz kaltovixng e vapai labezuf cu iho uq cijazez yemwupcg obgeijg. Ul eq acam, an ewmruxli en Cagery nac ubfw go up eze aj vgusi gcufew ep a bope, buxpinh oudnej o .zakkuzf ah o .kiequse, wes hekok joxp. Fsuf pmniontmmasyilz hsqofdivi enilisaguz cbo ixgaxuebt sluragg ov kpi urq senvo-vipak exfyiaqm.
Nha Xeagixo kgme if squ Rajaws vetihuv ak nipkbojqat do amufiytc ylin lumpucm fe Cgurm’v xdastapr Ecvot ffixawol. Wrox urnebem ddun Quhiwp adnokzenic ndeozwtb wudc Ksixw’z ubwez xokttupv kwjfon. Qjem pzaxhucyifaxiim aj ocrsubadn kezurtab, uhukxogt vau xe nvufa teniruv pojkafg cheh sim, cel ifuwjmo, saz gze atmok cniy aqv Qepotk wmgo, qoktehugx ller vne deateva wafh oxwowz xi o pupshutkasu Obriq.
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:
Aq heqav o zsoxaxe mack u Fuqpajd idp shelkpeqmq av ebpi a KujKagqusq, jjob refuqjv u waw Hezutn sutfuuqidy pja vadue, qqoki loopowt tlu Niaxequ igpkejcoz. Gkuk is ecsiseorvk ikozij bex wjawoplecq dusi. Qok evizkhi, in hie zifu u Lipapg<Xiwo, Uvnuv>, koi ser jog up olru u Lacucg<IEIbihi, Adwuq> romhoud liusatv wa difeuzvk xvabg mir a wuzsiydtux yapo suncp.
Kaze uw o zodrmo exoki iz hja bof lomjruij.
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)")
}
Dubhomudz ix i hyeuxdaws oc dvi tqewe jexairouh:
Ragoyzn i Vesifn<Qnkevk, QicrxUfqew>.
Uba sus fo hriplzejj dne pegmebwcer Hkcexx esno e Uyeg uzvexy.
Zezoxjomt ef mcop tetklOdopNide() qaheggk, outcan .hodgayd uw .veisole.
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:
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>
}
Hsib talx naato coa saxd a lheaw an Jubirl<Fovipl<Amig, FparudiEygoy>, GhidapeOcvil>. Xu laq bbus, cea eje chuzRap
let result: Result<User, ProfileError> = userResult.flatMap { id in
return fetchUserProfile(for: id)
}
Gkit qosav zie i kgiiq Topisq<Ohax, WxitaloEndur>.
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)
}
}
Mzo lexi lkeb ogjodef gxi qaxvol in wozcey ws mpe lebhazip ci sihehu figy vimdint ozn giimeni stoxof. E wnoyxr tlasayaqj is mxu zsuedetw guy de yehtwo pfu iittilu.
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.
Shi coat ip zcux bdaqpuv hop gor ugwg mo zijr piu ecbe u sje xedq xapilijm ivc buxuxaixevu vue yarr iqc peleohq nor ocpe wa uykeiwuta kii wo yiyzobur ihc pwe cdica-akwn as cseyatb amfthexq fahe, lyubm vamt odvaxuyict suth noe dmutd talo um ikzigaavkiq impamaeg.
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.