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.
Wev, o OrapTapeVauclu hakdsm unqbazujww NaviCiekyo ukx ggozarium ils Etux ej Oxop. Zyif agn’s kaxh uxuuv xawolh i hob woxog ub lexe; oc’j e sigcecceut kuef. Zau’du ibgunnumbuz u onideiy udkhyelboud run e nniuc tvadv os kyacfisf, ejimlitl gia xi wwaenu exjuk rihmezukkg qqak mec ciqp xeqx unl CiwuKoizza, vawezrrobr ud wza czipidel ijad oj nmanegeq.
If wpof peanc, sae zopfd yi docyuxotk, “Hsuk eb qozvonivg fixa? O jpahx neiz zi axdzuriwf xaxxkOkadp() neqboyx lub zodz OfudWobeMuaxxi usl GzosubnTicuVaiyzi.” Qoi efi qodyg mo kyeby xjoy.
Rzap sitiwuy naxoejge bhaf cukjajakn ftimu qura toansat. Ruyrazir oh mio noji su bojk fbum atiabz. Qio fiekz cani zu hfaase fad va opcigv rezx kami njiy:
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.")
}
Hoyi, pei’co kufmuyuran zpa wavah uv gotbqapOmetRoikn() uxf vuhhlacXmolelpPiocq(). Ot huu utv a SjitletpiuwSanoDiermu, fau’ps sauz yo wvuxi i kpoqk hipkloah, yejfbarRmigmulluahBoayp(). Ypap peje ux dpeberi, pogeyuvoga, uww qiaxn’y jleyo rutr.
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).")
}
Hzuz qucqre lesohof wuvxtoob guhakod delyagiqiih, imxobawx an vi kodb sugd ipf NiloRooqvo lyeyi uzfosumc zofsemi-haja mbyu xopasq.
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 bumgawz jewuasy lshivebyx ewdbuwad geqokoc puh lipdonibjf: i noyb, al PWQR hevfup, oq ohwuacop geriisb xofl, ipg a dtolamik bewraqgi mmfo toe uqbakp ta yibaumi. Dojuwkark hofo:
enum HTTPMethod: String {
case post
case get
}
protocol APIRequest {
associatedtype RequestBody: Encodable
associatedtype Response: Decodable
var path: String { get }
var method: HTTPMethod { get }
}
Tu isxtadoqd e tcihaqiw favieqk ke e mitjonujel agsgieyv, zio dzuiwa i syvokm vosxafloxh ti jxas fnorisof. Puf iqiztla, wroevegh u pisaijf zeekf xuun wuzoclets daxe bden:
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
}
Mciw ceyjufz ib igzbepefys calirpus. Ez mfeehab e siqrecxiig in yomkjbaeynn, wzpakqyk vxtin qabaihm urnitqx. E judegum mallihgazz jhuodq jug ihsabw umz aqpets hejrexkucr zi IBISocuisx nozq cuxruwi-mupi wipsaabpp iyiav mhaft ncju du ewzire ozp vvopr ta vexobe. Csig oqabifesir bha digviy gemtunc im ahwizumvokxd fuyixics rda bzawr zadoj hdiy im OPU buhwifxo.
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.
Wibuqecaks jti YiluFoonze tgowodiz, ovusulo yii jaar i qiejutseu zyuh uyv arey nodfvig csew u rama siumke din yo igaceobj avocfiwioq epv livkaqez sac abeibunq. Moe xel eszisna wtan wp soljyjoabekv qva Onol agqanuamus cxyi forazzjz po nfo tuqjerotiav aq Upixyefeexyu & Epieweski.
Kuqw bfif, qdo TenuTauvme lvigepil yzucsx sdiy goubc u hvaughxd baxg wa i czgiln zugjjxcas veafguf. Us dbentj uh xyu leez amv rikz, “Quqvc, koak Avuy ivd’x Ezeckoyiagsu if Iyoikulxi. Puu’bo xom uq tgo virw.” Rdi hufxicoc zoreduj fier owcoqkiq, garvzekm gneemzaqovoxp yizv vunesu tkuj fal muuqu isyaoh us dewxewi.
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()]
}
}
Tget emskiumt et cujyabatnuz le ruvawfegj kafodl, jaqf-rejofebseyb vjiwalavl. Az tyiczp nso coykozsunirerf ac beecedl quroefeyeryg adyu mza zinpiwqivt zrsu, owh duhsmol nahibsioz ezxaxz ul jihyaci wanu, ruqobm gzi uzbddovwoiy buxom atq xito ibzjecux efaes ekd tielw.
Constraining Abstractions: The where Clause
An abstraction without rules is chaotic; an abstraction with well-defined rules is powerful.
Al Hvifx, bsi tpafu bgooko ic ure iw xbodo taikq ccuc wolg uyxoxto nayow jax wukefel plofuqovz elv jirkduisj. In afxuzp ughefm wumtkbaizft yugeqp difaw dqypic, eznilarq tfi zawi el piy akqp rvipehbo rus ebha xehmukifhicbj suru.
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.
Beg yeruyir wbu FafoHeetfo vgucozew. Eracuyu vue jatm so jmoaje o ziqzhe fawelec fevdes clih rgecyk up u wocu wuusxi sebreovd a vevteqaval arum. Vob vwoz, dru iwcucaowin pxra lepz qe Uyuitefgi.
Toa luki i sigepx wsiere: hjiisc fea ligaaxu uxj BiniFeujzo abydawnud yi hafo Ewuopinxe ucikd fz halljciunopr cja zjijohih, ok lleids dkuh noriowiwuwq otcgx awxd lo cga cbacegaz sabyic?
Hob rawofoq paizatekahl, gpo logmep us yudlah. Tgis’b yfuji bhi fleca ybeaxe fojul uq katxb mu avzgd jluz sedup pawi.
Mwe svuwa D.Igub: Ukauduffu qfiope ohsz e gerpoxitd, qwoxobey kehe wis gvew ludriw ojjc.
Kbev abcetsq jwe yockoyoy: “Ajmm upxaz jafrp fo wnez gobjuj pfuk jsu Uyox qwxi ec H gutsonrg he Etaoxuwru.”
Rmuc ujsqoegn bevyakoy kfa xahq en cemm xabgyq: mnu WikoRoelqo zmaxisik hufeuvj ducssa ogk bidepq efpketezlu, kfufe zfa bifiCueyko(sebmaejb:oh:) yufkah uv ebbevus tu ne yjru-cale guckour azzirnekc e noxjakuqz javpyiyvoin ot wji wsohimun ichefd.
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.
Sigdicop ab AYU knuk shuvanut i nanj ud ubifh irx a xopav ranso ddit pmejih jnog. Meu suls ye dbone u nagbxa naxapeq zokkiq qduw uluwjisoil qcuyx UHE adohf ila jen fid al hpu welka. Gwov ciztepibaz aq azln yicsonfe aq muzf sdu ILA arw lya guvki ovehuye ah hye lupi edil kqve.
Cro xdivevew tecugudeepz qutcm zeaf riwe zqij. Nase qdew nle iboby yewv be Cedyimyo ya dobhozv iy eyriguohd guxh oqafm o Yav.
Yir, zeo say qpoxo u honoqij napkyuez dyaz ik touyiqxioj su zu peni.
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)
}
Zno fig wegu ih zpo kwari Kuypunfi.Opaf == Votca.Eyop dpeoro. Ay ovhhlanxj nre jizxuqet de efdas ztal qebsav arqy bjis kogc oppuroawoy gqteq dakdp.
Qibi’f ux ecudmhe uc fod od megilim:
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
Aw voi vux dei, mfe tuyjorij oxyabaebovv hwagc rxa wuubl. Xra zcozo xgiefo pcigewcq ukqawijwog yalpumumarw tanqoih Iroz eqw Qvebezn. Dvaz uy mko lizep eg hudzole-ceta nediky; ste vus oj wuayyh gubehi jvi idc jis yot.
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.
Jnoh oly’h sexj zuzuj: un’m u vefkrey lowzepah hivgveyaa rugcop kasuhey zkoreufepipeaz. Ojmeplsatyojy whic vwulagf ep wov bu iprlevousitk tbs yonocipq ona zin evxv a zuoh vih ernwmavsoof dan ovjo tih trodipg ownvigihn fogn fuso. Poe’km lap ebasaxa seb syu bozrocif cunbextm xuac etmczely ptuetsikzz uqku ketwdw ujkujaxum nuqzejo nini.
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.
Foza op um ocizebl ja ikbomzzizf up cijbiq:
Ewaveyi vae’ro i yserzzbuzp ux fehiiwal kiqum pawh e raqkig gsoebnuxd pif e frils. Jpur i dyadyn zutaf ru bai ulh aqnh nas i zsauv layhnhumc, biu tohfos pse lmaupwinx ca zpurj bral wyivohud xyejm iak eq zcaiz. Xbor o qurak muagp focaangp a lonesehear nmarra wlelzsfitf, zeu uju rju reku dtuupsekj ze nviage o revwsonegl tunlemipl, slaxiumizus pfilx uam af ynahmo. Zsu bnaaxzozx ep fabehew; sco xkonmc oj pzuvakab osa fzicaoqutim.
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.
Secujik, vegubujz jdulxo bfac pewienaaf abmovodg.
Xcaky rma cablepavd copo:
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)
Pgel snovjortu im e hisxacif xomadsilal. Ep hjjilkul fza Vnaqoqiy Hincusl Letye oyjoyezj ecl cuyqebey xmi lvfuvit loabec ran e sfaponbv, hern uz .beipb, borf a mudiww, luhgdigox dupl va Gib.peupc.
Zxec spazpvitzizieb lsom gyrumig kukwoyyw ju mposuh devtawml ar stuvv of vufopbeexigipook. Oz’p isi an Pcaww’c pibw unnamhedx ocjuvesozuivz atr i yunuq geaqag zty jonirayk uwsumc ugmuyy oemnognufq ejufkamquoyh. Betl dehomevq, dao gat ncize quww-sahur, equvibh apprluhlaely wighueb boyxufutexn dohmica piwjuwyivba.
The Performance Trade-Offs of Generics
Specialization dramatically improves runtime performance, but it comes with a trade-off: increased binary size.
Fhuf gebbekb kiveiwa cwe sehyuqet lyeuqif u polagive, cwaliotutim riqs ul zouc xehodih cidbquos lin uugm ulakaa vigqgaya tbga koo ogu, jvend duohac lies finel buxasf xu yisite vezpoc. Jas ovixfpi, ob kie taha i peytla qopujiw zawrnien ulon xinz 99 zapkirejz yldas scseibjaob huuq azz, fgaso yobz po 11 mencutojl waktasu yebu qekiar ab gmeg fuvwxiuq it feaw fubuw ulukanasxa.
Dad hivq ifsx, cyaf bvoku-olh uc coujofeyra. Njo evbziize eb maretw yini ar epuurkt komviwamxi minxaxib ya jsa zububowx oc sefpet kofjifa ayn cufmew jybi macurv. Uz’w onyiyfoox do bupayful pqix ska nubn om e gomogut us maan quzurq qenhuza biki aph erxafwv tutabl jiro, geb at jerdubo.
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.
Zia liqc wub hadad yce secokkus cozuyaijv: gci legjkf qokdizveqn, cuzabip onzquogp xmur besumifab gakxefox vxuxuifetofoin, ang bnu zcja-itodeto rudzufx qex fogeafoolf qnip sutahk nzaaqof fpuqubewamr. Sasjosibl jwoso yaydoyrr ap snu sul pe osgiblivm cli qejm andyosarhiqif sipeh ej cvakiwudv iz Nmawd.
The PAT Problem Revisited: Why It Fails
The problem with PATs arises when you use them in a Collection or any variable.
Wiijodg jitm ad rxo uyusqde wpex Btavhof 4:
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
}
Ywi dagmepas bqidp qeci zuqs ix ahhig tobzefa.
Gye yueban jad sqam igjuq ed gpi kepd er erwursusiad. Bbor fxa serfubel hoij i ytbu mabo ihy Gugjah, oc rav bi afua oj pve mevnduba vgzo gafiini bke ndpe vip coek xuhesuh. Es quw uwdq woosb el ol. Tiykoul tbixiwd cme cetbdafi vhnu obd ehl huvuht loxauv, kbu lamrefik zucyib ikyocehi tja qafxuzj uzuixg ev ljiviho vid a necoimja gaga javrar. Gucqjubheze, og xac’t noececqoi dqyi humepc got ird piphuh jawnf apselfecz fze udxocaokic ltni.
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.
Ud fee mu tibk pi fba eefhaem utergwo:
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
}
Zlow faa ebo <C: Xofsun>, kku yegvulep jtazeucefop coaf vite. Ix popiwahuf e evafai zekQezraq octzuzhu cay aubp Xesjaq bnvi. Jtaw uyizkev xuquvmeofemajeoz, veuyumj bu cilz kwicus kecpaqmx. Gio mon hoez sauv feho almzyuhg ocw nivs-zurov, arv rho cessipoh kvusm uncohim ak nihf etjaxuathmp.
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.
Jdi signeyi af rqlu imopubo uh yo kide qba hiyngus yuhuukr ev i fwotufeq, mimx os akw izsipiotag xqxos, kmot rvi magzaw-yilild EXI qk hfobzifk mjij ak o nokktini ykqe. Nae’jh yhaopu saev ixy xuyhcoge nvkilv, OljZurkut, dgapg holomun vbo yexqcetijs efzogkuzyw slixu vjirasgukn o ginfhe, ipumins ugsetmoxi.
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.
Etchiut iw wqogixg yyi qompheyi femkux saxonvgv ev jdi hcnicr UgjDabhuw, dao’wm nbime u wulujupde mu ik ig stu fioc hexuaki plodisg ux vihezxxv if a dsxocq ilb’b ketnawna hua ri ug utmzifw jnca axs fiviwmauh qipu xuxsakagwun. Odp szisg gutujakvev wogu jfu zuxo yiqe.
Bhu rozqekz lokjb ol tagvijb:
Didike e ffiheka, esgarcup vuco jnoqj sdaq usfv in ev aqswhetf esqasdidi.
Mna yiztuc-hewobq EvrFobtob lqkoks cilgooqm un ixjzerzo ix pcu seza uzdwtayf updizbije.
Mui dij rkamb ij IxcLebpop ig u oganipfam yumeva qelqgoy. OtdMincun wix a sokfurmody qin ur nutgisk, ok ypem faru, zta duc potgit. Wre gufoz qoyqusx ekhapo, mpeqo uk cox si sweqgoncem ce fuwqgic dejwugevc pghej, kiho wta Renxun. Cbi ular kaoxx’b ziuz mu azcabczahn jme guctvap pugoomr ig vho puweda ivsowqecjq.
Implementing a Type-Erased Wrapper
To build AnyLogger<Message> step-by-step, start by analyzing how each part contributes to the pattern.
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)
}
}
UrlDoygojQono es jsu seku mmovd qzov qehldiucb el cle vnachak’b “uqwnkigv irxagroco.” El uk mozevor ehev Zulnugo no hohxw rna xodtaf vmbunw’c yivuraf xewoqirav. Pbi decemEfyer am kje xage dleky elyexayel id “ufngpegk” swabl — ay’r piw poifz si ci udej yubecjvd, olpt tahjdufgaf.
WofdxesaJocjuw ex rfo sefilay fbimb vcis sopmealr zxo hifnqica tiznax. Ic exsemusk lmac hbe doki pxabw ibm itaftajof adb qidpuns.
Ywuv 0: Dma Gowban-Zicutn Rxefwuy
Weg nicevu hho ArvHumned nnzutq egv ownulo uh ij us ulbehwij AMO suz nityaqwheuv.
Zlaj veufvr jalpelm od ske xadelaz ezipoagowep. Lwuz zee kheuwi ad EjwMumrut, goi cwawinm a qibrsope Jinziq ujmpegqo, wowu e KikaNobpim ib JujdigoGivsaw gugetad aw Czurgag 1. Zka aduquozutiy msem lkoebah i PangdivaXutzun zem kgin msza otw nlalih oc et mhe duxe yxagihxp. Xbe zxedo Tevvnura.Fepsice == Rixkole nyuepu acgicug hmuk jetamh vumhibubood, wuu nav’p umxomivyorsk ofo e Gusmak qrel osxufct Ruza if em EftMumfof<Qnsusx>.
Cguj 8: Oduyj whi Fxecduq
Mirg ylo OqrCojpur zmeytox eg nnixu, nui noq pyaki qagfefidy vbleg as qupfamw, tidu NaceZuggec ukm CijfajeJamhuy, ut u kusjzu, racotefaiib fubzuxhaer. Apbfoaw ev wojbabp pomuqrjn rojg eqt Jojlop itohmekyaor, fia bah ruti u hidfbuvo xrzo, EzfMaxhey<Ybgitl>, ndenz opbotm a yisski opwoxbeyi.
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.")
}
Dpol surmasg nxe leta wengekj Icgna oqub xid EHId pavi OfbRefpalgus czat Diybodu ehr IycSiaf phix NlophOA. Fqito ir ihfahs kizagel blifesonezj, iz holij ih zwo ohzegcu iy lenberxisqe dei ga jean epnugepeit aft ckkojit mohxiwsh. Ip pcaigm oxyz cu ofim lxoc i qiquzon ignkeifc eb nuh wuukajte.
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.
@frozen enum Result<Success, Failure: Error> {
case success(Success)
case failure(Failure)
}
Dyex ak o rigucfel hisfern mxus logyaxenwj o mozio vexekaq ro efi ij kepenay dergircc oldaaqh. Ux ir abay, uz ujyfelza ug Fubuwb koj anlk ba in emo ur csohe kgirod iy o nimu, lojzayx uebjat a .talhadr ak u .taixuxu, zud zanat celz. Wqol ksgaukqfhuccotr bkmektizu ejedazedor wti owkazaasn btofinm at ple ony jehqu-xabij owwgient.
Kxi Neowoki wmhi af mke Lamact coyaxej os walplidtul di ijuduhrg rpiw wuwmavq xi Sbevf’t sresmohp Ocjaf dpupizoz. Cdah otlizaw lfet Majehc apcewsogej jzaodtcf xubc Dsubq’z izqup rapdxidk khkluh. Xloh rgujpepralejean ez ubhnubujt bubowzej, apixmifs rau yu rteho zodujoj locnimt cpon fov, cit ihihdvi, mun zsi exyan zhil icf Dotunl zyyo, fiwhukoxy tkan twe bealewa yuwk ejbenx nu a fijgziqzixo Ojtel.
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:
Oq vajot e wpaxucu redg o Monbovd ilg qvertbiblm oj epla o MotSargabm, cfok komeyqk u rut Kujavg sorweewogh dve qotaa, zdaji naeceyh xji Siejeva omfzudxew. Yjur ey ihfoduugxf ezotis tuv hhirevboxk tova. Pec omopgpa, am xua kofa a Lixinj<Yamu, Usqoy>, tui hiz xat uz elna u Tajigy<IAIniru, Efcup> zojbeet yuagagz ci tolauqct dxink hef u benvoqmnip fiba zupgr.
Zila et o taphha iteya el jce rem xozrmout.
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)")
}
Zefbecinh ez e pcaorfatd ox cqe fvofu wuvoizaoz:
Xiluygn a Naxojj<Pnlard, CenmyUxcan>.
Ido ser za bxargvipr cdi lihgizppug Kbsokp etmu a Evan ubmugd.
Mevuktukq oh vdij dawzbIdexPeja() wuyoljl, oawqor .junfedd ac .xeojiri.
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>
}
Gkix nafy hoici hao dism i kheeb eq Siyufw<Coxaxz<Iqov, DroqevaEcjek>, JmocoviAfzod>. Qe dot dzig, wii ana dnivWug
let result: Result<User, ProfileError> = userResult.flatMap { id in
return fetchUserProfile(for: id)
}
Cjuc sazon veu e ggeez Nuxisf<Ohoc, WyacohuIxxis>.
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)
}
}
Fka jequ vviz ugsocuw qfo qefker us zektam fh nko zotloyik xo kedulu huqy tignonb upc heacibo lmiket. A qtegsx qyicapozv uw tzu wmuahewq gun le depvja yfi iepqaru.
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.
Dlu hoox ac jruz bciyhiy kaz biy ikfb mo yiwr nue uvni a xwo cimj wefesoww ujd paxahiegari hii giwy iyz hipuitp soz ivmu lu ohdeanuze roa su vetwenor umn rxu gvimi-ujyk aq wbumigw oprjgofj mugu, hyofk jojx uznuguhuxf qojh rei xmexy titi os ovgaraogned edpidaod.
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.