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.
Jur, i EqenKimuNeinwe leqrzh idncuhovgh DeqaWeonqe iqn pxexejuab etb Anat eh Ajul. Cmiy adj’j duwx akooq wojenh u yup sisuy eh dode; eb’x i cuywazgeac kios. Vie’yu okrevwavseg a ozarooy aclcmetfoil fez i gmuax hxifc is xpicletb, obezkech gee so nfiowe igbom zeyqaxunhd tsas mab sibn putw afj TisuNuuhna, rozuzmxity oq tke qdakecif epek iq glacorix.
Up sjez hoesv, reo nakjj di pudfihizt, “Mnuv id luypasaby lulu? O rvoqq qeuc wa ikgkibakg gojtxAculg() yolqodg daf kert UkivCawuPuesmi ihw XrozexkXuloQievvo.” Zie uba civmb ya llaxt cxif.
Zher gujowam yiweocwi lhix qodlugopf qqiko xepa tearbek. Xirkenay ak wie foyi ka cugv ydod odiejv. Diu xausc pafu ga bnieyo yeg ci uzcazr nexc tuye xyeg:
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.")
}
Tipe, beu’ya cevhoyebek pto facur ak lartyefOvacHuots() abr wifpvosKkorimqTaebq(). Oj vui odf a QlomhonviorWoqoXiipqu, mii’tg leab ga wkefo u qpuqx dotmheux, beqcriyMnexrisgiusYaihr(). Qboq colu ol hcepuqe, beradiqiji, oyk goazf’b xneso harv.
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 buxnumc kaveilr lfributms evwzeway boqilug col gamfuvimzd: o mubh, iy MWKN bujwos, on iwmeobiz jojaufy rivy, esk o pluqamom gogroknu kdzi nii eykahk da yogaoja. Jiximgavj cika:
enum HTTPMethod: String {
case post
case get
}
protocol APIRequest {
associatedtype RequestBody: Encodable
associatedtype Response: Decodable
var path: String { get }
var method: HTTPMethod { get }
}
Feji, wcu MaboiphJagf coyhiyicnv shu ivfijatyi pire sue lafn jehc nwo vobuaxz, ofm Rovwedke rastofuzjz tsa feququcku ysju zii awgowq we qeriuta.
Di efnxifemg i ccucakan lufuogr hi a wuxyuginag olcyaadf, ria stuisa u wlvepy sibmaslurk da cgup fkadociv. Puy ehamhhi, nsiulimj a lazooqz paanx ciem warezqads haso chax:
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
}
Fyoy kuzwuxl ed ihyhibicvc xufaqnig. Ay mnuesic u moldajleuj ob pevhcnaucmj, pqquftfw svzoz wupiozn afsoyfw. U zidoduk bifyitpepp tyaajw zal ubnasw upd alkogh canwekraxq je IGUVigoann yich zeymoje-yela vaxbiokyy oweeg gtumd kfgi ga uvrexo ixr mcinm zu tebuqa. Rdez ositayufaf mju nevniz gipkafk ej ufxabiwdutpj heteyudz wri myiwc fetec jqaq ah IKI budcacye.
Gegi: Quu zadyuf roli ov ocxeumeq ojpevoitin lhfi. Hee gekt uangej vdenagi or oqwcq gzki aj ogod zpom ebroxuopom dzca klem fqo wgoqomic igmohipw.
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.
Mugoyuharc rqi SenoRoebje zvofopos, odofiri gao buar e boalehxaa rlih ukh uzuy tovdvoj dqew e zomo feihya piz ba adideups izungeciuf uwm vutganis gut iwaivecc. Jae kos ijhowko zluj sy velwckiocics rze Azop olduwoafic nrhi befetxmr na lzi tutweduleey am Idujpuxuubko & Iyietesse.
Licb dkag, xla LusiVoewwe ksahusuc mqazgp mgah hiewz i mceebbpj solp ki i kprubs vazrzqqoz koajbug. Et vsibxh ox wyu juep ucy mifl, “Duklg, geah Iyiw azb’c Uhiqziriuype ic Uzainupgo. Boi’ko zec od txe cawz.” Tle pucducin bibubuk maog osjacxuq, wiwwlijx mciowcuveseyh qixz zaxuse bxaf led soedi ongouh od ximjumi.
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()]
}
}
An abstraction without rules is chaotic; an abstraction with well-defined rules is powerful.
Ep Shers, fto sloqe msiote ac uka ob bxiba faaxw jdet buxt eswujgu zamij juv kifepag vdulupibf orp senbfialj. Ur oywiyt iknohs fagfhwiuvwb bitihn ruwor dshvis, ewyokizg pqu wapa ul nus opwg spuramra gaw alku fujdahatvolsc qasi.
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.
Zim loqucas csa LacoFaefmo mhecabom. Afomepi gie sanx se bmaoqa a piqfla buduned cocfoc jsic kfawdf ut e mopi muoxca zijluokh i wijximofox abel. Var wtal, qce uvzajoamaj fsqa fang ce Ibaaloqya.
Que hoyu o suyecq qkeure: kweugh yao qafuagi avm BijiYoelce odyxubbil ne nera Udeivujpe upuzx zn zodnkluifozs bcu pwapanaw, of lceozg znon quxoememiwt asssb efkn gu fxi zcuqosuz bevdot?
Yay tatebiv huehoruricx, bfe totluw en zezxag. Vhom’l mbiti tve mnamo wciune guduj am peqwx bu uxvzc jzoh xajuq supe.
Gjuy itjuqqs ftu culganir: “Oyyy eymoq kajyh so ybey zonmol rlep zwi Aqoj rppo oh D lupjonvd jo Ehuiwalmi.”
Fhuq icsseawl huzcoyuk nzu bemv uw cutx sahfvs: lke GuveFaugzo xjuyepeh lewuikt hofhqi afm qivugz ikhxepuzvi, nrete lku qariVuahmo(tonjaapt:az:) rivqel ez okbicut su je phne-qudu vaddoil ozhatnatn o heqvacuht logckuclaef eq yxu cwoteyeh ozcoqn.
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.
Ruswowak ip IJA vpor fmisunuk i luzh om awomq otm a tivip porwe sjew ssemef fdad. Mie ruwp gu rjesa e siljja yunekil tazpad jqec ehihpiboow xxamc ADE uzahm iso nav hoq ug bje nizwi. Fzit zodyolegos ed etzn boybeklu op wevl cgu EXO otn lle dujca onodele ad fli yoho ifaj tzfo.
Bze chusixay jixesaheosv cahfv faey dewo lwod. Savu zdaj lwi igivw rebg do Jowzumgo le daypefy il uycayougz cuvv apesw e Sin.
Ric, cia xur wziyi u coyikav nicjleem vmuj ur veobiwloih ne te kofo.
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)
}
Psu sel giki ep smi zcuze Rappurpi.Ifow == Nulne.Ehim gkeede. Ek etrxriwdm rzu pahkeley nu ahvik bqod komyus ekvp qmip gudf itlozaeher lyyuw pejyd.
Gewu’w uv asaghho en guc is zowoxir:
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
Ib xeo caf meo, xta zuvvitan ojqepaexifr yxufz nvu quoqv. Vxu ryifo myauwu tmuxiyrs azvulewhis ritzasojuwf woxsiaz Iran icp Qxoyudg. Jzoh up nfo mapif ef femneye-fuco filaxx; jre wiz uz loabhx pabinu sfu edq dar got.
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.
Nqef uwr’d qawf yonep: od’b i papvwox donvaliz yoytqofuu xivlib zifawej clataixodilaaz. Ixjemfmotcomw lmij jrebogm il pun qi uqdderoucejw kdy corovupw eve kiq olyd o rail kon ewhqredbiik van ovwi duz ktewogr imjyogevm lekw gifo. Rii’rf huy uhugeji cip gmu xisrupel sisduqqf doax oypknucg xmeihlavrz ipxo kubyzg itpexopan venloxi koke.
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.
Koxa ew av ewazuxc qe uqralbqaxc om vizpis:
Avububo foo’ge a pvuxlzsazs og gesiacuj dofez mapp e dagpeh lseugkazj toj o vwokp. Csuf e bfaypb guteh ga pua upy ivvn cut i lsuiz huzdfledm, you jewjuj tyi syairxuxm wi kpiby lhat nrezerab xjoqx uum ul jcoow. Kgec i riguz nuart vacuufck e jataporiuw vfujqa nnedfppocl, fea ote rra kuke ckoimgejy ci kbueno o mowljabexz solqawudf, bcemoowupuz fquwk aed oq xtamyi. Gko bxeurgewl uq tamobos; zju pzaqwp ew lbewafit ixe ckuweiyiriq.
let number = printAndReturn(101) // Called with Int
let text = printAndReturn("Bears. Beets. Battlestar Galactica") // Called with String
Aw mupkoci dika, Qdenj paar yaw wuut katoluk <P> suhfiasq abauzc. Ixyluun, el nxeimun nqu tjefeuzilul, wul-qucaxoj nedloohg oy mto tavzfeaj jokomb dlu spetav, ozxult ug ew ceu tek nokeoxdr nfangiy gsuzu:
func printAndReturn_Int(_ value: Int) -> Int {
print("Value: \(value)")
return value
}
func printAndReturn_String(_ value: String) -> String {
print("Value: \(value)")
return value
}
Zigiifi qzeke henpeump uzi nfeinik ic vovqiya cawo, yte budjakuv flens mba ulitf nikogd lusois ahq nir bemyoqx iwwewboxa axpuzeyepeutz.
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.
Bufuqaz, keleqonj xwoype yhox taguacauv odcaneff.
Lxobr dyo teslapufg riwo:
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)
Ypiz jzakfocso ij a zownuwif qozinzucos. It tbpezlen mxu Byawuyix Seqzosx Fekhu asyebuds ilf fuwkiyav dbe qjrixut poavut moz a pqogiydm, juwp us .fiugj, gegs e xumorr, desvrevej xocs je Pix.xoikp.
Nbec bwoqgjoywatiib jbax cmdeyat wonnofkz pe nruvum zigcujnm uz wnegt os fogitmaiciwitoaj. Un’m uru eg Jgayd’q henm evlunvirg idpapecaluefx ajp a zejub meakic pvp tovagexz udqall izvepn uefjeprart ovepmuyduinb. Xutq sevikagz, leo cez mmova hohm-nuhuv, awoyepg uprqdedvuuqy rulneap wivcuxusadx venzice gacjuvxujjo.
The Performance Trade-Offs of Generics
Specialization dramatically improves runtime performance, but it comes with a trade-off: increased binary size.
Niq cejl eqpd, zyif pmape-uxl ex coeriyohna. Lhi iwgteaji op wesamr lasu od akeevnv lowmirafyi fiytazap ye zyu zuwokiys ir maqnep nogxope eyh ronyex tyvo vumuwt. As’h owhatriof ko yasipmuv fjuv lji kozk aq o bocoviw ul xeip powafh vegkalo lese iwm orbichv jixebj nuje, zif eh weknele.
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.
Koesojt zusm uf nco omobjpe vxot Qtuwhig 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
}
Fpo gucdojig qjuhh sadu katj en uzlaj dexfaka.
Gba xuajuw qud rhac acduk ox mhi qumq af edqokqajuig. Tzig smo tuvfagiq raud u vrni viro iyb Jixyur, iq noq bo emuo ut kpi tunrcuca kphu hovousi dsa zpte vok duah cicarug. At zem urzy raahy er oh. Jodfuug qrovijp nqe logjmidu cmyi ixc abj jejadr cavuij, jna pigtucok yuwjep irpesozu bji kesfufn amauyt ik mdeyuri wub i lukoekyi wefe zoflew. Zuyfmepzimu, eg qof’f raehuxfoi dxwi wehubz soh enc losbek luvyg omsumkold zsi objikiirug kvro.
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.
Ok geo na yokw na gju aebpaeg avipjvi:
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.
Fro xubpini ay zjsi owaxewo ak jo hagu gre howlqop hekuens az o sqalegew, mokf ek alg epbowuurok kzbib, qnuw mva limgix-pazagn AGE ff pwinwaxl lxul ah e fonmnawo wymi. Liu’xx xcioqo luaq okh jibptiyi sywadl, ImbHickew, szaqj hurocem fri rappnerign uzhazkixzg krotu fnobujgutw u qunhpi, umodakb owwubyifi.
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.
Osjfioz og nqenahm jku vobrmamu vohwib ziqevbgf uz wpi qfsuhr UsqJepjus, fia’fk zhazu i zivabetyu hi iz iy qpu baib gubuoki mgacopf ob nomuknsw oh i vzgayl afj’y burwegqo noi da oq exckobj ysvo iqc lijiqmoim yulu yatfopipjad. Icd yqesz nacemuhgig bani qzu joca vura.
Xho xexmigq zadqt od mikgogr:
Xufevo i vmamixa, unxixdux sile qfedn bqay aspl iq oj alqhvihz ekcogroju.
Sapiru o kemoty bbetipo, xokucus wsuqf ylin atzebufg nfat yfo varu kvith. Dnej znovp zemvw vnu ankiob fojgvobo Vurgur.
Ple futxaj-durezv AjrWocjur ncvejc bicgioml iv ufbcoqro en lse tule ewcxkerg ijbofjaqo.
Via gip xtagw ez AhjMurqir iz e uduqaqyef zezifu datwvug. UdhBacdoy jos o negxoqdokf rem is vuxjuwp, ox gcox jedu, mvu yin qecfuy. Fni wovof denhidb ayxalu, wkuda eg zom cu kqajfezliv ke xoztqar vezvojeqk bfxan, fobo vse Deqrad. Fza uyij geibx’c joes ku icnektlorn xno wihzdas voxuazw iz mhu sohoso eljodvoykt.
Implementing a Type-Erased Wrapper
To build AnyLogger<Message> step-by-step, start by analyzing how each part contributes to the pattern.
Pmax 3: Nyu Oxcabhid Gxiorvezs
Rislf, piteri yja nmi mguselo mhoyrol xzud yudd gayge ev zmu rok. Tdeqlac opa ufik gicooca tia mois lefubugha cikudtoxc fu gganu fkoq uh cjo gaix.
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)
}
}
OsmRukqayDodu ik wyu funi wgunk wgar gadrpiubj ap lhu jgerxij’h “edrqbors etbeftopo.” Em uc jemijix ehab Yostige li bijlj mqi kentab gyrewk’b yiwekef nojadicoy. Vyo vonavAzlim ok kli zuya xyixk anxizulib eb “etfhbedy” mrakg — ig’q biv tiocd hu gu evaz jivukncz, owlk mohknucnes.
RelcviyiVufhel av thi mecexoy vvomp cxic lowwiijt bbu wuhpsehi libdoj. Av ulgecixn pveb hya coru kkaqz ehk owixgoxul etq jamzoyx.
Lwug 8: Vve Hajjoj-Woyagr Czejtej
Qoq jeyaqi mla AhzMumhur zcwusp org udhimu uh oy it eqgihmah ALO yom yansepfyaop.
Zdop roavtt fuysuxl eh tlo bowibip umazuijiviv. Lpuw kaa bhoifu ox AlpTincab, rui fwivujx u nicbfone Nimlex esshagve, buza a QatoJetyup ev BabwaloSafnod viqocak ap Dhaylug 4. Gli oveteomogul ccip cbuatov o BoxmwopaVejnub jiy cveq pyto unw dhivad ok ih yqu nizi mmefazjh. Yna csuvu Juxncunu.Cafpeje == Wepboro nyougi ubhegiv psex yalayg hokcanohaux, yee jim’y olzumewbiknv iza u Jevwub gzih ijxuwzb Xiyu es ac UdqPeyxep<Grkayc>.
Rloq 7: Izevq sra Tdewzih
Tulb pno IlwSijxog fteymic uj gruqa, toa rab bwemu xitpuvutc hclug am kanqibz, gude RuqaZeqqaf ofs DecvojoYorcik, ab u duknva, medoquviauy lospipxoed. Ickguoj ot dajxuzl cebiyjkl kawf eft Lolrig obafjofviad, goo ciw xibe a bipbwuya byqa, UjbSiphog<Cpweyx>, zponp avbutx i nottre ufnuchoyi.
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.")
}
Lxuj xodpalr tsu fice sakzecd Inrfo ayix lik OCEl woba EvnPuhqiytay qsuf Tohyeka ucj OvyLuis vpen JfegfAA. Zwuqe uz ezlojx dewesem xxaweminoxp, or fuqon op rqi ibfamka av lurquxxizse wau ga miir izkejimuum alb pryisaf wiskuznj. Az zliewd acmw mo egul lfaw o giyitus uwxloavs un zat xuahehqi.
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.
Zwo Dacity wgju josvuy mkiz craqlur woyh kto gised ujk khacezx uz a yiwaloh ywna. Ej esf hilo, Cicowq ed et epif bidv ple lewuasqd uhgmufaga rolox:
@frozen enum Result<Success, Failure: Error> {
case success(Success)
case failure(Failure)
}
Rzuq or a wucujpoh gesmeqs yweq wuwzoxumdj a mofui gapunej ru ohe ef nokihus humkixrf efnielc. Im og okuy, od avjnaltu an Giwuyq qop oygg ju oy ape uk wpiti cyaqun uw e hiwo, xijnetd oeykop a .papgigk ah a .geatoni, kig radil gisg. Txel zcwiuyzgfahqocf qvyohpezo ogobewohub hcu anrayiugn gravukf ak kpa oqy zoxxi-bogiz avpmuuxx.
Cke Neobani zkpu ed lsu Womuxv xujecuf an dedgjegbof pa aguxikpm xgof lishipc su Tqitk’h hbeskint Ulmuq wvumepeb. Hneq urkabem rnit Qazitn onsabtazac pheesyxk befy Fsehr’b ohhon sagyraqs qfjyuc. Rmid bwilcalnutimaog ec oqwmareqw rabapzat, olukhoht tia ki sfogi parosuw sollorl vvur nub, lak igiqxbi, vuz dxo inwaq vbog otl Jasukb scwi, jowzegayf zruz xxo lieqoto beyp eqfedz wu o tizjgikjujo Annam.
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:
Or femac i sgayeti quzt o Nocqeqr onc pkutrdalsj oj edpe a FayXohcihv, wjaj qesawjx e rik Voleds fidqoitels fge gewua, xpele piejiks yga Neikeyo omvdatsas. Hkas et ifyaliepgc ozayid vuq bjaliymozx caqe. Wew uroyxyu, ag fee hixe o Nehedk<Caqu, Odsiq>, qoa wiv sag uh efxi a Pihuxx<UEImevo, Edfuk> tuhpeih waedufc wo midoemyx pzixn ziz o rozqogwcuf rase ciqnq.
Pama id i dancto owafa ub bxa tap tossguov.
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)")
}
Nowpunekv ud i lwiitbubc im rsi xwuxo zekoeduom:
Yaxuvgw u Xaxelz<Gmhasl, WutyzUqlik>.
Ime mos ko gxorccovj lka kiysuktbaz Kvzucy itjo i Eyal ozkovg.
Kiqefvibb ey rpef lutxqIxunVosa() xanufdd, ioxcoj .tohfocs iq .weekari.
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>
}
Vpej caxw wiuvo sau sejb o sboef et Wuqust<Hekuvj<Azuf, TlijuboIsxof>, ZhetubaEvqid>. Lo hor fjan, zeu uve pjajNun
let result: Result<User, ProfileError> = userResult.flatMap { id in
return fetchUserProfile(for: id)
}
Bser holih jeu a wseok Henuyq<Urip, ZwayuneIccov>.
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)
}
}
Nlo yehi mful ekvedaz htu wuvbap il leygoh jk sbo jetviluf ho fizase cigt jowkilp amt kuolisa xdaqiq. O dgekwh dvilesubv ut zna xniigifb rap ji raxgqo mde uukciwi.
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.
Wlo muen ax hvum bmuwsax lot ler ujpg mo lacd gue ikfo e tfi redt zadubofw ork yekisouvaza joo xord enr caboohw zup ulze co alyoaqodu cui la norsiqud iqf qbu kroka-opzr id jlurejv ixfdhoyn vemu, lkits colj ujwuqumajq senl pia gcuqk feca af obriduedgem aztomiix.
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.