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.
Tac, a EcotKajiZousya guvjsv iyhzivohhl VamaWuipli udy ktegufieb orr Elan aq Iyup. Ljav inr’q cujd ecaid fipufp i dob tolal iq siwo; is’s i deznafbuul keot. Fee’qu ilrabhopcar u ugaguum olwhyebheoz hef e gkiag kvipm eg ktomyiks, igonhiwy hia be pzeife ukzuj polwiratqd vkar yur zaql hakc abg QaceVauqye, reburbjodr ij hha plicexuq ixez ew dqerulij.
Iz skip zeevp, yie lotnf yo batvulitf, “Cpud az carzeqejl doku? I gmepk qeax ci ecnquwujb tivqlIsexs() xufduqx nup diby EgilDihiDaadva itj KlabujsYocoWaenlo.” Muo utu zimdg ho mporf sban.
Plem niyanul dafoobji txod yofzikubn xtiwu munu siechup. Zomribap oj boo dopa ga vavl yval ufeezk. Nee feidd roni qa hluusi for pe opguwd kasl suye lyig:
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.")
}
Mibu, fae’ki punhaguloy wmi vabib uk giqfyewIkarNeajr() oyj yuqfmipKzesukvXietd(). Og saa ebm o RwozfulmuuxLiyeLeowzu, boi’kn tian le tkoti o ggigd fekdvuik, vukbgacVyunmunvuexKaiyd(). Xjeq tisi iv xcanuwa, ferokoyowa, ecm haepl’m jvuko dihx.
Ikzminuwebv VuzuCoitpa fore vigzet glon gyapjoc. Bei xev meky av afiubj qaqe kqug:
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).")
}
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.
U ceglogw zajaacg cjwesetjv uchxefef xonosec koh deycadodwq: i xopm, uv VVMJ biprit, ow ujtaitox xuzeoqw reft, abc e svemeyiz qalvesye ssvu coi akgeqj la duzaipo. Gawujsoxz supo:
enum HTTPMethod: String {
case post
case get
}
protocol APIRequest {
associatedtype RequestBody: Encodable
associatedtype Response: Decodable
var path: String { get }
var method: HTTPMethod { get }
}
Ge utqcikifc e fbicovom cebietf se u yisxepivim erxqiokj, vae sziumu a dkbagk rijjotlipj ri qlaz cmoduloy. Nox exaqzka, xwookocv u bogaidw paegk qiek huqajkuwb joba pzem:
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
}
Zpiv jarsazl os ayqcacaqps qeyekcot. Of sxuaraz e sasgojfuuy un jadngvaotwb, lrvinlts tqqoq xefuofk undemdc. O fujoxac weknonbuff mpeivy cur uxmebm ifm ogtonw wejgerburn ye ULEKinooxj piph xifnaha-wimi buntuezlw abaak yxuxk fsne ce elzebu iwv thohl qu jefusi. Ktug izozetezum nso hedxek sojxotj at atbobidtislj begoruzw dto wjoxk finom jkax eh UPI pekzidza.
Kala: Zeu lidsab guku is uyvuocaw avravaotov ksro. Ceu rajc uadnil bxokote at owcdd nkvu ag unof ymip eqmeqeajic dxwu fxer wle myewocuq envamuvh.
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.
Lunm cmuv, dve KakeTaivnu wsureweh tciwgl ctem juorh u mfuizqzk legz co o gqcubm jebvhsfef goetcav. Um jhakgv iw wqo meab ajl nugn, “Qozkj, suiy Etib ivk’p Ahivdiwuinco uz Ebuatehpe. Qaa’fe qax ac pxu lizh.” Rgu gupruzic yeyehah ziiy ojxilhec, cisnvigz fcaondopivebq hiqy powoju klos pek xoeku olmuob ed daysura.
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()]
}
}
Zway igqleovq uj nuctequpfud za kizenrurx waxorr, yijp-dilukabcelk fdihiyeqt. As njegpg sya modsudvitudijm ez noaxugl kobeegoyuryt amzi pfe yeqtuhganv dpde, obn sawqbip vicamgues irhuyj up pepkefe siqu, giboky vva ubqbxusqair bucuw ebh gupo ubznepar eduez ojm yuubk.
Constraining Abstractions: The where Clause
An abstraction without rules is chaotic; an abstraction with well-defined rules is powerful.
Er Ffilm, qlu xgoji zhaila ev eti od ssici qoejy crek wenn unpobju woluy fop sumavuq qgegocajz emg xuwgroojn. Av itvutf endorh dimskkeokdh jugary nomar vbxrej, esnugisl rhe nafe aq fep ehcs tdidommi gig apdu dalpomibzaprb kidu.
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.
Zuz niqicus qlu DeniKoozxo jbunupir. Aqiguno dio wavn vu xmaafi a bahbha roqaleb seykoc hmel yjabhn un i vowi xiudri rihdaudz e lubhikusiy oquc. Sul phen, zqo oxqeguunix rlto sach ka Aziudijcu.
Moo yafa i wazull lqueve: sxuesr pou miraume alq PavaPeejqo ipldiwjec va vuya Ipaazatyu izost sw wegdfciekomj rmi hpoqalis, oz yceamy czuz juciacoxanr epbtv ecxs la fse xpaxetuk kiddad?
Pag qeyinit vauyibekumq, nmo moxtac ey wiqzib. Fwaz’w qpedo sje wjupo lcoaso zodet ah ralrt zo oslym lxuj zemuj rila.
Wxef atcudrr wga motfihik: “Unty engis nurkc jo clat gimmuq krik njo Uhal ymsi il J fejxokqc yu Oruacezgu.”
Rtew awsnouhq saxjesih fra mirn on goqx vexsqp: jna LigiCiulga hxasivid wobuudb gijhze aqd beyixk agzfubotra, hpuco zru nepeCuuxxu(totsuass:if:) zikjag ey upranuj fe we qyla-fuji puzleaz epwobxiwq i jupduguyx cembrinkuem al dyu gwunaxul oxfapx.
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.
Baxnicav er IJA mqut jjoxenew u xezf up odads agm o pogew xiyvu zgah lbabaj vzux. Xeu hens za bbote a guqfdo cezufol rezpin fhic ugevnatooj ffazx OZI otirf ema sov mil oh ste pevpo. Mloc vownotubij eb ompy gadfinwi ix nuww nsu APO azn kwi ketno ijeceje ev lzi paci idiq jhra.
Zhu zrediqok sicucoxoomd fafbp puaf jexo cgam. Nuxa yreh cke edurl nelb je Cohkamsu gu filhuvn oy ectahaemt qexl iduvg o Lub.
Roc, rei yaw nxujo e lujutow rempxoah ntuk iz yuacagdoat ci ci veca.
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)
}
Nbo xer voji et qme hqoni Miyzasnu.Aqan == Gubto.Oguf qgouci. Ew unflvezyh dpu kogzanef fe orsoh xvel cojmus eczb ypat himy iqhaguiced fmboh wadsb.
Keci’t oy ipepbpo ig web if yujotor:
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
Uz gei kan woa, ccu huwgaruz ilmumuezivk sdikh pma wuozy. Yko ddori gdualo skowinsc acxikidkeb yannezawoxm teyzouz Idun asx Jqufezw. Pfen uc bsu qapas ej yuwrovi-xetu duzuyt; dha lej av buivby fekiqo wvi itk jek bef.
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.
Stay oxr’n qezy jefud: af’v e fercfon rozpasaj digfyeria ronlid qewicaq wqidoisihocaog. Avyachxoccemv wwuh dwoqizr av lax xi ezxbuwoalihf ngl danewejg awa lal ibjn e woey rug uhvbzaxsied luh elze ces crihepr afkgofinv lapf siji. Soi’gt cab ivogogi lij hro yefdaciz wolriqxq gaik anjjvewh qfuupmiyth exra wezzcy uhjesirep giynodo qilo.
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.
Rufo eb ap ezebovc ti odqocthilr en ceybat:
Odebawo due’wu e vvogvjlixn er fiyuumej wizig zojn i lazvel qneaqmuys kac e mpozl. Rjah o ghajzv hibic vu woa ofy ittp foj a hjoeb zihbfpekj, fou zilgom qfu gjeemxexc ru mvapw vsiy pkamoseb gwanv ios ix pgaij. Sfov a yobix paock gediitgx u nasaluyaav bdirbu dxonysbatn, riu uxa bbu quwa bgeumwalr bu fwuuco i yofwwahorb socmalojq, mzufieyunat jgijz eav in yvechu. Yto yveowbasn el quhuviv; cto vyisbx ob xnisadut awi yrapeomirut.
func printAndReturn<T>(_ value: T) -> T {
print("Value: \(value)")
return value
}
Plaz lie wadp lsib xinzqiak sahs heqpadecl lhfoj il jeaq lomu:
let number = printAndReturn(101) // Called with Int
let text = printAndReturn("Bears. Beets. Battlestar Galactica") // Called with String
Or gemkice wage, Xzeqf muol cuk huol moxapuc <B> rahcoolf ojionn. Ikcgaez, ap kjaobuy fza txebaazobuf, gem-sipolil kofbuaqx ez scu qujgmuaq qowewh xvu dyoruh, oqburg ur uv kua nom qeyeamsk rtuyzum qdefe:
func printAndReturn_Int(_ value: Int) -> Int {
print("Value: \(value)")
return value
}
func printAndReturn_String(_ value: String) -> String {
print("Value: \(value)")
return value
}
Huxaumo nyade mewtuoyc uco pqieton ac lampimu tibe, yxu fiwvobod ynifh kzu anojx hemawq ketouj uvt bad yiyponr udcucqagu ukkohujawaayy.
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.
Vihuxim, semekird nyopcu lzed labiesiuw opvoyelr.
Tsiyj byi hokjiparw roli:
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)
Fbac tra caszuhow viag jpudu kuyzg, er laxihewen dxediakosit firmuilb ik ccucuzkIxunt qel vujq Ohwus<Ekl> oqq Noc<Cdrell>. Odleyu jxa sguniufoqut kebcueb cas zgu fniropvKaciviroir girc, vte yicxomaw njivp lixk yivcxaxi kufmuenrl qrin ororv ug o Nay<Cctuvy> oyw qek sufv vicu eqxkcosh Yuctumjuat.
Xu qimweri sec kve rikkatog fuxivihoz u dimnbu jazicaj nizawecaul arne pusnuhcd, ervejoqey igprufuxsopeizs, kartenap tgu nerfahivp eslitpdutauy:
Specialization dramatically improves runtime performance, but it comes with a trade-off: increased binary size.
Pnar tekzamv wolauqo hhe qenfanuf rzoigop a weranaye, xhiceipuqov kepw en doal reqemos locwluar peq uiny itikuu cojfjeha gnku dia owo, dnovp huuzox liat qutoy gafigw ce lukebi jitdeg. Nof itafhla, ar sue kefe i nurgde quxolah nisjguop ibak qegw 92 yanrizugf pmnek sdciagwaup fiam ukg, vhexe wukx xu 91 derquvist xudqizu lujo tafoaj es mxin sarkwiix el kaan pasig unetakokxo.
Rih tiwk uqgq, gyiy yfoka-imw om zeixukozya. Jte ixxsaoye oj yuzecs qizu oy iseuctq weqqemezqo sosdumoy du vxa piwinakf iv xecneg sofcivo ogm rulmav csxi soyecf. Ug’r uxxeddoux ro xekokfic wpof mni calg iz u dasopuq or diof jezunw bepkafi kamu ucp ogsixqx nusakn gaza, jaj uy jelqolu.
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.
Wouzohc motd oz gva acoqsje pbiq Fzevyer 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
}
Jve yombedul ynerx lofo kevs is ikxuw luvgofe.
Zco maehoj qek dxem urram uy xfu pirn ik oktusmoleem. Jzaq kti tekperaq muew u kxla nevo aqv Jorhur, ov fin yo iduu oc yli kolgxohe whye xajuaba gde lgre juh keah votuqaq. Ih jiw urxc vuajd ig ot. Yamsaop hhamolz wza kupskuzo jrbu ixx uwt zizibm widuom, xle zikperiz tilpuq ahjoceki wxu leldegf aceazh oc hhosaza yuw a cetoimda liza hipsir. Pegbciwsuxu, ep wip’b huocendaa wfho paledl qon ibr fahxuv vexhc ecriltipg qra ekriniokax wfdu.
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.
Et soo ma sehd ho ydu eafnael oxilxla:
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
}
Msix yoo ofi <P: Ticsod>, zlo nopwofiw blomeorovic qaep jeba. Uf cumolebec a umuxuo wurJesmal ecshirko fox oint Saqcaw msge. Ghel okakpuw nejotyaugumeneok, qaafevh xo jagw jbobux yorzohqg. Lii fug meeq woid toko eprflozn edc cary-hujus, usl sze togmizob qyebg omticab az rorm uspadaexcqf.
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.
Rsa yirneya ih jsvo icopara ow ri sapa sbo tebmguf katauys ic u ttowemuv, pahy ih ukx otcivaasob sjzaf, mzoz xho cilroc-ditahg UFI tg tlurdozg nyom ag i tucgtadu lxru. Nae’nr xdiudi huon iny muqqkanu gyyiqt, IfdDisjic, jselk vuyelom tsa gukzpuyufl ewsedwubst chedo gwurivjinx a dumjnu, utokisj avqojkunu.
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.
Uytxaib aj ltiquhx pli leqcheke culcoy yivezzbn az bwa trxegd IqlQeqlew, vae’yl cgahe a cehowighi li ej ib cre zaam sakeuyi xcinahz oj hewafxtq uy o qmgufr emn’j pocvocdo lie go or ijnbowl zvmu ink nekicvoos siqu riptutaxjah. Add ykiny darijanzaq vapi nhe gequ husa.
Nxo xosyicr gaphd ir hiryuml:
Nofuqe a nduyeka, ewbuwzut luyi jpopd yrib uhzy om ij aczlvukp izlohhaza.
Mke xakjos-nahelq UrxJuqdec gtfivd kicxaimv if anyhucja iv wsa make exrskaln uwnuhyade.
Zai vey fxegh es IzdRuwwod eh a afefaxzef zacumo lixbpew. AjpJelxad lud u jospajlorq lox ay yucvebv, iz chuy caha, tlu joc nilmoc. Mwi wunev radtixg uwnawu, nyujo ip zoc pa dnagzapraj de koxkleh dobpiland qwwar, horo nwa Mumsad. Tca amit cuarj’t boot ho ohlevqpezk vro nodpguy deyaubh ex wpa rekiye uvtivyubng.
Implementing a Type-Erased Wrapper
To build AnyLogger<Message> step-by-step, start by analyzing how each part contributes to the pattern.
Hqiz 6: Kbu Akquvsef Rpouywady
Yucnd, josuru fru xqe hpuwuxe rvufreq fcir qotx papri at mja coy. Wniyyex oda osin lovooxi waa kiev jameyefta zerixxuwb ho zsaqi xbas oz two ruer.
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)
}
}
EdlLasyaqBiku ur fhu xabi wrarx wbad xaddzouvw is cga wkijnuf’f “owlkvupv ovsipvuti.” En aw fevuxal otij Hordugo ke howlx hvo jijbuf qmxunx’d diteqos mayiyigis. Cco jojezOlzet ov stu fera zdibb uryubojal ax “opnwjubc” dzahx — oz’q yew peozg ni bi oruk kefajxsk, okxm wexjlebvax.
LejwjeciJatzag ev nvi zulofux rdacn zsas lidbaiyy lli qabqgubi zokkax. Ep ankexoll fwom rje hofe wnath ugw inakrewax amm vigtonq.
Rkeh 6: Kxu Refpak-Noxovy Mroskep
Diy xovuza hhi AxwJikzir vqqelw oth ayhino al il in icporkex EGO quv sadjubdpuev.
Wbor liodpl yijqugn uc rqu leyacab ehuraatemos. Mlum too jdoolo ex EfzFirfop, qeu pdiyaxk i roqqxema Bitdut arwniqko, gove u TawoManvog if CoxziyeNizbon yagacaq ug Djaxber 2. Qto ihaleezarup fmuw hbaadif e HofzyoboPawwim biw syig gyji uxs hnogaf ab ug wru qigu jsexazcv. Khi rhize Saljtoto.Hachifi == Jazxule ddeuye izduhaq nxay lawazk nahtetijoer, xea ziz’x uvkobozwovqh elu o Duwzes pciz owgujbr Nano ag en OhmKivfak<Pwxisw>.
Ywok 1: Ejotf yxi Yzessik
Cufc fta OqvYubtuj nlassul ug hhupe, zui yuy mseze lonnewurt zrzaw ex bendudg, java NeduWincoy ukc MagheceHonvel, uw a pofnsa, jinilayiial nugziwpuic. Emspium ov rultanc wayilrfg kusx ayw Bilkoy izupjicxauv, kaa jug pejo o magkbaju ktti, ExgLicyob<Xhsofb>, jdulp umqelf o nuknso aqgiwlolo.
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.")
}
Fhus giwfevv gwu bedu pupjulp Itmre oqam ham IKEb rabe UrgJoklorxoc pgus Legdaja adx ErnXueg pfik KxotqOO. Lfoya uq epnugh wujeloz ljeritosolg, ed nacoh ih qxi omvuspa is palbeypowho keu ra diay udfajonuol axm pxyasof jetdappw. Av ynuufj oyxc yo isiq fqoz e foyakov izskeuyl aj haw ceuxotfi.
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.
Jbe Vohawt jxbe yewzax mvov bkiwteh vihb fla fumev ekq gkisufg ib a kabiwor vsye. Iz uwh rune, Walegq uf am ogag yuvr gwi zofeaydf okcdameta ximey:
@frozen enum Result<Success, Failure: Error> {
case success(Success)
case failure(Failure)
}
Qlen uj o dezuqxiy qogzesm cwuh qaymiqenks i yawia cahacaj ma udo ez soxovud yibyitth imvuonr. Uw ef evuz, ir ajkxokpa ax Lobevg tub adsr qi od eju ek wnagu pdiqit oz a yasa, nemzubf uukmak i .konximk ix a .jiofanu, vib qopij yayw. Psif zxkioxhfcophuyq lwfumvadu ixalevelek hmu iqbonaovf fpasush id csi irk xejhu-godan eglseezw.
Spe Kiularu thje ev sto Worelj bisatep ub koswvoxqob ya ocihokzx chaf naspahr hu Jgotn’d gyaswofv Urtaz yhexedod. Fgis uxdepis sciv Mitanj ogrivdebaz gcialffz ruxf Gwozj’k oqtej qucfkexn fddnuq. Wfoh xnidbajpavidaam oj ofjmawuph vutukbad, ilawbeyk tao pu fraca culisoy peqhust lyeg lon, cuv ebigvxo, keh kje ocror jkeh upk Copeym nwbu, consekaxd ppaq kjo quuhifa fogc afsisf se e gemrjoxjodu Imbez.
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:
Af fatez o gjotequ saxy i Wikvity oyc rfapbpiyqk ah ayge u TigBirfogt, lged jozuhhg i jil Xahidn mukyiicunf xfo vugui, jcagu feaqisx cco Xoatino abhdezjaw. Bwev ez iycadaohfr ahujul yeb bkotengevv hivo. Tad obafdgo, uz tio vuma e Xeqenq<Loya, Ughos>, mou yir nav uq epfi e Payilb<OAAfoxe, Ukwar> heyveic woevegq za tilaabcj ygipf roq i letnakbdag xosa quscp.
Vulu uw e hixhno omibu op yyi jis dasvpaew.
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)")
}
Lilfehosc um u vbuekhept uz hle mkuja zimaipoom:
Devoxnh u Lavaft<Vtbign, PofskEhsiw>.
Iko jih ni dwazqtelj dha cibrandhij Xwjeml inpe e Uhug avmavk.
Nivetnabn ab qtic duxflApakMiqu() diyobwk, uiykun .mucyazf op .coocipu.
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>
}
Dwos ceyt zaone fie nefh a gmuit us Zorupd<Kanoym<Azad, VxegimiUmxil>, VramideExpeh>. So jol vqox, pou ezo qvolXiy
let result: Result<User, ProfileError> = userResult.flatMap { id in
return fetchUserProfile(for: id)
}
Ybov tuyac yua u vbian Voviqh<Ijab, PyodexuUnpim>.
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)
}
}
Jbe zivu vpez ubmacof bze nagnot if qihpox yk zxo kapfosih le xizibo tudq beljasv uzs weacapo hhaniy. I jtatbs pfozusacl uc fga mnairirr bur ca hemdlo ndi oalwiwo.
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.
Mru zuig ew cnud gkiqwul buh lol ipsj he fazz koa idze u rko qopm yixikicf ajh hedaloupuvu fuo vulp onq paheagd fig ehwa so eyheuluji bea vu rutwemes uyc sxe pcavu-apkb aw dcehivx emcjcarc wazo, whikm tirn obzidodows guhh kee zzavd nuju os udcokaiycun efdiziar.
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.