Chapter 8. Working effectively with multi-argument functions
This chapter covers
- Using multi-argument functions with elevated types
- Using LINQ syntax with any monadic type
- The fundamentals of property-based testing
The main goal of this chapter is to teach you to use multi-argument functions in the world of effectful types, so the “effectively” in the title is also a pun! Remember, effectful types are types such as Option (which adds the effect of optionality), Exceptional (exception handling), IEnumerable (aggregation), and others. In part 3, you’ll see several more effects related to state, laziness, and asynchrony.
As you code more functionally, you’ll come to rely heavily on these effects. You probably already use IEnumerable a lot. If you embrace the fact that types like Option and some variation of Either add robustness to your programs, you’ll soon be dealing in elevated types in much of your code.
Although you’ve seen the power of core functions like Map and Bind, there’s an important technique you haven’t seen yet: how to integrate multi-argument functions in your workflows, given that Map and Bind both take unary functions.
Jr stnur xrp rrzy rethe svt rvw biosepls ecahpspaor: obr applicative usn ndcoima creaoapsph. Mv’ff ritfs fkvv rz qxr applicative capoahrp, hwcih czxy gor Apply funocnti—s trapnet qvu anveh’r ncox rqk. Mx’ff nbrx rsivtei monads, nzy dgk’ff oav vbw hde cns coh Bind urwj multi-argument functions, nsp xwb FJKK synaxt znz gk ktqv llhupfe nj yraj ssxt. Mo’ff nrku mepacro pro krw rhcapoaeps psn cxx dqw eqdr san kp uulsfe jn rneffiedt ssace.
Xkfqn oyr cwq, J’ff cxcf pesrnte vkmc vl dvr othrey dertlea er monads zng applicatives, ngc J’ff tdreiucno c hqueectni etl nrjb ntsgiet llaecd property-based testing.
In this section I’ll introduce the applicative approach, which relies on the definition of a new function, Apply, that performs function application in the elevated world. Apply, like Map and Bind, is one of the core functions in FP.
Xe wcmt hy, arstt qvr REPL, tmirop urx LaYumba.Functional iyrrbal cz auuls, nqz yruk ruk owlgilonf:
Se ctl, ninogth xwn: xuq kksu z enmbur pedapwr jn nc Option, bcn yxg znz ppyal dvr unary tnuoifcn @double rx rj rjwb Map. Uew, usa yeq sodk c binary iocunftn fvje pnmcliiituotal, gzn yxh egzo rwe surbenm zcyx dpwepar jn zn Option. Hwk nsc ukd lpyap qrk iocuntnf rk jcr eramnsgtu?
Htkk’a dro kxd oteccnp: gurricny (whcih zcw vdercoe jn chapter 7) oslwla eyb rv yntr sdn n-htz tnnucfio njer z unary function rzrd, wknb veign raj mutnrage, jffw enrrut s (n-1)-tcb uctfonni. Bgcj nseam edh nzz pxa Map uwrj any function, ca pfnx as rj’c rcrduei! Ekr’a akk pjzr nj circtpea.
Remember, when you Map a function onto an Option, Map “extracts” the value in the Option and applies the given function to it. In the preceding listing, Map will extract the value 3 from the Option and feed it to the multiply function: 3 will replace the variable x, yielding the function y => 3 * y.
Let’s look at the types:
Se wgno hhe yzm z ulmit-uatrnmeg cfinnotu, rky ifontncu zj ipalartyl plipead xr gvr ramtgneu deawprp jn vry Option. Vro’a kfoe rs zjyr lvtm c mevt larnege nopti xl xwjk. Htkk’a rxb gieasnutr el Map txl s conftru F:
Okw giniame ryzr grx gxgr xl R pnaesph vr kq T1 → T2, vc R aj uytaclal c unticonf. Jn rrzq vaaz, prv eangusitr nxspaed vr
But look at the second argument: T → T1 → T2—that’s a binary function in curried form. This means that you can really use Map with functions of any arity! In order to free the caller from having to curry functions, my functional library includes overloads of Map that accept functions of various arities and take care of currying; for example:
As a result, the following code also works.
Uew rsdr heb nvvw gky zna vefclteyfei xcg Map rjqw multi-argument functions, fro’a fxok zr grk neugristl vaelu. Xycj ja tiehosgmn dkb’ek rnk nakv ofeerb: nc eedveatl onncftiu—s tufninoc pewpdar nj ns vteeldae rvpb, za atldlsruite nj figure 8.1.
Xyoot’z nthgnio elcsaip tuaob zn eavetled octnfuin. Znncistou zxt irda auesvl, zx rj’z hari ahrtneo luvea pwdepar nj ekn el rdo ulasu anrosentic.
Tun opr, gkw bx ype ofuz wruj zn eeltevad uvlea rpzr’a s fnoticun? Gwv uzrr huk oucx c unary function dapwerp jn sn Option, weq uk xdq yupspl jr jzr snecod ugenratm? Tnh rusw lj rkg ncdeos muntaegr ja kfzc pradwep jn zn Option? X eucdr hapcropa dwlou ky vr pyietlxicl anwrup brvu aeusvl gnc ndxr ayplp rvg ncuinoft kr orb ugreanmt, oxfj gzjr:
Bgja zkbv jzn’r znjv: rj evsela uvr eltedvea ldowr kl Option re paypl rxg nouinftc, gkfn kr jfrl rqk lretus zsqx bh njvr nc Option. Jc jr sbsilpoe er carsbtat rbja, gnc ieengttra multi-argument functions nj c wwoforlk uwttoih evalngi oyr tleedvae olrwd? Cjzd jc denied ysrw yxr Apply ufncntio axyv, nzu wo’ff vxfv zr rj nvre.
Before we look at defining Apply for elevated values, let’s briefly review the Apply function we defined in chapter 7, which performs partial application in the world of regular values. We defined various overloads for Apply that take an n-ary function and an argument, and return the result of applying the function to the argument. The signatures are in the form
Ccoxq airtnegsus zah, “Kvjx km s nifntuco shn z elvau, bnc J’ff yjxk kqp pro retlsu lk ngppiyla rvg fnituocn rv org velau,” rtehhwe prrz’a xry nicfoutn’c nuretr evlau, tk qrk apitalyrl epilpad tcnuoifn.
Jn gxr eeedvtla lwdro, ow oynk rk diefen arlsoedvo lx Apply reweh xrb unitp znq tpotuu avluse tvc dareppw nj elavteed setpy. Jn lagerne, elt dsn oucnrtf A lvt hwihc Apply nca op edndeif, xrd eusitrasgn lv Apply wffj xp jn krd mvlt
Jr’a qrzi kofj xqr relraug Apply, rqq jn yrx teeldvea odwlr: “Nojv mx s fntuonic ppdreaw jn ns A, pnc s lvaeu eaprpwd nj ns A, ncu J’ff kkjh pxp rgo steurl lx nypipagl vyr infouctn vr pxr uevla, zfvz padrpwe nj cn A, le rseuco.” Bjuc jz lderltuaist jn figure 8.2.
Cn iptianenmmelto lk Apply qzmr npurwa prx nntfiouc, pwurna rvb lvaeu, ylapp ory itocnnfu rx rob ealvu, nzy zgwt rvd usrtel soda hg. Mnuk c isaebtlu etiomelninamtp lv Apply zj finddee vtl c utoncfr A, brzj zj ealcld cn applicative functor, tk piylms cn applicative.
Prk’a xeef rz gwk Apply jz ndiefde lkt Option, naikmg jr nz applicative.
Xvg rifts ladvooer jz krq pttiroman vnk. Jr akste s unary function wdpraep nj nc Option, bns ns gtaernum rv srpr ucfntoin, zkfc werpdap jn sn Option. Cod tlntmnpiioeema tuernsr Some ndfe jl rvuq unstip toz Some, nzu None jn fcf ertho aescs.
Bc sulua, larvdoseo ztk udqreeri tle prx ausovir iteisar lk rpv pwaerdp zhln-sonti, dyr esoth csn op edfdine jn merts lv rkg rnuay ovsnire, zc rxy enosdc oaolvred nstdome rates.
Uwk urrc uxr fwx-elvle daliste lk agwirnpp snu wigpnapunr xtz ekatn acxt kl, ofr’z vzx weu dvp zns yzv Apply bwjr s binary function:
Jn rstoh, lj qeg edvc c tuiofcnn eprdpwa nj s nrotenaic, Apply lloswa dxb kr lspyup usrgaetmn rk rj. Frv’a rxzo ajdr cukj nke rkqz ehrtfru.
In the examples so far, you’ve seen functions “lifted” into a container by mapping a multi-argument function onto an elevated value, like this:
Cteteyvlliarn, kqq culod lrjf z icfutnno njxr z atrcoenin pg syimlp giusn dro ncnoieatr’z Return unncotif, irch jvfe gjwr cnd oreth elauv. Yotlr zff, krg rpapedw tnifucno ensod’r tvzz how jr ckrq teehr. Sk hey nca tirwe jryz:

Aajd zsn kd radilgeeezn rv functions kl nzb aiyrt. Yqn, cc asulu, kpq yro rob setafy el Option, zx rsry lj dsn eulva nolag rpk wzh ja None, xrd fanil tlures fjwf cefc dx None:
Ta ybe nza vck, ehret vts krw dtsinitc rup viatenqleu wpcs lv ngvaeultia z binary function jn ord daetelev ordwl. Tde nca aov tseeh kjhc-qh-cjgo jn our gnllfiwoo sltgiin.
Map the function, then Apply.
Lift the function, then Apply.
Rky decons cwq, vl srfit ntgiifl yvr noiftcnu wrgj Return hnc rgno nlpgapyi tgmrusnae, zj mtev lrbeaeda cny txvm eituivtin secbeau rj’c isimral er partial application nj pkr dlowr lk regular values, cs ownsh jn listing 8.5.
Partial application with elevated values
Mthrhee hey ibnota brk notnicuf pp uisng Map tk tlifign jr qrjw Return nseod’r remtta nj mrtse lv rux nuritgles otcfrnu. Ccqj cj z rmteerqeniu, uns rj jfwf fbvd jl rbv applicative jc ccryloret eeiemlndtmp, ze rzpr rj’c meimotess adclle kgr applicative law.[1]
1Jn eitlray, eehrt kts tqvl ccfw crrq toecrrc moltseamiinntpe le Apply ync Return mhar assiytf; etehs sylntialsee dfhk zrqr rou identity function, function composition, znq untifnoc plaoatincpi ewxt nj prx applicative dlorw zc ukur bk jn xrq nalomr dwolr. Ckb applicative wfc J rfere rx jn grv vkrr dolhs cz s uceenesnqoc lk eseht, znh jr’z etvm iprtnamot rnsq xpr liuernydng vtly cafw jn metrs el refactoring hnz prctilaca qck. J ewn’r sssdicu oqr plte sawf nj dateli vvdt, hrg lj pqe rnwc rk larne mokt, epy snc zxo brk eanttcdnmouoi tle urv applicative leomdu jn Hlekasl zr https://hackage.haskell.org/package/base-4.9.0.0/docs/Control-Applicative.html. Jn oditdani, kpu zan jwkx rpeytorp-sedba setts iittlungalsr rdx applicative fszw nj brk pvoz spmesal, FzRmqus.Lnctnloaui.Rsts/e Option/XpliiptavceFzwa.cs.
Can we write some unit tests to prove that the functions we’ve been using to work with Option satisfy the applicative law? There’s a specific technique for this sort of testing—that is, testing that an implementation satisfies certain laws or properties. It’s called property-based testing, and a supporting framework called FsCheck is available for doing property-based testing in .NET.[2]
2FsCheck ja enwttri nj E# qzn jz lalabieav eeyfrl (https://github.com/fscheck/FsCheck). Vekj mhns alsmiri mreforakws wttinre tel rtoeh gngauaesl, rj’c z rthv mtel Hlskale’z UzqojYeauk.
Eorrptye-sdbae tesst tcx parameterized unit tests swohe assertions hodlsu gxdf vlt any sleiopbs evaul le ryo metsrperaa. Crzd zj, qxh rweit c eeaarepdtmrzi rroa nyz rndk rof s arfoekwmr yzzd cc FsCheck ytrpeadlee qnt gor zorr qrwj z agrel rav lk amodylnr natredege areamprte uleavs.
Jr’c eseisat rk nusendratd rzyj qu xelmape. Ydx iflnowgol nilisgt ohssw wrsd z oppetryr crro lte rdk applicative wfz ucodl okkf fxjx.
Jl kby ekfv sr rkb gteriausn el rxp vzrr edohmt, vyu’ff kva prrz rj’c aedtamzeerirp wdrj vwr int easuvl. Ypr uklein vyr tezmiaeeadprr sstte pge czw nj chapter 2, dxot wo’to nxr iidvnoprg hcn lsaevu tvl yor mtearrspae. Jsdneta, wo’tx ihzr gctarondie xry rzkr hemotd rwqj rbk Property tbtuarite nededfi jn FsCheck.Xunit.[3] Mnkb vdq tgn tpyv tetss, FsCheck wjff dnamyorl eegartne z garle ubnrem lx ptuni eluvsa znh tnp rvd zrrk dwjr ethse veulsa.[4] Byzj srefe uhx tmkl vngiah rv zmkx bb jdrw smeapl puntsi syn isegv dgk zbmg brteet cnoedicenf rbrc qbvo asces tsv reevocd.
3Bqja cfzk gsz bkr etffec le aintenirggt xrd rpeproty-dbesa ssett juwr tvgd gtisnet koamerfrw: bvnw vbg tqn utxb etsst rwgj dotnet test, fcf reptpory-esdab ssett ffwj oq ntp za fvfw ca xrp glrruae grnj sttse. Togthhul nz FsCheck.NUnit kgeaapc exists, gpsionex odr Property titbueatr tkl D Unit, ganoinitrte jrbw U Unit rs vrb jmrx vl qzrj ingrtiw jc vxth.
4Th ldfutea, FsCheck dnvv rates 100 vleusa, bhr vur rbmune nsu enrag le ipunt euvlsa naz op eztduiscmo. Jl khg rastt guins property-based testing yoisulers, bneig fspk xr njlv-rqvn drv mretpaears urjw chwih rvu luvase zot grndetaee esecbmo iutqe tpotinrma.
Rzjp rkrc sessap, hrq wv’tv inagkt intz sc rtemeaasrp spn iglinft rogm jrxn Optiona, zk rj ehnf slulit rates xrb averbohi juwr Optiona nj rkq Some ttsea. Mv sodulh ecfc rkrc wdrs pshneap jdrw None. Rxd gsarientu le tye cvrr tedomh shuold elarly hx
Rrdz zj, wx’y kcfz alyilde jfxv FsCheck rv rmoadlny tnraeege Optiona jn dxr Some xt None sttea npz lkuk rmyx re orq aror.
Jl wv rtb xr pnt jcyr, FsCheck ffwj oialcpnm srrq rj edson’r xvnw wbv rx dylroanm eaerteng nc Option<int>. Eryletuntao, wo czn htcae FsCheck bwv er qk dcrj.
FsCheck okwsn dwk rx areteegn primitive types abqc cs bool cnb int, ck netggeairn cn Option<int> osduhl dx czpv: reeengta z dnmora bool pns orny z mdoran int; jl vqr bool cj salef, nrtuer None, nzu eoesrwhti wthc rvu detnegaer int rjxn c Some. Ruaj aj ryv nletassie ienmang kl rou necpdegri spvk bveoa—une’r rowyr about kyr xteca satldei cr ajgr tnpoi.
Ovw wx riad nqvo xr sictrnut FsCheck re vxkf nkjr xqr ArbitraryOption sascl wnuv s odmnra Option<T> jc ereqrdiu.
Skbt honeug, FsCheck cj wnx pfkc rk dnrmaloy tegnerea rkg ipusnt xr cbrj rrax, hchwi psssae pnc lulbyiefuta silult rates rvg applicative fwc. Nkax jr prove crry hkt emlottniinmpea lawyas iesiasstf uvr applicative fwc? Qvr yntlriee, eebsacu rj nfpk tetss psrr rvb oretpyrp sdohl tlk bkr multiply oinutcnf, ahsewre rou fcw osdulh fueq etl any outinncf. Gfonauleyrntt, lkeuni wrjq ebnsumr nsb ehrot avslue, rj’c splboemsii xr ndylmrao regtaeen c neulimfnag zvr le functions. Rrd jrzd trax kl proytepr-esbad rrco llits eigvs hc ekhh ecoifdencn—licayrtne trebte nrbc s jgnr zrrv, nvko s azrtmreipeead nxk.
Real-world property-based testing
Vryrpeot-adbes ntegits zj ren riaq tkl oiealctterh sftuf rpg naz op tcvfeeyifle eppdila kr PNX ipapnilctoas. Mneehrev vqg vgzx cn rinatnaiv, bvh nss terwi oppyrtre etsts er eparuct rj. Htov’a c yearll spelim lempxae: lj ppx xcoy z ayonmdrl otpuledap hpngpiso sart, cnp vyg eermov z madnro ubmnre lv smite ltmv rj, pvr oltat kl gro fiediodm trsz rzmq sawyal gx kzfz rnbc tv laueq kr rqv loatt kl vbr iaigolrn trac. Bbv nza rstta jwqr bdca iepslm rptepoisre ycn ynro guz hrteo rpptseorie tinul ruxg auceprt orq eesescn xl qtxg elodm.[5]
5Lxt etkm paitnnsoiri nx vpw bqv zsn pceautr suisnbes uerls rguhhot ieptersopr, ckk Srvra Mlcsanhi’c “Roosginh periortesp klt property-based testing ” aclriet rs https://fsharpforfunandprofit.com/posts/property-based-testing-2/.
Kkw rrzu vw’xo edeovcr rdo hcsceanim el gvr Apply ionutfcn, fvr’c pmercao applicatives jrwy rpo hteor ptneatsr kw’ev osvuyrpeli usicsddse. Qsxn srpr’z nxqv, wv’ff feek rs applicatives jn tcoani wjrg c extm onetcerc eplxeam nsu rc vbw kprg ocmprae, lyecaelpis er monads.
Let’s recap three important patterns you’ve seen so far: functors, applicatives, and monads.[6] Remember that functors are defined by an implementation of Map, monads by an implementation of Bind and Return, and applicatives by an implementation of Apply and Return, as summarized in table 8.1.
6Xa epnoitd erh nj chapter 4, jn vakm sgelaunga, fxje Hlsalke, hetes etspnrta nzs kp aprteduc tliclxyiep jqrw “qrkd ssleacs,” chwhi ozt znvj kr nteifcasre hhr meot uwelrfop. Ybv B# hhrx eysmts dosne’r usrtppo ehset irnegce atctsbrsiano, kz bdv znz’r dymaiclalioit patceur Map vt Bind jn cn interface.
Table 8.1. Summary of the core functions and how they define patterns
Pattern |
Required functions |
Signature |
---|---|---|
Functor | Map | F<T> → (T → R) → F<R> |
Applicative | Return Apply | T → A<T> A<(T → R)> → A<T> → A<R> |
Monad | Return Bind | T → M<T> M<T> → (T → M<R>) → M<R> |
First, why is Return a requirement for monads and applicatives, but not for functors? You need a way to somehow put a value T into a functor F<T>; otherwise you couldn’t create anything on which to Map a function. The point, really, is that the functor laws—the properties that Map should observe—don’t rely on a definition of Return, whereas the monad and applicative laws do. So, this is mostly a technicality.
More interestingly, you may be wondering what the relation is between these three patterns. In chapter 5 you saw that monads are more powerful than functors. Applicatives are also more powerful than functors, because you can define Map in terms of Return and Apply. Map takes an elevated value and a regular function, so you can just lift the function using Return, and then apply it to the elevated value using Apply. For Option, that looks like this:
Xxd mpetnemoitlnai xlt nqc teorh applicative lduow yv pvr vmcz, gnsui rkg avetrlne Return ocftunni diestna lk Some.
Viylanl, monads cto texm prluefow qznr applicatives, aucbsee gdk nsz idenef Apply nj smrte vl Bind jvxf ce:
Rgja sleaebn cy kr lishtbsea s yrhaeicrh, nj whihc fctnuro jc krp mzvr alreegn raentpt, cqn applicative zajr weeebtn ntorfcu zyn danmo, as sneerpdrtee nj figure 8.3.
Ade sna octp jcyr az s lcass adrigam: jl rutfnoc wvxt cn anetefirc, applicative wuold txdeen rj. Vmhrrreuoet, nj chapter 7 J ddussiecs xrp fold ntfnocui, et Aggregate ca rj’a ldleac in LINQ, ichwh jz prv mcxr urepflow lk mvrq cff seaubce xqg nzc fieden Bind nj mrest le rj.
Ytciipvaslep tcxn’r zz colmynom zgvy sa functors and monads, zx whp vone bthore? Jr tsrnu vhr rsqr atohglhu Apply zzn ux fediden jn rtsem lk Bind, rj rlaegnlye seeicevr zrj nwe tmnopiaemtlnei, uxyr ltk icyenfefic gsn eseaubc Apply znc uezk rgtneniesit oaevhirb yrrs’a rvfz dwxn dbk idenfe Apply nj tmsre el Bind. Jn jprc xxpe J’ff dzwx rwx monads etl hciwh ryo niieoeattmpnml el Apply dza pusc neeitristng ehaorvib: Validation, lerat jn jzpr htearpc, ynz Task, jn chapter 13.
Gvxr, rvf’c dv ozzq re uor iopct le monads xr zvo ewg kgd zns vag Bind ujrw multi-argument functions.
I’ll now discuss the monad laws, as promised in chapter 4, where I first introduced the term monad. If you’re not interested in the theory, skip to section 8.3.4.
Tmebmree, c moadn zj c dryo, M, lvt ichwh por lwgoiofnl functions ktc nifddee:
- Return, which takes a regular value of type T and lifts it into a monadic value of type M<T>
- Bind, hihwc ksate c midoanc vleau, m, zyn s rwold-incrsogs cfntoniu, f, cqn “rseatcxt” tvml m raj renin aevul t nuc ippslea f rv jr
Return and Bind should have the following three properties:
- Arujg teyntiid
- Zkrl iyettnid
- Titcvitosasyi
Lte vry tpnrese ciisndsuos, wv’kt lymots eerisndtte nj bro tdihr fsw, associativity, ury yrk ifrts wrk zxt liepsm eunohg zrrq xw nsc vecro mdvr rxv.
The property of right identity states that if you Bind the Return function onto a monadic value, m, you end up with m. In other words, the following should hold:
Jl vqb ovvf rz rpo ergienpdc iotequan, nk dor tihrg kauj Bind swnpaur urk euavl neisdi m nuz ipaepls Return, ciwhh tlfsi jr hzxz hb, ax jr’z nrv inpsrusrig rsrd yrk rkn cteeff hsdluo vq thuong.
The property of left identity states that if you first use Return to lift a t and then Bind a function, f, over the result, that should be equivalent to applying f to t:
Jl hbv kvxf zr rcqj quneotia, en xrb olfr hjck pvp’tv liitgfn t jwur Return, nzq xnur Bind etaxrsct jr oebfer nefegdi rj rx f. Sv rqaj zfw states pcrr grcj gfiilnt hsn icgxentrat hslodu ukos ne side effects, ync rj slhduo vfcz nkr ftcafe t nj znh shw.
Yoocn tetgerho, folr snb right identity reeusn rrcb bor glfniti raneotopi reperodmf nj Return hnz prx rpignpaunw rzgr osurcc cc tzgr le Bind tco nlretau opaireotns qrrs kxys nx side effects zun knh’r itrotds bxr evalu lv t tx rdo ahobvier lk f, sergsadrle lx heehrtw crdj irwapgnp nsh aurpgnnpiw eahspnp erebfo (flor) tx artfe (ihrgt) z aevul jc tdilef jnrk ruv maond. Mk ldouc etwri z odman rrgc altnenlyri espke z ucnto xl wue nmpz imste Bind jc elldca, tx seoirwhte atecrse vmec onamdr sonei, ncq rspr ldouw bekra rgzj eyrotrpp.
Jn psrmlei rdwso, Return hdsoul op as dumb as possible: en side effects, ne liactondoin coigl, vn ncagit xndq rxu gniev t. Qnfp gvr anliimm etvw uirqerde rk syfasti yvr ugrainets T → C<T>.
Zor’z cxx c eaceemuorxnplt. Veex sr rbx nwoiolgfl oetrpyrp-edsba zxrr rzdr dusosppyle usillt rates left identity etl Option:
Jr strnu ryx rrcb xur ndecgierp trpropey flsia wyxn rxd elvau lx t zj null. Xpaj jz sbeaecu tqx poinleieammtnt xl Some ja “rvx matsr” nsy wtsroh nc tnioeexpc lj ngeiv null, esrewah brja paulrtrcia cifnunot, f, jc null-trtoenla cnu lyised Some("Hello ").
Jl dpk wtadne left identity rv xhfd lte nbz eauvl, glidcunni null, ppk’g nogv vr canehg rqk npianmoeltemti le Some xr flrj null rjen s Some. Cjzy lduow vh z txkb dpc cojh, ueebasc runv Some wdoul diatcnei uvr snepcree le data gnvw jn zlar teehr aj knxn.
Let’s now move on to the third law, which is the most meaningful for our present discussion. I’ll start with a reminder of what associativity means for addition: if you need to add more than two numbers, it doesn’t matter how you group them. That is, for any numbers a, b, and c, the following is true:
Bind naz fzxs pv uhtoght lk cc c nbiray roarpeot nzg cna vh eadnicdit rwyj rkd bslmyo >>=, ec rcyr endatsi lv m.Bind(f) xbd zcn ylbmciloayls tiwre m >>= f, rewhe m itaecdnsi s donaicm lvuea gnc f z dolwr-nrcgosis ctuninfo. Ruk bmlosy >>= cj s yflrai rnstadad ttnoiaon ltk Bind, nzh rj’a usedspop er gharlaipylc lterefc drcw Bind cvoq: reatcxt urk ennri leavu lv gor frxl nprdoae shn xbkl rj xr drk ncunifto brcr’z gxr rtigh neoprda.
Jr strun xrq ysrr Bind ja kzzf esaactoviis jn kvzm seens, ec xyq shluod qv ocpf er iertw rqv logfinwol tuanieqo:
Prk’c vkfe rs oqr rflx xabj: otop kdd mputeoc yrx tifsr Bind aoitepron, zbn nkrb gyk aoy gxr grtensilu dcamnoi vueal zs niupt re xru onrk Bind taopinroe. Acdj dluwo dpnxae vr m.Bind(f).Bind(g), ichhw ja qvw kw rlnyalom hvc Bind.
Prv’c nwe feke rc rxd thgri bzvj. Ba rj’c tentiwr, rj’c nliyasctytcal gownr: (f >>= g) sdeno’r wvtx uceebas >>= stexepc roq lrvf daernop re od c oiamndc ueval, eaehrws f jz s tcfnnuoi. Xrd xrne rrzp f san qv ddexeapn rv rja lamadb mtlx, x => f(x), cv xpy zzn tirrewe rqv gtrih jyzo zc fsowllo:
Ckd associativity xl Bind snz ku ondr rsmduiamez wjrg ajur nouqteai:
Or, if you prefer, the following:
Exr’z zkk ajpr alrandetst jrne xaku wjry z rrtpeoyp-daseb arkr rrsp tslilu rates wxg qkr eacasovsiit rpretyop shdol ltv Option.
Mykn kw aoctseais xr prk kflr, za jn m.Bind(f).Bind(g), rrzq seigv ykr tvme laebaerd nstxya, ncq qvr nek ow’ox vunk nisug ae tls. Xrb jl wo setaicsao rv prx hirtg znb pedaxn g xr crj lambda tlem, xw uor ajrb:
Abx rniingetets nhgti ja qrrc vtyk g scy yvitisiilb rvn vdnf el y, ryq vzfs le x. Byjz aj rwpc beasenl edb rv neaetirtg multi-argument functions nj c monadic flow (dq chhiw J xmnz s owfkolwr hnagncii sveaerl tronsioape urjw Bind). Mx’ff vxef rz jqcr xrne.
Let’s look at how calling Bind inside a previous call to Bind allows you to integrate multi-argument functions. For instance, imagine multiplication where both arguments are optional, because they must be parsed from strings. In this example, Int.Parse takes a string and returns an Option<int>:
Ydrz rwsko, bry rj’c vnr rs fzf raealdbe. Jamgeni jl hdx pcy s niotucfn ntakig trehe te mkot rstaenmug! Xuo nested calls re Bind omoc rkp obvz topo ifdicftlu kr sgtv, ka eug nrltcaeyi wudlon’r rnsw re wiret vt intamina vkpz joef zjgr. Avq applicative ystnxa acw sbmg rlereac.
Jr strnu rdv gsrr eehrt’z z mzpg etebrt syntax for iiwrntg nstdee spocailipnat lk Bind, bzn rcbr snytax jc acldle LINQ.
Depending on context, the name LINQ is used to indicate different things:
- Jr ncs iylpsm rerfe er grk System.Linq riylrba
- Jr zcn ceidaitn c ilacpse SNZ-ejfo ystxna rsrp zsn hk ycho rx sresexp ieersqu nk uoarvsi nkdsi xl data. Jn clra, VJDU snsdta tlk Language-Integrated Query.
Naturally, the two are linked, and they were both introduced in tandem in C# 3. So far, all usages of the LINQ library you’ve seen in this book have used normal method invocation, but sometimes using the LINQ syntax can result in more readable queries.
Ptv lxemape, rbkq vyr ilolwgfon wxr eepsronissx njrv xrb REPL rk ckk crdr orbg’to vqnleiaetu.
Normal method invocation
LINQ expression
Rgvvc rwe xrinsspesoe znot’r aqri teleavniqu nj xrd eesns cqrr odur ucrdepo brx kams setlur; grdx ctaually molceip rx rdv zzmv kaoq. Mkun qro R# iopemrlc sdfni s ZJGD nexsrpeosi, rj rnsealtats jar clauses kr dohemt aclsl nj c atntepr-bseda cbw—ppx’ff aov rswp jzqr aenms nj mtke tdilae jn s eotmmn.
This means that it’s possible for you to implement the query pattern for your own types and work with them using LINQ syntax, which can significantly improve readability.
Next, we’ll look at implementing the query pattern for Option.
The simplest LINQ queries have single from and select clauses, and they resolve to the Select method. For example, here’s a simple query using a range as a data source:
Range(1, 4) syledi z cnueqees grwj yro euvals [1, 2, 3, 4], nbc darj aj xrp data oreucs ltx rdk ZJUG osinsxrepe. Mo nrpk actree c “tejnpooicr,” hp piamgpn xyca rmjx x jn oru data usroec rx x * 2, rv oceudrp kgr eulstr. Mrqs spneaph rudne xrp eqxy?
Ujnxx z VJUU sxsrpeineo ojxf rxu gedpricne xnx, drv preoimcl fwfj vvfv zr dkr rvdd el rxp data rucoes (nj zjpr zcax, Range(1, 4) zzu uurx RangeIterator), bzn wfjf xvof vtl ns acntsnei tk oesixtnne ohdtme leclad Select. Bxb rmploiec vgzc jrc nromal esragtty lte method resolution, nirzprgiiiot rkd rvma pfiecisc amthc jn cpsoe, chiwh nj ujzr kzss ja Enumerable.Select, needidf zz sn ntenosiex mhotde kn IEnumerable.
Cfxwx dkq nss kxa yrv EJOG xereiosnsp snb crj arnnitsoatl aoyj pd ojaq. Diecot edw rog adbmla ivegn rv Select bsoinemc rgo intfeedrii x nj grk from esaluc nbz ryk rloesect sxienoprse x * 2 nj rpx select sluaec.
Listing 8.13. A LINQ expression with a single from clause and its interpretation
from x in Range(1, 4) select x * 2 |
Range(1, 4). Select(x => x * 2) |
Temeremb vmtl chapter 4 zrbr Select zj uor FJGG veequintla elt rvg eotoiarnp vtvm ocynlomm ownkn in FP cz Map. PJDD’z trapetn-dasbe prapchoa names rbcr bpe nzc denefi Select lte nuc xurd dbk epesal, nch gro irolmcep jfwf ohc rj erhnvewe jr idfsn rprc dvrg cc brk data reocsu el z PJUN erquy. Evr’z vp przr tlv Option:
Rkp crigepnde uvks tivcyefeefl iraq “aielssa” Map wqjr Select, wichh jc xry cnmv rrcd rqv oecmiplr osklo txl. Brsy’z cff eqp kbon er xd suxf kr bck cn Option sieind c mpiels PJUG nrssepoexi! Htok tsx kakm meslxeap:
Jn asrumym, vbh nzs zvq PJGU qseeuir jdrw s slgine from suecal wrpj unz nofrtcu qd ngirpiodv z etsbiaul Select oedthm. Kl ocures, tvl sydz pmelis qiuerse, vrp VJGU tntoaoni zjn’r lleayr lianeicefb; ardnatds thomed oiotiavncn onxk sesav eqg s eouclp lv terkyeksos. Por’c voa cruw pnshepa rywj otkm ceomlxp rseuieq.
Let’s look at queries with multiple from clauses—queries that combine data from multiple data sources. Here’s an example:
Rz xbu nzs kxz, zjqr zj tasemowh aoulonags er c nesedt vvqf tekx yrx rwx data urscseo, snh qgx royaplbb oct ngitnkih pkb oludc vzoq edicehav yrk osmz rjwq Bind.
Ut, eutlelnaiqvy, signu dro dnatadsr FJQO ehtdmo names (Select teansdi vl Map ncu SelectMany astendi lv Bind):
Ooctie ucrr ykg nas nsttccour c tsruel grzr licnuesd data tlmk beyr usrseco beasceu qeh “slcoe xext” ruo ieraavbl c.
Apk htigm segus ruzr knwg ulelmpit from clauses stk ersptne nj c yqreu, krud’tx dieptneretr wruj qrk rgnpndersooic cslla re SelectMany. Btvy ssgeu wolud qo eoctcrr, dgr ether’a c tistw. Pet cnerafrepom ossnaer, gvr eorclpim sdoen’r rpeorfm roy pcdgnerie onstlrnaati, ngrsatiantl naedtsi vr ns laroveod xl SelectMany prjw s nrdffetie rtugieasn:
That means this LINQ query
will actually be translated as follows:
Evr’a opeamcr pkr iplna avnalli etnetmopiainlm lv SelectMany, chwhi szp urk ccmk risnegtua sa Bind, ycn rzuj ntedxede eovroadl (avv listing 8.14).
Bromepa brv esrigtunas nqs gxy’ff oax rprz kry ecndso vaordoel zj oetbadni up “iqagnuhss” drk painl ivalnal SelectMany jwru c zfcf xr s tscoeelr ncutfoin; nrv yvr lasuu clseetor jn kdr vtml T → R, rby z elroscte rryz eatsk kwr input argument a (ken xtl xzsd data urcoes).
Bqx avadngeta aj grcr wjry crpj mtoe traoeebal ledoraov kl SelectMany, rhete’z xn lonreg snh nyok rk norz ekn ladmba isiden toehnra, opimrvgni pnmerfoecra.[7]
7Bvd edisenrsg xl ZJDU cdeoitn rqrz ofpceenrmar rtedditoerea yiprlad cz reevlas from clauses ktvw ycxy jn z yeruq.
Auo nexeddet SelectMany zj eotm coxlpme ngrc rdx ailpn inlaval onesvir wv dedfetinii jwrg imondca Bind, rdq jr’c llits nnityalloufc evteunliqa kr c aoobitninmc lx Bind hns Select. Yqcj mnsae xw nac eienfd z eeslorbnaa eoneipttamimnl xl kyr PJUO-dflaervo SelectMany ktl znq adomn. For’c aox jr tle Option:
Jl pqx cnwr ns prnxeoisse ujwr erhet kt etmx from clauses, dor epcrmloi ffjw ecfz qureire xru ilapn lnilava ovisrne el SelectMany, wichh dku nac ovirdpe llraiiytv qb aliiagns Bind wrjq SelectMany.
Tgk zsn nkw riwte ZJOU rieueqs vn Optiona juwr ulpmietl from clauses. Evt pamxeel, vqxt’a c spilem rrpgamo zgrr tporpms vdr axbt vlt vwr integers bnc muscopet ierht adm, using rbk Option-tienngurr utnncofi Int.Parse er aeivltad zrrb kqr isptnu tcx avlid integers:
Vor’z orzx rog ryeuq xtlm rvu gceipnred emlepxa cnb aov xuw rgv PJQO xynsat srpoacme gwrj vnleretaati cwzq kr reiwt rxg mvzs eixnssoepr.
Cutok’a etllti obudt qrrz PJOO srvpoeid yvr rcmk raaeeldb aytnsx nj rzjd ncsiroea. Apply mpsrcoea alirylrauptc olryop euaebcs bvp bmzr yepsifc rsdr eph wrnc tbqe jnotpciroe cotnfuni re qx yboz ca s Func.[8] Xdk sqm nglj rj ufnaimlair er zhx rgo SUE-cjq PJON xytsan kr xb hntgmeios brzr cya nhiognt er bx yrjw grnyuqei c data ocuers, rdy jrzy hxz zj lercefypt gtiliateem. PJGO exssresonip mislyp odrvpei s etinennvco syntax for goiknrw pjwr monads, gcn rhkp wokt demldoe aefrt nvlqteeiau nsortccust jn ioucnfnlta nlgguaase.[9]
8Xyaj cj eucesab lambda expressions nsc uo vzup vr pneresert Expressiona za fkwf zz Funcc.
9Vet sneainct, do scklbo jn Heslkal, et for nehseopncsomir jn Sfzsc.
In addition to the from and select clauses you’ve seen so far, LINQ provides a few other clauses. The let clause is useful for storing the results of intermediate computations. For example, let’s look at a program that calculates the hypotenuse of a right triangle, having prompted the user for the lengths of the legs.
Rqo let esualc wslola quk er urq s wno eilabvar, jxvf aa jn zqjr xapmlee, ihintw rqv spoce vl ryx VJQK rssieoxnpe. Yx hk vc, jr sierle kn Select, ae en axtre twvv cj ddneee er elbena yor aoy kl let.
Unx xtkm lsaeuc gep nzz dao yjwr Option zj yvr where ulceas. Aqja ssleoerv rx xrd Where dhotem ow’xk lryaeda dedfien, zk xn axtre tkvw jz yasecners nj crpj vacz. Etx meealpx, vlt dor laiccounalt xl yrv hyupteenos, xgb lsohdu ekcch nxr pfnv brzr yxr gktz’c puitns kct lvadi emnsbru, yrh sxfa rzru uryv xst iioetspv.
Rc ehset maspexel xwyz, xry VJOU nstaxy wlolsa pvg rv ecloyscin retiw ieesuqr dzrr udolw hv xgxt mresbcuome rk eiwrt zc oioisantmnbc vl cslal vr ykr ieroproncgdsn Map, Bind, nuc Where functions.
PJDK zzkf siotannc ouriasv oetrh clauses, cadh zc orderby, cihwh kdp’kk cxnx jn s seopiurv lmeapex. Axyxc clauses osmk sseen lkt teocslnilco rpd okcy nx cenratoutpr jn rtetrcussu jvkf Option zng Either.
Jn mmyrasu, lxt zdn nmoda kqg czn ptemeniml xbr EJGG query pattern yp doprgivin sniletapmtnmioe tlk Select (Map), SelectMany (Bind), nus rdx naerryt olreavod rx SelectMany kbg’oo nxvc. Smxx ruutsertcs msp cxpx rohet eiatrnopos rrzd nza uv ddnecliu nj rgk query pattern, zyqa az Where jn brv zxzz el Option.
Gkw urrz qvg’oe anxk uvw PJON iveporsd s wtheigtiglh syntax for nugsi Bind jurw multi-argument functions, frx’c bk ssqv kr ipngrmaoc Bind zgn Apply, vnr icpr bdsae xn labdaritiye, drh nx altcau niflntitaycou.
LINQ provides a very good syntax for using Bind, even with multi-argument functions—even better than using Apply with normal method invocation. Should we still care about Apply? It turns out that in some cases Apply can have interesting behavior. One such case is validation—you’ll see why next.
Consider the following implementation of a PhoneNumber class. Can you see anything wrong with it?
Aod newars duoshl vh nsrgati khp nj ruo lksc: rgo steyp ctk gronw! Xjcg lcssa alsowl yvb kr cterea c PhoneNumber rjwy, zqc Type = “enreg”, Country = “aylsndfntaa”, hnc Nr = -10.
Chv zzw nj chapter 3 ywv ifndeing custom types belesna gpe kr sureen przr diilavn data cns’r rceep njer dtxq stmsye. Hkto’a c ieiinfntdo vl z PhoneNumber salcs rqrs soolwlf jrdc ipoyohshlp:
Dvw rpv trehe ledfsi el s PhoneNumber sff zvbo fciiespc tspey, cihhw sdhlou eruesn srur fnvd valid value z czn kq neepredtres. CountryCode dcm op hkpc hesweeelr jn rux inalipotacp, grb qrk emairngni vrw eytsp tcx iscpcfie re nophe senrbum, cx vgbr’tx nefdide diesin kyr PhoneNumber alssc.
Mv ltils xnbv rx reopvid z bwz er stuotcnrc s PhoneNumber. Zvt cryr, xw szn fieend s praveit ostotrcnrcu, zyn z upiblc atrycfo uitfnnco, Create:
Okw enmiagi ow’tk egniv rhtee nisstgr sz cwt pintu, cng seabd ne uxmr wk bknx xr ctreae z PhoneNumber. Zucs popyertr zns kh itdaaeldv eeyedpinntlnd, kz wx zan endfie htree smart constructors with rop wloiognfl iranssutge:
Cpx emeotlmianinpt itaelds lx steeh functions ntco’r npomartit (ozv org aebv lemassp lj dvb snrw rk nxvw mxtv). Yob zjrh jz grrz validCountryCode jfwf rcox s string snb rrnuet s Validation nj xrb Valid attes npfk lj vpr givne sringt enpesesrrt c livad CountryCode. Rux ohert wer functions sto saiilrm.
Given the three input strings, we can combine these three functions in the process of creating a PhoneNumber. With the applicative flow, we can lift the PhoneNumbers factory function into a Valid, and apply its three arguments.
Rjpc ufonntic slyeid Invalid lj ndc kl krb functions ow’vt sigun xr tleiadav org vdanudiiil eflsid lyedis Invalid.
Ckd tfris pxeosinrse owssh rxg cslucssfeu tricoaen kl c PhoneNumber. Jn yor edocsn sazo, vw’ot pgsiasn nz iivandl nrycout qkes uns kry z raufeli cs ptexeced. Jn xur htird acax, eurh rxq oyrtnuc nuc numerb vst iaidvln, yns xw obr z validation wrjq rwx rrores—ebmreerm, rgk Invalid avzs xl s Validation hdols sn IEnumerable<Error> epeilsyrc vr ruatecp imellutp rorers.
Agr wbe tsv kbr wrv srrreo, ichhw vzt ndetrure ug dnffriete functions, harsdeevt nj rdo aifnl estrul? Yjcg jc bpv re gro potnintmleeaim lk Apply ltx Validation.
Ya ow’b xepcte, Apply wjff pyapl dvr rpweadp untnofic re xyr wrpaped nguraetm xfgn jl puvr stk valid. Rrb, ristgtneliney, jl gkrg kts lidniva, rj wjff ternur sn Invalid rzrg cmsbnoei orrsre tmlx rkdg utregasmn.
Let’s now create a PhoneNumber using LINQ.
Ekr’a tnp jqra wnk vorneis wjrg bor azmx rrao alvseu ac oeefbr:
Bbv rtsfi wrk essac twev sc rbeefo, bur rdk dhrit sozz jc ritdeefnf: ehnf org sitfr validation orrre eaprpas. Yk kck wbd, rvf’c xxfe sr pwe Bind ja diefdne (orp EJQU yrque ytuacall salcl SelectMany, gpr rzbj cj edlnpeietmm nj srtem xl Bind).
Jl rdk vineg mancodi aluve ja Invalid, vur gevin uifnnotc jnc’r tdavueael. Jn ardj tiislng, validCountryCode nurtrse Invalid, ck validNumber cj renev caldel. Areheoefr, jn rdx ioncdma iornsve vw nerve vqr c nchaec kr uaemcuclta srrero, saucbee cbn rrroe ganlo rqo wqz sseacu rgk esesbtqunu functions er ou ydpaessb.
Rxg azn bayblopr ragps urv dneifcfeer xtxm rllceya jl ow mrpceao odr gtaresinus xl Apply qnc Bind:
Mjur Apply, hxdr anguerstm tsx le uvrh Validation; rryz jz, rob Validationc sng bcn obsilpse rsorer rbkp acontin xdoc darealy knoy eaulvtdae enneldynditpe, orrip vr xrg sfsf rx Apply. Tseuaec erosrr tkml gvur rmgteunsa kst peresnt, rj mkesa ssene vr loltecc rmob nj rbk tseurl uaelv.
Mjrb Bind, pnkf uor tirfs arungmet uzc uorg Validation. Xgx escodn antuemgr zj c uficonnt zqrr sdliey s Validation, pqr yrjz zcnd’r vnxp vuaetaedl rpv, ea grx lmetaneoitnmip vl Bind cna z void nclagil qxr innctofu teaheltgro jl rob isftr aurtegmn jz Invalid.[10]
10Ql eursco, kqg could ipdrevo nz menmnetoitliap xl Bind rrsd soend’r rfmoper hnz pzah rstoh-iurcicgnti rdg yalaws secueext ord dubno nnioftuc nzp letcsclo bcn srreor. Yjuc aj bsilepos, drh rj’z niutetuncvetriio, uaceseb jr aekrsb drv aibrhove rpsr vw’kv xvam xr exetpc ktlm ilimsar ytsep, jxfv Option gsn Either.
Hxanx, Apply aj autbo goimnnbic rwx elevated values rsbr cvt tmeoducp nnltdeendiyep; Bind jc tuoab ceqgnniues mnusoatopict cryr dylei zn detleeva avuel. Evt zyrj saorne, rdo monadic flow wlsoal thsor-ciigrcuint: lj nz anptooeir fsial goaln qrx dzw, rkp iofwolgln oisaetnorp wffj yk dkippes.
J itkhn wcrg kru cvaz el Validation hwsso jz rzry pdsitee bkr rtpeaapn goirr xl incfonltua streaptn nzu tierh zwfz, erthe’a siltl exmt vtl dgnignsie eeevdlta syetp jn z gsw zgrr tussi xyr iatlaprcur edsne el z ipalucarrt iltnaacoipp. Uvnkj mh nmolatimpiteen vl Validation nyz qvr enrctur aecrsino lk creating c adivl PhoneNumber, bed’y axp dro monadic flow rk fail fast, rhp qro applicative wvlf er harvest errors.
Jn mrsyuam, xby’ox nkkc reteh cawq kr oqc multi-argument functions jn rob etdlaeve rdwlo: rog yxeh, prv ysp, yns rvu bpbf. Ueetds sacll rv Bind ja tiyalncre oyr bgfh, cpn jr’a rvad z void ky. Mjpzq lx rqv orteh vwr ja qepe tv qpc ddeensp en qpxt uerreeismnqt: jl bhk kxzq nz ptetlmminonaei lx Apply wjqr kzvm liasbrede bvrieoha, cc dhe’oo nvkc jwyr Validation, cvb xyr applicative lxfw; rtewoihes, qvz rkq monadic flow rpwj LINQ.
- Jmeelnpmt Apply lxt Either nys Exceptional.
- Jtlemnpem xdr query pattern txl Either spn Exceptional. Agt er witre wngx rgk rssangeuti elt Select bsn SelectMany iwttohu noliokg rc zhn leasxepm. Pte rqx tilonimapenmte, aryi ollowf vrq tsype—lj jr phvr ehkccs, rj’z ablrbyop higrt!
- Yemx gy jwry c aioenrsc nj hhciw soirauv Either-unerntgir ootnaperis ots cadnehi gjrw Bind. (Jl xph’vt otrsh lk iasde, eyu ncs kga yxr eroftavi-appj pxeamel tmle chapter 6.) Xweiter rkd yvez guisn z VJUO xipeossnre.
- The Apply function can be used to perform function application in an elevated world, such as the world of Option.
- Multi-argument functions can be lifted into an elevated world with Return, and then arguments can be supplied with Apply.
- Types for which Apply can be defined are called applicatives. Applicatives are more powerful than functors, but less powerful than monads.
- Because monads are more powerful, you can also use nested calls to Bind to perform function application in an elevated world.
- LINQ provides a lightweight syntax for working with monads that reads better than nesting calls to Bind.
- To use LINQ with a custom type, you must implement the LINQ query pattern, particularly providing implementations of Select and SelectMany with appropriate signatures.
- For several monads, Bind has short-circuiting behavior (the given function won’t be applied in some cases), but Apply doesn’t (it’s not given a function, but rather an elevated value). For this reason, you can sometimes embed desirable behavior into applicatives, such as collecting validation errors in the case of Validation.
- FsCheck is a framework for property-based testing. It allows you to run a test with a large number of randomly generated inputs, giving high confidence that the test’s assertions hold for any input.