clojure reactive programming -...
TRANSCRIPT
TableofContents
ClojureReactiveProgramming
Credits
AbouttheAuthor
Acknowledgments
AbouttheReviewers
www.PacktPub.com
Supportfiles,eBooks,discountoffers,andmore
Whysubscribe?
FreeaccessforPacktaccountholders
Preface
Whatthisbookcovers
Whatyouneedforthisbook
Whothisbookisfor
Conventions
Readerfeedback
Customersupport
Downloadingtheexamplecode
Errata
Piracy
Questions
1.WhatisReactiveProgramming?
AtasteofReactiveProgramming
Creatingtime
Morecolors
Makingitreactive
Exercise1.1
Abitofhistory
Dataflowprogramming
Object-orientedReactiveProgramming
Themostwidelyusedreactiveprogram
TheObserverdesignpattern
FunctionalReactiveProgramming
Higher-orderFRP
Signalsandevents
Implementationchallenges
First-orderFRP
Asynchronousdataflow
ArrowizedFRP
ApplicationsofFRP
Asynchronousprogrammingandnetworking
ComplexGUIsandanimations
Summary
2.ALookatReactiveExtensions
TheObserverpatternrevisited
Observer–anIterator’sdual
CreatingObservables
CustomObservables
ManipulatingObservables
Flatmapandfriends
Onemoreflatmapfortheroad
Errorhandling
OnError
Catch
Retry
Backpressure
Sample
Backpressurestrategies
Summary
3.AsynchronousProgrammingandNetworking
Buildingastockmarketmonitoringapplication
Rollingaverages
Identifyingproblemswithourcurrentapproach
RemovingincidentalcomplexitywithRxClojure
Observablerollingaverages
Summary
4.Introductiontocore.async
Asynchronousprogrammingandconcurrency
core.async
Communicatingsequentialprocesses
Rewritingthestockmarketapplicationwithcore.async
Implementingtheapplicationcode
Errorhandling
Backpressure
Fixedbuffer
Droppingbuffer
Slidingbuffer
Transducers
Transducersandcore.async
Summary
5.CreatingYourOwnCESFrameworkwithcore.async
AminimalCESframework
ClojureorClojureScript?
DesigningthepublicAPI
Implementingtokens
Implementingeventstreams
Implementingbehaviors
Exercises
Exercise5.1
Exercise5.2
Arespondentapplication
CESversuscore.async
Summary
6.BuildingaSimpleClojureScriptGamewithReagi
Settinguptheproject
Gameentities
Puttingitalltogether
Modelinguserinputaseventstreams
Workingwiththeactivekeysstream
ReagiandotherCESframeworks
Summary
7.TheUIasaFunction
TheproblemwithcomplexwebUIs
EnterReact.js
Lessonsfromfunctionalprogramming
ClojureScriptandOm
BuildingasimpleContactsapplicationwithOm
TheContactsapplicationstate
SettinguptheContactsproject
Applicationcomponents
Omcursors
Fillingintheblanks
Intercomponentcommunication
CreatinganagileboardwithOm
Theboardstate
Componentsoverview
Lifecycleandcomponentlocalstate
Remainingcomponents
Utilityfunctions
Exercises
Summary
8.Futures
Clojurefutures
Fetchingdatainparallel
Imminent–acomposablefutureslibraryforClojure
Creatingfutures
Combinatorsandeventhandlers
Themoviesexamplerevisited
FuturesandblockingIO
Summary
9.AReactiveAPItoAmazonWebServices
Theproblem
Infrastructureautomation
AWSresourcesdashboard
CloudFormation
ThedescribeStacksendpoint
ThedescribeStackResourcesendpoint
EC2
ThedescribeInstancesendpoint
RDS
ThedescribeDBInstancesendpoint
Designingthesolution
RunningtheAWSstubserver
Settingupthedashboardproject
CreatingAWSObservables
CombiningtheAWSObservables
Puttingitalltogether
Exercises
Summary
A.TheAlgebraofLibraryDesign
Thesemanticsofmap
Functors
TheOptionFunctor
Findingtheaverageofages
ClojureReactiveProgrammingCopyright©2015PacktPublishing
Allrightsreserved.Nopartofthisbookmaybereproduced,storedinaretrievalsystem,ortransmittedinanyformorbyanymeans,withoutthepriorwrittenpermissionofthepublisher,exceptinthecaseofbriefquotationsembeddedincriticalarticlesorreviews.
Everyefforthasbeenmadeinthepreparationofthisbooktoensuretheaccuracyoftheinformationpresented.However,theinformationcontainedinthisbookissoldwithoutwarranty,eitherexpressorimplied.Neithertheauthor,norPacktPublishing,anditsdealersanddistributorswillbeheldliableforanydamagescausedorallegedtobecauseddirectlyorindirectlybythisbook.
PacktPublishinghasendeavoredtoprovidetrademarkinformationaboutallofthecompaniesandproductsmentionedinthisbookbytheappropriateuseofcapitals.However,PacktPublishingcannotguaranteetheaccuracyofthisinformation.
Firstpublished:March2015
Productionreference:1160315
PublishedbyPacktPublishingLtd.
LiveryPlace
35LiveryStreet
BirminghamB32PB,UK.
ISBN978-1-78398-666-8
www.packtpub.com
CreditsAuthor
LeonardoBorges
Reviewers
EduardBondarenko
ColinJones
MichaelKohl
FalkoRiemenschneider
AcquisitionEditor
HarshaBharwani
ContentDevelopmentEditor
ArunNadar
TechnicalEditor
TanviBhatt
CopyEditors
VikrantPhadke
SameenSiddiqui
ProjectCoordinator
NehaBhatnagar
Proofreaders
SimranBhogal
MariaGould
Indexer
MariammalChettiyar
Graphics
AbhinashSahu
ProductionCoordinator
ManuJoseph
CoverWork
ManuJoseph
AbouttheAuthorLeonardoBorgesisaprogramminglanguagesenthusiastwholoveswritingcode,contributingtoopensourcesoftware,andspeakingonsubjectshefeelsstronglyabout.Afternearly5yearsofconsultingatThoughtWorks,whereheworkedintwocommercialClojureprojects,amongmanyothers,heisnowasoftwareengineeratAtlassian.HeusesClojureandClojureScripttohelpbuildreal-timecollaborativeeditingtechnology.Thisishisfirstfull-lengthbook,buthecontributedacoupleofchapterstoClojureCookbook,O’Reilly.
LeonardohasfoundedandrunstheSydneyClojureUserGroupinAustralia.Healsowritespostsaboutsoftware,focusingonfunctionalprogramming,onhiswebsite(http://www.leonardoborges.com).Whenheisn’twritingcode,heenjoysridingmotorcycles,weightlifting,andplayingtheguitar.
AcknowledgmentsIwouldliketotakethisopportunityandstartbythankingmyfamily:mygrandparents,AltamirandAlba,fortheirtirelesssupport;mymother,Sônia,forherunconditionalloveandmotivation;andmyuncle,AltamirFilho,forsupportingmewhenIdecidedtogotoschoolatnightsothatIcouldstartworkingasaprogrammer.Withoutthem,Iwouldhaveneverpursuedsoftwareengineering.
Iwouldalsoliketothankmyfiancee,Enif,whoansweredwitharesounding“yes”whenaskedwhetherIshouldtakeupthechallengeofwritingabook.Herpatience,love,support,andwordsofencouragementwereinvaluable.
Duringthewritingprocess,PacktPublishinginvolvedseveralreviewersandtheirfeedbackwasextremelyusefulinmakingthisabetterbook.Tothesereviewers,thankyou.
Iamalsosincerelygratefulformyfriendswhoprovidedcrucialfeedbackonkeychapters,encouragingmeateverystepoftheway:ClaudioNatoli,FábioLessa,FabioPereira,JulianGamble,SteveBuikhuizen,andmanyothers,whowouldtakemultiplepagestolist.
Lastbutnotleast,awarmthankstothestaffatPacktPublishing,whohelpedmealongthewholeprocess,beingfirmandresponsible,yetunderstanding.
Eachofyouhelpedmakethishappen.Thankyou!
AbouttheReviewersEduardBondarenkoisasoftwaredeveloperlivinginKiev,Ukraine.HestartedprogrammingusingBasiconZXSpectrumalongtimeago.Later,heworkedinthewebdevelopmentdomain.
HehasusedRubyonRailsforabout8years.HavingusedRubyforalongtime,hediscoveredClojureinearly2009,andlikedthelanguage.BesidesRubyandClojure,heisinterestedinErlang,Go,Scala,andHaskelldevelopment.
ColinJonesisdirectorofsoftwareservicesat8thLight,wherehebuildsweb,mobile,anddesktopsystemsforclientsofallsizes.He’stheauthorofMasteringClojureMacros:WriteCleaner,Faster,SmarterCode,PragmaticBookshelf.ColinparticipatesactivelyintheClojureopensourcecommunity,includingworkontheClojureKoans,REPLy,leiningen,andmakessmallcontributionstoClojureitself.
MichaelKohlhasbeendevelopingwithRubysince2004andgotacquaintedwithClojurein2009.Hehasworkedasasystemsadministrator,journalist,systemsengineer,Germanteacher,softwaredeveloper,andpenetrationtester.HecurrentlymakeshislivingasaseniorRubyonRailsdeveloper.HepreviouslyworkedwithPacktPublishingasatechnicalreviewerforRubyandMongoDBWebDevelopmentBeginner’sGuide.
FalkoRiemenschneiderstartedprogrammingin1989.Inthelast15years,hehasworkedonnumerousJavaEnterprisesoftwareprojectsforbackendsaswellasfrontends.He’sespeciallyinterestedindesigningcomplexrich-userinterfaces.In2012,henoticedandlearnedClojure.HequicklycameincontactwithideassuchasFRPandCSPthatshowgreatpotentialforaradicallysimplerUIarchitecturefordesktopandin-browserclients.
Falkoworksforitemis,aGermany-basedsoftwareconsultancyfirmwithstrongcompetenceforlanguage-andmodel-basedsoftwaredevelopment.HecofoundedaClojureusergroup,andencouragesotherdeveloperswithinandoutsideitemistolearnfunctionalprogramming.
Falkopostsregularlyonhttp://www.falkoriemenschneider.de.
Supportfiles,eBooks,discountoffers,andmoreForsupportfilesanddownloadsrelatedtoyourbook,pleasevisitwww.PacktPub.com.
DidyouknowthatPacktofferseBookversionsofeverybookpublished,withPDFandePubfilesavailable?YoucanupgradetotheeBookversionatwww.PacktPub.comandasaprintbookcustomer,youareentitledtoadiscountontheeBookcopy.Getintouchwithusat<[email protected]>formoredetails.
Atwww.PacktPub.com,youcanalsoreadacollectionoffreetechnicalarticles,signupforarangeoffreenewslettersandreceiveexclusivediscountsandoffersonPacktbooksandeBooks.
https://www2.packtpub.com/books/subscription/packtlib
DoyouneedinstantsolutionstoyourITquestions?PacktLibisPackt’sonlinedigitalbooklibrary.Here,youcansearch,access,andreadPackt’sentirelibraryofbooks.
Whysubscribe?FullysearchableacrosseverybookpublishedbyPacktCopyandpaste,print,andbookmarkcontentOndemandandaccessibleviaawebbrowser
FreeaccessforPacktaccountholdersIfyouhaveanaccountwithPacktatwww.PacktPub.com,youcanusethistoaccessPacktLibtodayandview9entirelyfreebooks.Simplyuseyourlogincredentialsforimmediateaccess.
PrefaceHighlyconcurrentapplicationssuchasuserinterfaceshavetraditionallymanagedstatethroughthemutationofglobalvariables.Variousactionsarecoordinatedviaeventhandlers,whichareproceduralinnature.
Overtime,thecomplexityofasystemincreases.Newfeaturerequestscomein,anditbecomesharderandhardertoreasonabouttheapplication.
Functionalprogrammingpresentsitselfasanextremelypowerfulallyinbuildingreliablesystemsbyeliminatingmutablestatesandallowingapplicationstobewritteninadeclarativeandcomposableway.
SuchprinciplesgaverisetoFunctionalReactiveProgrammingandCompositionalEventSystems(CES),programmingparadigmsthatareexceptionallyusefulinbuildingasynchronousandconcurrentapplications.Theyallowyoutomodelmutablestatesinafunctionalstyle.
Thisbookisdevotedtotheseideasandpresentsanumberofdifferenttoolsandtechniquestohelpmanagetheincreasingcomplexityofmodernsystems.
WhatthisbookcoversChapter1,WhatisReactiveProgramming?,startsbyguidingyouthroughacompellingexampleofareactiveapplicationwritteninClojureScript.ItthentakesyouonajourneythroughthehistoryofReactiveProgramming,duringwhichsomeimportantterminologyisintroduced,settingthetoneforthefollowingchapters.
Chapter2,ALookatReactiveExtensions,exploresthebasicsofthisReactiveProgrammingframework.Itsabstractionsareintroducedandimportantsubjectssuchaserrorhandlingandbackpressurearediscussed.
Chapter3,AsynchronousProgrammingandNetworking,walksyouthroughbuildingastockmarketapplication.ItstartsbyusingamoretraditionalapproachandthenswitchestoanimplementationbasedonReactiveExtensions,examiningthetrade-offsbetweenthetwo.
Chapter4,Introductiontocore.async,describescore.async,alibraryforasynchronousprogramminginClojure.Here,youlearnaboutthebuildingblocksofCommunicatingSequentialProcessesandhowReactiveApplicationsarebuiltwithcore.async.
Chapter5,CreatingYourOwnCESFrameworkwithcore.async,embarksontheambitiousendeavorofbuildingaCESframework.Itleveragesknowledgegainedinthepreviouschapterandusescore.asyncasthefoundationfortheframework.
Chapter6,BuildingaSimpleClojureScriptGamewithReagi,showcasesadomainwhereReactiveframeworkshavebeenusedforgreateffectsingamesdevelopment.
Chapter7,TheUIasaFunction,shiftsgearsandshowshowtheprinciplesoffunctionalprogrammingcanbeappliedtowebUIdevelopmentthroughthelensofOm,aClojureScriptbindingforFacebook’sReact.
Chapter8,Futures,presentsfuturesasaviablealternativetosomeclasses’reactiveapplications.ItexaminesthelimitationsofClojurefuturesandpresentsanalternative:imminent,alibraryofcomposablefuturesforClojure.
Chapter9,AReactiveAPItoAmazonWebServices,describesacasestudytakenfromarealproject,wherealotoftheconceptsintroducedthroughoutthisbookhavebeenputtogethertointeractwithathird-partyservice.
AppendixA,TheAlgebraofLibraryDesign,introducesconceptsfromCategoryTheorythatarehelpfulinbuildingreusableabstractions.Theappendixisoptionalandwon’thinderlearninginthepreviouschapters.ItpresentstheprinciplesusedindesigningthefutureslibraryseeninChapter8,Futures.
AppendixB,Bibliography,providesallthereferencesusedthroughoutthebook.
WhatyouneedforthisbookThisbookassumesthatyouhaveareasonablymoderndesktoporlaptopcomputeraswellasaworkingClojureenvironmentwithleiningen(seehttp://leiningen.org/)properlyconfigured.
Installationinstructionsdependonyourplatformandcanbefoundontheleiningenwebsite(seehttp://leiningen.org/#install).
Youarefreetouseanytexteditorofyourchoice,butpopularchoicesincludeEclipse(seehttps://eclipse.org/downloads/)withtheCounterclockwiseplugin(seehttps://github.com/laurentpetit/ccw),IntelliJ(https://www.jetbrains.com/idea/)withtheCursiveplugin(seehttps://cursiveclojure.com/),LightTable(seehttp://lighttable.com/),Emacs,andVim.
WhothisbookisforThisbookisforClojuredeveloperswhoarecurrentlybuildingorplanningtobuildasynchronousandconcurrentapplicationsandwhoareinterestedinhowtheycanapplytheprinciplesandtoolsofReactiveProgrammingtotheirdailyjobs.
KnowledgeofClojureandleiningen—apopularClojurebuildtool—isrequired.
ThebookalsofeaturesseveralClojureScriptexamples,andassuch,familiaritywithClojureScriptandwebdevelopmentingeneralwillbehelpful.
Notwithstanding,thechaptershavebeencarefullywritteninsuchawaythataslongasyoupossessknowledgeofClojure,followingtheseexamplesshouldonlyrequirealittleextraeffort.
Asthisbookprogresses,itlaysoutthebuildingblocksrequiredbylaterchapters,andassuchmyrecommendationisthatyoustartwithChapter1,WhatisReactiveProgramming?,andworkyourwaythroughsubsequentchaptersinorder.
AclearexceptiontothisisAppendixA,TheAlgebraofLibraryDesign,whichisoptionalandcanbereadindependentoftheothers—althoughreadingChapter8,Futures,mightprovideausefulbackground.
ConventionsInthisbook,youwillfindanumberofstylesoftextthatdistinguishbetweendifferentkindsofinformation.Herearesomeexamplesofthesestyles,andanexplanationoftheirmeaning.
Codewordsintext,databasetablenames,foldernames,filenames,fileextensions,pathnames,dummyURLs,userinput,andTwitterhandlesareshownasfollows:“Wecanincludeothercontextsthroughtheuseoftheincludedirective.”
Ablockofcodeissetasfollows:
(defnumbers(atom[]))
(defnadder[keyrefold-statenew-state]
(print"Currentsumis"(reduce+new-state)))
(add-watchnumbers:adderadder)
Whenwewishtodrawyourattentiontoaparticularpartofacodeblock,therelevantlinesoritemsaresetinbold:
(->(repeat-obs5)
(rx/subscribeprn-to-repl))
;;5
;;5
Anycommand-lineinputoroutputiswrittenasfollows:
leinrun-msin-wave.server
Newtermsandimportantwordsareshowninbold.Wordsthatyouseeonthescreen,inmenus,ordialogboxes,forexample,appearinthetextlikethis:“IfthiswasawebapplicationouruserswouldbepresentedwithawebservererrorsuchastheHTTPcode500–InternalServerError.”
NoteWarningsorimportantnotesappearinaboxlikethis.
TipTipsandtricksappearlikethis.
ReaderfeedbackFeedbackfromourreadersisalwayswelcome.Letusknowwhatyouthinkaboutthisbook—whatyoulikedormayhavedisliked.Readerfeedbackisimportantforustodeveloptitlesthatyoureallygetthemostoutof.
Tosendusgeneralfeedback,simplysendane-mailto<[email protected]>,andmentionthebooktitleviathesubjectofyourmessage.
Ifthereisatopicthatyouhaveexpertiseinandyouareinterestedineitherwritingorcontributingtoabook,seeourauthorguideatwww.packtpub.com/authors.
CustomersupportNowthatyouaretheproudownerofaPacktbook,wehaveanumberofthingstohelpyoutogetthemostfromyourpurchase.
DownloadingtheexamplecodeYoucandownloadtheexamplecodefilesforallPacktbooksyouhavepurchasedfromyouraccountathttp://www.packtpub.com.Ifyoupurchasedthisbookelsewhere,youcanvisithttp://www.packtpub.com/supportandregistertohavethefilese-maileddirectlytoyou.
ErrataAlthoughwehavetakeneverycaretoensuretheaccuracyofourcontent,mistakesdohappen.Ifyoufindamistakeinoneofourbooks—maybeamistakeinthetextorthecode—wewouldbegratefulifyouwouldreportthistous.Bydoingso,youcansaveotherreadersfromfrustrationandhelpusimprovesubsequentversionsofthisbook.Ifyoufindanyerrata,pleasereportthembyvisitinghttp://www.packtpub.com/submit-errata,selectingyourbook,clickingontheerratasubmissionformlink,andenteringthedetailsofyourerrata.Onceyourerrataareverified,yoursubmissionwillbeacceptedandtheerratawillbeuploadedonourwebsite,oraddedtoanylistofexistingerrata,undertheErratasectionofthattitle.Anyexistingerratacanbeviewedbyselectingyourtitlefromhttp://www.packtpub.com/support.
PiracyPiracyofcopyrightmaterialontheInternetisanongoingproblemacrossallmedia.AtPackt,wetaketheprotectionofourcopyrightandlicensesveryseriously.Ifyoucomeacrossanyillegalcopiesofourworks,inanyform,ontheInternet,pleaseprovideuswiththelocationaddressorwebsitenameimmediatelysothatwecanpursuearemedy.
Pleasecontactusat<[email protected]>withalinktothesuspectedpiratedmaterial.
Weappreciateyourhelpinprotectingourauthors,andourabilitytobringyouvaluablecontent.
QuestionsYoucancontactusat<[email protected]>ifyouarehavingaproblemwithanyaspectofthisbook,andwewilldoourbesttoaddressit.
Chapter1.WhatisReactiveProgramming?ReactiveProgrammingisbothanoverloadedtermandabroadtopic.Assuch,thisbookwillfocusonaspecificformulationofReactiveProgrammingcalledCompositionalEventSystems(CES).
BeforecoveringsomehistoryandbackgroundbehindReactiveProgrammingandCES,Iwouldliketoopenwithaworkingandhopefullycompellingexample:ananimationinwhichwedrawasinewaveontoawebpage.
Thesinewaveissimplythegraphrepresentationofthesinefunction.Itisasmooth,repetitiveoscillation,andattheendofouranimationitwilllooklikethefollowingscreenshot:
ThisexamplewillhighlighthowCES:
UrgesustothinkaboutwhatwewouldliketodoasopposedtohowEncouragessmall,specificabstractionsthatcanbecomposedtogetherProducesterseandmaintainablecodethatiseasytochange
ThecoreofthisprogramboilsdowntofourlinesofClojureScript:
(->sine-wave
(.take600)
(.subscribe(fn[{:keys[xy]}]
(fill-rectxy"orange"))))
Simplybylookingatthiscodeitisimpossibletodeterminepreciselywhatitdoes.However,dotakethetimetoreadandimaginewhatitcoulddo.
First,wehaveavariablecalledsine-wave,whichrepresentsthe2Dcoordinateswewilldrawontothewebpage.Thenextlinegivesustheintuitionthatsine-waveissomesortofcollection-likeabstraction:weuse.taketoretrieve600coordinatesfromit.
Finally,we.subscribetothis“collection”bypassingitacallback.Thiscallbackwillbecalledforeachiteminthesine-wave,finallydrawingatthegivenxandycoordinatesusingthefill-rectfunction.
Thisisquiteabittotakeinfornowaswehaven’tseenanyothercodeyet—butthatwas
thepointofthislittleexercise:eventhoughweknownothingaboutthespecificsofthisexample,weareabletodevelopanintuitionofhowitmightwork.
Let’sseewhatelseisnecessarytomakethissnippetanimateasinewaveonourscreen.
AtasteofReactiveProgrammingThisexampleisbuiltinClojureScriptandusesHTML5CanvasforrenderingandRxJS(seehttps://github.com/Reactive-Extensions/RxJS)—aframeworkforReactiveProgramminginJavaScript.
Beforewestart,keepinmindthatwewillnotgointothedetailsoftheseframeworksyet—thatwillhappenlaterinthisbook.ThismeansI’llbeaskingyoutotakequiteafewthingsatfacevalue,sodon’tworryifyoudon’timmediatelygrasphowthingswork.ThepurposeofthisexampleistosimplygetusstartedintheworldofReactiveProgramming.
Forthisproject,wewillbeusingChestnut(seehttps://github.com/plexus/chestnut)—aleiningentemplateforClojureScriptthatgivesusasampleworkingapplicationwecanuseasaskeleton.
Tocreateournewproject,headovertothecommandlineandinvokeleiningenasfollows:
leinnewchestnutsin-wave
cdsin-wave
Next,weneedtomodifyacoupleofthingsinthegeneratedproject.Openupsin-wave/resources/index.htmlandupdateittolooklikethefollowing:
<!DOCTYPEhtml>
<html>
<head>
<linkhref="css/style.css"rel="stylesheet"type="text/css">
</head>
<body>
<divid="app"></div>
<scriptsrc="/js/rx.all.js"type="text/javascript"></script>
<scriptsrc="/js/app.js"type="text/javascript"></script>
<canvasid="myCanvas"width="650"height="200"style="border:1pxsolid
#d3d3d3;">
</body>
</html>
ThissimplyensuresthatweimportbothourapplicationcodeandRxJS.Wehaven’tdownloadedRxJSyetsolet’sdothisnow.Browsetohttps://github.com/Reactive-Extensions/RxJS/blob/master/dist/rx.all.jsandsavethisfiletosin-wave/resources/public.TheprevioussnippetsalsoaddanHTML5Canvaselementontowhichwewillbedrawing.
Now,open/src/cljs/sin_wave/core.cljs.Thisiswhereourapplicationcodewilllive.Youcanignorewhatiscurrentlythere.Makesureyouhaveacleanslatelikethefollowingone:
(nssin-wave.core)
(defnmain[])
Finally,gobacktothecommandline—underthesin-wavefolder—andstartupthefollowingapplication:
leinrun-msin-wave.server
2015-01-0219:52:34.116:INFO:oejs.Server:jetty-7.6.13.v20130916
2015-01-0219:52:34.158:INFO:oejs.AbstractConnector:Started
[email protected]:10555
Startingfigwheel.
Startingwebserveronport10555.
CompilingClojureScript.
Figwheel:Startingserverathttp://localhost:3449
Figwheel:Servingfilesfrom'(dev-resources|resources)/public'
Oncethepreviouscommandfinishes,theapplicationwillbeavailableathttp://localhost:10555,whereyouwillfindablank,rectangularcanvas.Wearenowreadytobegin.
ThemainreasonweareusingtheChestnuttemplateforthisexampleisthatitperformshot-reloadingofourapplicationcodeviawebsockets.Thismeanswecanhavethebrowserandtheeditorsidebyside,andasweupdateourcode,wewillseetheresultsimmediatelyinthebrowserwithouthavingtoreloadthepage.
Tovalidatethatthisisworking,openyourwebbrowser’sconsolesothatyoucanseetheoutputofthescriptsinthepage.Thenaddthisto/src/cljs/sin_wave/core.cljsasfollows:
(.logjs/console"helloclojurescript")
Youshouldhaveseenthehelloclojurescriptmessageprintedtoyourbrowser’sconsole.Makesureyouhaveaworkingenvironmentuptothispointaswewillberelyingonthisworkflowtointeractivelybuildourapplication.
ItisalsoagoodideatomakesureweclearthecanvaseverytimeChestnutreloadsourfile.Thisissimpleenoughtodobyaddingthefollowingsnippettoourcorenamespace:
(defcanvas(.getElementByIdjs/document"myCanvas"))
(defctx(.getContextcanvas"2d"))
;;Clearcanvasbeforedoinganythingelse
(.clearRectctx00(.-widthcanvas)(.-heightcanvas))
CreatingtimeNowthatwehaveaworkingenvironment,wecanprogresswithouranimation.Itisprobablyagoodideatospecifyhowoftenwewouldliketohaveanewanimationframe.
Thiseffectivelymeansaddingtheconceptoftimetoourapplication.You’refreetoplaywithdifferentvalues,butlet’sstartwithanewframeevery10milliseconds:
(defintervaljs/Rx.Observable.interval)
(deftime(interval10))
AsRxJSisaJavaScriptlibrary,weneedtouseClojureScript’sinteroperabilitytocallitsfunctions.Forconvenience,webindtheintervalfunctionofRxJStoalocalvar.Wewillusethisapproachthroughoutthisbookwhenappropriate.
Next,wecreateaninfinitestreamofnumbers—startingat0—thatwillhaveanewelementevery10milliseconds.Let’smakesurethisisworkingasexpected:
(->time
(.take5)
(.subscribe(fn[n]
(.logjs/consolen))))
;;0
;;1
;;2
;;3
;;4
TipIusethetermstreamverylooselyhere.Itwillbedefinedmorepreciselylaterinthisbook.
Remembertimeisinfinite,soweuse.takeinordertoavoidindefinitelyprintingoutnumberstotheconsole.
Ournextstepistocalculatethe2Dcoordinaterepresentingasegmentofthesinewavewecandraw.Thiswillbegivenbythefollowingfunctions:
(defndeg-to-rad[n]
(*(/Math/PI180)n))
(defnsine-coord[x]
(let[sin(Math/sin(deg-to-radx))
y(-100(*sin90))]
{:xx
:yy
:sinsin}))
Thesine-coordfunctiontakesanxpointofour2DCanvasandcalculatestheypointbasedonthesineofx.Theconstants100and90simplycontrolhowtallandsharptheslopeshouldbe.Asanexample,trycalculatingthesinecoordinatewhenxis50:
(.logjs/console(str(sine-coord50)))
;;{:x50,:y31.05600011929198,:sin0.766044443118978}
Wewillbeusingtimeasthesourceforthevaluesofx.Creatingthesinewavenowisonlyamatterofcombiningbothtimeandsine-coord:
(defsine-wave
(.maptimesine-coord))
Justliketime,sine-waveisaninfinitestream.Thedifferenceisthatinsteadofjustintegers,wewillnowhavethexandycoordinatesofoursinewave,asdemonstratedinthefollowing:
(->sine-wave
(.take5)
(.subscribe(fn[xysin]
(.logjs/console(strxysin)))))
;;{:x0,:y100,:sin0}
;;{:x1,:y98.42928342064448,:sin0.01745240643728351}
;;{:x2,:y96.85904529677491,:sin0.03489949670250097}
;;{:x3,:y95.28976393813505,:sin0.052335956242943835}
;;{:x4,:y93.72191736302872,:sin0.0697564737441253}
Thisbringsustotheoriginalcodesnippetwhichpiquedourinterest,alongsideafunctiontoperformtheactualdrawing:
(defnfill-rect[xycolour]
(set!(.-fillStylectx)colour)
(.fillRectctxxy22))
(->sine-wave
(.take600)
(.subscribe(fn[{:keys[xy]}]
(fill-rectxy"orange"))))
Asthispoint,wecansavethefileagainandwatchasthesinewavewehavejustcreatedgracefullyappearsonthescreen.
MorecolorsOneofthepointsthisexamplesetsouttoillustrateishowthinkingintermsofverysimpleabstractionsandthenbuildingmorecomplexonesontopofthemmakeforcodethatissimplertomaintainandeasiertomodify.
Assuch,wewillnowupdateouranimationtodrawthesinewaveindifferentcolors.Inthiscase,wewouldliketodrawthewaveinredifthesineofxisnegativeandblueotherwise.
Wealreadyhavethesinevaluecomingthroughthesine-wavestream,soallweneedtodoistotransformthisstreamintoonethatwillgiveusthecolorsaccordingtotheprecedingcriteria:
(defcolour(.mapsine-wave
(fn[{:keys[sin]}]
(if(<sin0)
"red"
"blue"))))
Thenextstepistoaddthenewstreamintothemaindrawingloop—remembertocommentthepreviousonesothatwedon’tendupwithmultiplewavesbeingdrawnatthesametime:
(->(.zipsine-wavecolour#(vector%%2))
(.take600)
(.subscribe(fn[[{:keys[xy]}colour]]
(fill-rectxycolour))))
Oncewesavethefile,weshouldseeanewsinewavealternatingbetweenredandblueasthesineofxoscillatesfrom–1to1.
MakingitreactiveAsfunasthishasbeensofar,theanimationwehavecreatedisn’treallyreactive.Sure,itdoesreacttotimeitself,butthatistheverynatureofanimation.Aswewilllatersee,ReactiveProgrammingissocalledbecauseprogramsreacttoexternalinputssuchasmouseornetworkevents.
Wewill,therefore,updatetheanimationsothattheuserisincontrolofwhenthecolorswitchoccurs:thewavewillstartredandswitchtobluewhentheuserclicksanywherewithinthecanvasarea.Furtherclickswillsimplyalternatebetweenredandblue.
Westartbycreatinginfinite—asperthedefinitionoftime—streamsforourcolorprimitivesasfollows:
(defred(.maptime(fn[_]"red")))
(defblue(.maptime(fn[_]"blue")))
Ontheirown,redandbluearen’tthatinterestingastheirvaluesdon’tchange.Wecanthinkofthemasconstantstreams.Theybecomealotmoreinterestingwhencombinedwithanotherinfinitestreamthatcyclesbetweenthembasedonuserinput:
(defconcatjs/Rx.Observable.concat)
(defdeferjs/Rx.Observable.defer)
(deffrom-eventjs/Rx.Observable.fromEvent)
(defmouse-click(from-eventcanvas"click"))
(defcycle-colour
(concat(.takeUntilredmouse-click)
(defer#(concat(.takeUntilbluemouse-click)
cycle-colour))))
Thisisourmostcomplexupdatesofar.Ifyoulookclosely,youwillalsonoticethatcycle-colourisarecursivestream;thatis,itisdefinedintermsofitself.
Whenwefirstsawcodeofthisnature,wetookaleapoffaithintryingtounderstandwhatitdoes.Afteraquickread,however,werealizedthatcycle-colourfollowscloselyhowwemighthavetalkedabouttheproblem:wewillusereduntilamouseclickoccurs,afterwhichwewilluseblueuntilanothermouseclickoccurs.Then,westarttherecursion.
Thechangetoouranimationloopisminimal:
(->(.zipsine-wavecycle-colour#(vector%%2))
(.take600)
(.subscribe(fn[[{:keys[xy]}colour]]
(fill-rectxycolour))))
Thepurposeofthisbookistohelpyoudeveloptheinstinctrequiredtomodelproblemsinthewaydemonstratedhere.Aftereachchapter,moreandmoreofthisexamplewillmakesense.Additionally,anumberofframeworkswillbeusedbothinClojureScriptandClojuretogiveyouawiderangeoftoolstochoosefrom.
Exercise1.1Modifythepreviousexampleinsuchawaythatthesinewaveisdrawnusingallrainbowcolors.Thedrawingloopshouldlooklikethefollowing:
(->(.zipsine-waverainbow-colours#(vector%%2))
(.take600)
(.subscribe(fn[[{:keys[xy]}colour]]
(fill-rectxycolour))))
Yourtaskistoimplementtherainbow-coloursstream.Aseverythingupuntilnowhasbeenverylightonexplanations,youmightchoosetocomebacktothisexerciselater,oncewehavecoveredmoreaboutCES.
Therepeat,scan,andflatMapfunctionsmaybeusefulinsolvingthisexercise.BesuretoconsultRxJs’APIathttps://github.com/Reactive-Extensions/RxJS/blob/master/doc/libraries/rx.complete.md.
AbitofhistoryBeforewetalkaboutwhatReactiveProgrammingis,itisimportanttounderstandhowotherrelevantprogrammingparadigmsinfluencedhowwedevelopsoftware.Thiswillalsohelpusunderstandthemotivationsbehindreactiveprogramming.
Withfewexceptionsmostofushavebeentaught—eitherself-taughtoratschool/university—imperativeprogramminglanguagessuchasCandPascalorobject-orientedlanguagessuchasJavaandC++.
Inbothcases,theimperativeprogrammingparadigm—ofwhichobject-orientedlanguagesarepart—dictateswewriteprogramsasaseriesofstatementsthatmodifyprogramstate.
Inordertounderstandwhatthismeans,let’slookatashortprogramwritteninpseudo-codethatcalculatesthesumandthemeanvalueofalistofnumbers:
numbers:=[1,2,3,4,5,6]
sum:=0
foreachnumberinnumbers
sum:=sum+number
end
mean:=sum/count(numbers)
TipThemeanvalueistheaverageofthenumbersinthelist,obtainedbydividingthesumbythenumberofelements.
First,wecreateanewarrayofintegers,callednumbers,withnumbersfrom1to6,inclusive.Then,weinitializesumtozero.Next,weiterateoverthearrayofintegers,oneatatime,addingtosumthevalueofeachnumber.
Lastly,wecalculateandassigntheaverageofthenumbersinthelisttothemeanlocalvariable.Thisconcludestheprogramlogic.
Thisprogramwouldprint21forthesumand3forthemean,ifexecuted.
Thoughasimpleexample,ithighlightsitsimperativestyle:wesetupanapplicationstate—sum—andthenexplicitlytellthecomputerhowtomodifythatstateinordertocalculatetheresult.
DataflowprogrammingThepreviousexamplehasaninterestingproperty:thevalueofmeanclearlyhasadependencyonthecontentsofsum.
Dataflowprogrammingmakesthisrelationshipexplicit.Itmodelsapplicationsasadependencygraphthroughwhichdataflows—fromoperationtooperation—andasvalueschange,thesechangesarepropagatedtoitsdependencies.
Historically,dataflowprogramminghasbeensupportedbycustom-builtlanguagessuchasLucidandBLODI,assuch,leavingothergeneralpurposeprogramminglanguagesout.
Let’sseehowthisnewinsightwouldimpactourpreviousexample.Weknowthatoncethelastlinegetsexecuted,thevalueofmeanisassignedandwon’tchangeunlessweexplicitlyreassignthevariable.
However,let’simagineforasecondthatthepseudo-languageweusedearlierdoessupportdataflowprogramming.Inthatcase,assigningmeantoanexpressionthatreferstobothsumandcount,suchassum/count(numbers),wouldbeenoughtocreatethedirecteddependencygraphinthefollowingdiagram:
Notethatadirectsideeffectofthisrelationshipisthatanimplicitdependencyfromsumtonumbersisalsocreated.Thismeansthatifnumberschange,thechangeispropagatedthroughthegraph,firstupdatingsumandthenfinallyupdatingmean.
ThisiswhereReactiveProgrammingcomesin.Thisparadigmbuildsondataflowprogrammingandchangepropagationtobringthisstyleofprogrammingtolanguagesthatdon’thavenativesupportforit.
Forimperativeprogramminglanguages,ReactiveProgrammingcanbemadeavailablevialibrariesorlanguageextensions.Wedon’tcoverthisapproachinthisbook,butshouldthereaderwantmoreinformationonthesubject,pleaserefertodc-lib(seehttps://code.google.com/p/dc-lib/)foranexample.ItisaframeworkthataddsReactiveProgrammingsupporttoC++viadataflowconstraints.
Object-orientedReactiveProgrammingWhendesigninginteractiveapplicationssuchasdesktopGraphicalUserInterfaces(GUIs),weareessentiallyusinganobject-orientedapproachtoReactiveProgramming.Wewillbuildasimplecalculatorapplicationtodemonstratethisstyle.
TipClojureisn’tanobject-orientedlanguage,butwewillbeinteractingwithpartsoftheJavaAPItobuilduserinterfacesthatweredevelopedinanOOparadigm,hencethetitleofthissection.
Let’sstartbycreatinganewleiningenprojectfromthecommandline:
leinnewcalculator
Thiswillcreateadirectorycalledcalculatorinthecurrentfolder.Next,opentheproject.cljfileinyourfavoritetexteditorandaddadependencyonSeesaw,aClojurelibraryforworkingwithJavaSwing:
(defprojectcalculator"0.1.0-SNAPSHOT"
:description"FIXME:writedescription"
:url"http://example.com/FIXME"
:license{:name"EclipsePublicLicense"
:url"http://www.eclipse.org/legal/epl-v10.html"}
:dependencies[[org.clojure/clojure"1.5.1"]
[seesaw"1.4.4"]])
Atthetimeofthiswriting,thelatestSeesawversionavailableis1.4.4.
Next,inthesrc/calculator/core.cljfile,we’llstartbyrequiringtheSeesawlibraryandcreatingthevisualcomponentswe’llbeusing:
(nscalculator.core
(:require[seesaw.core:refer:all]))
(native!)
(defmain-frame(frame:title"Calculator":on-close:exit))
(deffield-x(text"1"))
(deffield-y(text"2"))
(defresult-label(label"Typenumbersintheboxestoaddthemup!"))
TheprecedingsnippetcreatesawindowwiththetitleCalculatorthatendstheprogramwhenclosed.Wealsocreatetwotextinputfields,field-xandfield-y,aswellasalabelthatwillbeusedtodisplaytheresults,aptlynamedresult-label.
Wewouldlikethelabeltobeupdatedautomaticallyassoonasausertypesanewnumberinanyoftheinputfields.Thefollowingcodedoesexactlythat:
(defnupdate-sum[e]
(try
(text!result-label
(str"Sumis"(+(Integer/parseInt(textfield-x))
(Integer/parseInt(textfield-y)))))
(catchExceptione
(println"Errorparsinginput."))))
(listenfield-x:key-releasedupdate-sum)
(listenfield-y:key-releasedupdate-sum)
Thefirstfunction,update-sum,isoureventhandler.Itsetsthetextofresult-labeltothesumofthevaluesinfield-xandfield-y.Weusetry/catchhereasareallybasicwaytohandleerrorssincethekeypressedmightnothavebeenanumber.Wethenaddtheeventhandlertothe:key-releasedeventofbothinputfields.
TipInrealapplications,weneverwantacatchblocksuchasthepreviousone.Thisisconsideredbadstyle,andthecatchblockshoulddosomethingmoreusefulsuchasloggingtheexception,firinganotification,orresumingtheapplicationifpossible.
Wearealmostdone.Allweneedtodonowisaddthecomponentswehavecreatedsofartoourmain-frameandfinallydisplayitasfollows:
(config!main-frame:content
(border-panel
:north(horizontal-panel:items[field-xfield-y])
:centerresult-label
:border5))
(defn-main[&args]
(->main-framepack!show!))
Nowwecansavethefileandruntheprogramfromthecommandlineintheproject’srootdirectory:
leinrun-mcalculator.core
Youshouldseesomethinglikethefollowingscreenshot:
Experimentbytypingsomenumbersineitherorbothtextinputfieldsandwatchhowthevalueofthelabelchangesautomatically,displayingthesumofbothnumbers.
Congratulations!Youhavejustcreatedyourfirstreactiveapplication!
Asalludedtopreviously,thisapplicationisreactivebecausethevalueoftheresultlabelreactstouserinputandisupdatedautomatically.However,thisisn’tthewholestory—itlacksincomposabilityandrequiresustospecifythehow,notthewhatofwhatwe’retryingtoachieve.
Asfamiliarasthisstyleofprogrammingmaybe,makingapplicationsreactivethiswayisn’talwaysideal.
Givenpreviousdiscussions,wenoticewestillhadtobefairlyexplicitinsettinguptherelationshipsbetweenthevariouscomponentsasevidencedbyhavingtowriteacustomhandlerandbindittobothinputfields.
Aswewillseethroughouttherestofthisbook,thereisamuchbetterwaytohandlesimilarscenarios.
ThemostwidelyusedreactiveprogramBothexamplesintheprevioussectionwillfeelfamiliartosomereaders.Ifwecalltheinputtextfields“cells”andtheresultlabel’shandlera“formula”,wenowhavethenomenclatureusedinmodernspreadsheetapplicationssuchasMicrosoftExcel.
ThetermReactiveProgramminghasonlybeeninuseinrecentyears,buttheideaofareactiveapplicationisn’tnew.Thefirstelectronicspreadsheetdatesbackto1969whenRenePardoandRemyLandau,thenrecentgraduatesfromHarvardUniversity,createdLANPAR(LANguageforProgrammingArraysatRandom)[1].
ItwasinventedtosolveaproblemthatBellCanadaandAT&Thadatthetime:theirbudgetingformshad2000cellsthat,whenmodified,forcedasoftwarere-writetakinganywherefromsixmonthstotwoyears.
Tothisday,electronicspreadsheetsremainapowerfulandusefultoolforprofessionalsofvariousfields.
TheObserverdesignpatternAnothersimilaritythekeenreadermayhavenoticediswiththeObserverdesignpattern.Itismainlyusedinobject-orientedapplicationsasawayforobjectstocommunicatewitheachotherwithouthavinganyknowledgeofwhodependsonitschanges.
InClojure,asimpleversionoftheObserverpatterncanbeimplementedusingwatches:
(defnumbers(atom[]))
(defnadder[keyrefold-statenew-state]
(print"Currentsumis"(reduce+new-state)))
(add-watchnumbers:adderadder)
Westartbycreatingourprogramstate,inthiscaseanatomholdinganemptyvector.Next,wecreateawatchfunctionthatknowshowtosumallnumbersinnumbers.Finally,weaddourwatchfunctiontothenumbersatomunderthe:adderkey(usefulforremovingwatches).
TheadderkeyconformswiththeAPIcontractrequiredbyadd-watchandreceivesfourarguments.Inthisexample,weonlycareaboutnew-state.
Now,wheneverweupdatethevalueofnumbers,itswatchwillbeexecuted,asdemonstratedinthefollowing:
(swap!numbersconj1)
;;Currentsumis1
(swap!numbersconj2)
;;Currentsumis3
(swap!numbersconj7)
;;Currentsumis10
Thehighlightedlinesaboveindicatetheresultthatisprintedonthescreeneachtimeweupdatetheatom.
Thoughuseful,theObserverpatternstillrequiressomeamountofworkinsettingupthedependenciesandtherequiredprogramstateinadditiontobeinghardtocompose.
Thatbeingsaid,thispatternhasbeenextendedandisatthecoreofoneoftheReactiveProgrammingframeworkswewilllookatlaterinthisbook,Microsoft’sReactiveExtensions(Rx).
FunctionalReactiveProgrammingJustlikeReactiveProgramming,FunctionalReactiveProgramming—FRPforshort—hasunfortunatelybecomeanoverloadedterm.
FrameworkssuchasRxJava(seehttps://github.com/ReactiveX/RxJava),ReactiveCocoa(seehttps://github.com/ReactiveCocoa/ReactiveCocoa),andBacon.js(seehttps://baconjs.github.io/)becameextremelypopularinrecentyearsandhadpositionedthemselvesincorrectlyasFRPlibraries.Thisledtotheconfusionsurroundingtheterminology.
Aswewillsee,theseframeworksdonotimplementFRPbutratherareinspiredbyit.
Intheinterestofusingthecorrectterminologyaswellasunderstandingwhat“inspiredbyFRP”means,wewillhaveabrieflookatthedifferentformulationsofFRP.
Higher-orderFRPHigher-orderFRPreferstotheoriginalresearchonFRPdevelopedbyConalElliottandPaulHudakintheirpaperFunctionalReactiveAnimation[2]from1997.ThispaperpresentsFran,adomain-specificlanguageembeddedinHaskellforcreatingreactiveanimations.Ithassincebeenimplementedinseverallanguagesasalibraryaswellaspurposebuiltreactivelanguages.
Ifyourecallthecalculatorexamplewecreatedafewpagesago,wecanseehowthatstyleofReactiveProgrammingrequiresustomanagestateexplicitlybydirectlyreadingandwritingfrom/totheinputfields.AsClojuredevelopers,weknowthatavoidingstateandmutabledataisagoodprincipletokeepinmindwhenbuildingsoftware.ThisprincipleisatthecoreofFunctionalProgramming:
(->>[123456]
(mapinc)
(filtereven?)
(reduce+))
;;12
Thisshortprogramincrementsbyoneallelementsintheoriginallist,filtersallevennumbers,andaddsthemupusingreduce.
Notehowwedidn’thavetoexplicitlymanagelocalstatethroughateachstepofthecomputation.
Differentlyfromimperativeprogramming,wefocusonwhatwewanttodo,forexampleiteration,andnothowwewantittobedone,forexampleusingaforloop.Thisiswhytheimplementationmatchesourdescriptionoftheprogramclosely.Thisisknownasdeclarativeprogramming.
FRPbringsthesamephilosophytoReactiveProgramming.AstheHaskellprogramminglanguagewikionthesubjecthaswiselyputit:
FRPisabouthandlingtime-varyingvaluesliketheywereregularvalues.
Putanotherway,FRPisadeclarativewayofmodelingsystemsthatrespondtoinputovertime.
Bothstatementstouchontheconceptoftime.We’llbeexploringthatinthenextsection,whereweintroducethekeyabstractionsprovidedbyFRP:signals(orbehaviors)andevents.
SignalsandeventsSofarwehavebeendealingwiththeideaofprogramsthatreacttouserinput.Thisisofcourseonlyasmallsubsetofreactivesystemsbutisenoughforthepurposesofthisdiscussion.
Userinputhappensseveraltimesthroughtheexecutionofaprogram:keypresses,mousedrags,andclicksarebutafewexamplesofhowausermightinteractwithoursystem.Alltheseinteractionshappenoveraperiodoftime.FRPrecognizesthattimeisanimportantaspectofreactiveprogramsandmakesitafirst-classcitizenthroughitsabstractions.
Bothsignals(alsocalledbehaviors)andeventsarerelatedtotime.Signalsrepresentcontinuous,time-varyingvalues.Events,ontheotherhand,representdiscreteoccurrencesatagivenpointintime.
Forexample,timeisitselfasignal.Itvariescontinuouslyandindefinitely.Ontheotherhand,akeypressbyauserisanevent,adiscreteoccurrence.
Itisimportanttonote,however,thatthesemanticsofhowasignalchangesneednotbecontinuous.Imagineasignalthatrepresentsthecurrent(x,y)coordinatesofyourmousepointer.
Thissignalissaidtochangediscretelyasitdependsontheusermovingthemousepointer—anevent—whichisn’tacontinuousaction.
ImplementationchallengesPerhapsthemostdefiningcharacteristicofclassicalFRPistheuseofcontinuoustime.
ThismeansFRPassumesthatsignalsarechangingallthetime,eveniftheirvalueisstillthesame,leadingtoneedlessrecomputation.Forexample,themousepositionsignalwilltriggerupdatestotheapplicationdependencygraph—liketheonewesawpreviouslyforthemeanprogram—evenwhenthemouseisstationary.
AnotherproblemisthatclassicalFRPissynchronousbydefault:eventsareprocessedinorder,oneatatime.Harmlessatfirst,thiscancausedelays,whichwouldrenderanapplicationunresponsiveshouldaneventtakesubstantiallylongertoprocess.
PaulHudakandothersfurtheredresearchonhigher-orderFRP[7][8]toaddresstheseissues,butthatcameatthecostofexpressivity.
TheotherformulationsofFRPaimtoovercometheseimplementationchallenges.
Throughouttherestofthechapter,I’llbeusingsignalsandbehaviorsinterchangeably.
First-orderFRPThemostwell-knownreactivelanguageinthiscategoryisElm(seehttp://elm-lang.org/),anFRPlanguagethatcompilestoJavaScript.ItwascreatedbyEvanCzaplickiandpresentedinhispaperElm:ConcurrentFRPforFunctionalGUIs[3].
Elmmakessomesignificantchangestohigher-orderFRP.
Itabandonstheideaofcontinuoustimeandisentirelyevent-driven.Asaresult,itsolvestheproblemofneedlessrecomputationhighlightedearlier.First-orderFRPcombinesbothbehaviorsandeventsintosignalswhich,incontrasttohigher-orderFRP,arediscrete.
Additionally,first-orderFRPallowstheprogrammertospecifywhensynchronousprocessingofeventsisn’tnecessary,preventingunnecessaryprocessingdelays.
Finally,Elmisastrictprogramminglanguage—meaningargumentstofunctionsareevaluatedeagerly—andthatisaconsciousdecisionasitpreventsspaceandtimeleaks,whicharepossibleinalazylanguagesuchasHaskell.
TipInanFRPlibrarysuchasFran,implementedinalazylanguage,memoryusagecangrowunwieldyascomputationsaredeferredtotheabsolutelylastpossiblemoment,thereforecausingaspaceleak.Theselargercomputations,accumulatedovertimeduetolaziness,canthencauseunexpecteddelayswhenfinallyexecuted,causingtimeleaks.
AsynchronousdataflowAsynchronousDataFlowgenerallyreferstoframeworkssuchasReactiveExtensions(Rx),ReactiveCocoa,andBacon.js.Itiscalledassuchasitcompletelyeliminatessynchronousupdates.
TheseframeworksintroducetheconceptofObservableSequences[4],sometimescalledEventStreams.
ThisformulationofFRPhastheadvantageofnotbeingconfinedtofunctionallanguages.Therefore,evenimperativelanguageslikeJavacantakeadvantageofthisstyleofprogramming.
Arguably,theseframeworkswereresponsiblefortheconfusionaroundFRPterminology.ConalElliottatsomepointsuggestedthetermCES(seehttps://twitter.com/conal/status/468875014461468677).
Ihavesinceadoptedthisterminology(seehttp://vimeo.com/100688924)asIbelieveithighlightstwoimportantfactors:
AfundamentaldifferencebetweenCESandFRP:CESisentirelyevent-drivenCESishighlycomposableviacombinators,takinginspirationfromFRP
CESisthemainfocusofthisbook.
ArrowizedFRPThisisthelastformulationwewilllookat.ArrowizedFRP[5]introducestwomaindifferencesoverhigher-orderFRP:itusessignalfunctionsinsteadofsignalsandisbuiltontopofJohnHughes’Arrowcombinators[6].
Itismostlyaboutadifferentwayofstructuringcodeandcanbeimplementedasalibrary.Asanexample,ElmsupportsArrowizedFRPviaitsAutomaton(seehttps://github.com/evancz/automaton)library.
TipThefirstdraftofthischaptergroupedthedifferentformulationsofFRPunderthebroadcategoriesofContinuousandDiscreteFRP.ThankstoEvanCzaplicki’sexcellenttalkControllingTimeandSpace:understandingthemanyformulationsofFRP(seehttps://www.youtube.com/watch?v=Agu6jipKfYw),Iwasabletoborrowthemorespecificcategoriesusedhere.ThesecomeinhandywhendiscussingthedifferentapproachestoFRP.
ApplicationsofFRPThedifferentFRPformulationsarebeingusedtodayinseveralproblemspacesbyprofessionalsandbigorganizationsalike.Throughoutthisbook,we’lllookatseveralexamplesofhowCEScanbeapplied.Someoftheseareinterrelatedasmostmodernprogramshaveseveralcross-cuttingconcerns,butwewillhighlighttwomainareas.
AsynchronousprogrammingandnetworkingGUIsareagreatexampleofasynchronousprogramming.Onceyouopenaweboradesktopapplication,itsimplysitsthere,idle,waitingforuserinput.
Thisstateisoftencalledtheeventormaineventloop.Itissimplywaitingforexternalstimuli,suchasakeypress,amousebuttonclick,newdatafromthenetwork,orevenasimpletimer.
Eachofthesestimuliisassociatedwithaneventhandlerthatgetscalledwhenoneoftheseeventshappen,hencetheasynchronousnatureofGUIsystems.
Thisisastyleofprogrammingwehavebeenusedtoformanyyears,butasbusinessanduserneedsgrow,theseapplicationsgrowincomplexityaswell,andbetterabstractionsareneededtohandlethedependenciesbetweenallthecomponentsofanapplication.
AnothergreatexamplethatdealswithmanagingcomplexityaroundnetworktrafficisNetflix,whichusesCEStoprovideareactiveAPItotheirbackendservices.
ComplexGUIsandanimationsGamesare,perhaps,thebestexampleofcomplexuserinterfacesastheyhaveintricaterequirementsarounduserinputandanimations.
TheElmlanguagewementionedbeforeisoneofthemostexcitingeffortsinbuildingcomplexGUIs.AnotherexampleisFlapjax,alsotargetedatwebapplications,butisprovidedasaJavaScriptlibrarythatcanbeintegratedwithexistingJavaScriptcodebases.
SummaryReactiveProgrammingisallaboutbuildingresponsiveapplications.Thereareseveralwaysinwhichwecanmakeourapplicationsreactive.Someareoldideas:dataflowprogramming,electronicspreadsheets,andtheObserverpatternareallexamples.ButCESinparticularhasbecomepopularinrecentyears.
CESaimstobringtoReactiveProgrammingthedeclarativewayofmodelingproblemsthatisatthecoreofFunctionalProgramming.Weshouldworryaboutwhatandnotabouthow.
Innextchapters,wewilllearnhowwecanapplyCEStoourownprograms.
Chapter2.ALookatReactiveExtensionsReactiveExtensions—orRx—isaReactiveProgramminglibraryfromMicrosofttobuildcomplexasynchronousprograms.Itmodelstime-varyingvaluesandeventsasobservablesequencesandisimplementedbyextendingtheObserverdesignpattern.
Itsfirsttargetplatformwas.NET,butNetflixhasportedRxtotheJVMunderthenameRxJava.MicrosoftalsodevelopsandmaintainsaportofRxtoJavaScriptcalledRxJS,whichisthetoolweusedtobuildthesine-waveapplication.ThetwoportsworkatreatforussinceClojurerunsontheJVMandClojureScriptinJavaScriptenvironments.
AswesawinChapter1,WhatisReactiveProgramming?,RxisinspiredbyFunctionalReactiveProgrammingbutusesdifferentterminology.InFRP,thetwomainabstractionsarebehaviorsandevents.Althoughtheimplementationdetailsaredifferent,observablesequencesrepresentevents.Rxalsoprovidesabehavior-likeabstractionthroughanotherdatatypecalledBehaviorSubject.
Inthischapter,wewill:
ExploreRx’smainabstraction:observablesLearnaboutthedualitybetweeniteratorsandobservablesCreateandmanipulateobservablesequences
TheObserverpatternrevisitedInChapter1,WhatisReactiveProgramming?,wesawabriefoverviewoftheObserverdesignpatternandasimpleimplementationofitinClojureusingwatches.Here’showwedidit:
(defnumbers(atom[]))
(defnadder[keyrefold-statenew-state]
(print"Currentsumis"(reduce+new-state)))
(add-watchnumbers:adderadder)
Intheprecedingexample,ourobservablesubjectisthevar,numbers.Theobserveristheadderwatch.Whentheobservablechanges,itpushesitschangestotheobserversynchronously.
Now,contrastthistoworkingwithsequences:
(->>[123456]
(mapinc)
(filtereven?)
(reduce+))
Thistimearound,thevectoristhesubjectbeingobservedandthefunctionsprocessingitcanbethoughtofastheobservers.However,thisworksinapull-basedmodel.Thevectordoesn’tpushanyelementsdownthesequence.Instead,mapandfriendsaskthesequenceformoreelements.Thisisasynchronousoperation.
Rxmakessequences—andmore—behavelikeobservablessothatyoucanstillmap,filter,andcomposethemjustasyouwouldcomposefunctionsovernormalsequences.
Observer–anIterator’sdualClojure’ssequenceoperatorssuchasmap,filter,reduce,andsoonsupportJavaIterables.Asthenameimplies,anIterableisanobjectthatcanbeiteratedover.Atalowlevel,thisissupportedbyretrievinganIteratorreferencefromsuchobject.Java’sIteratorinterfacelookslikethefollowing:
publicinterfaceIterator<E>{
booleanhasNext();
Enext();
voidremove();
}
Whenpassedinanobjectthatimplementsthisinterface,Clojure’ssequenceoperatorspulldatafromitbyusingthenextmethod,whileusingthehasNextmethodtoknowwhentostop.
TipTheremovemethodisrequiredtoremoveitslastelementfromtheunderlyingcollection.Thisin-placemutationisclearlyunsafeinamultithreadedenvironment.WheneverClojureimplementsthisinterfaceforthepurposesofinteroperability,theremovemethodsimplythrowsUnsupportedOperationException.
Anobservable,ontheotherhand,hasobserverssubscribedtoit.Observershavethefollowinginterface:
publicinterfaceObserver<T>{
voidonCompleted();
voidonError(Throwablee);
voidonNext(Tt);
}
Aswecansee,anObserverimplementingthisinterfacewillhaveitsonNextmethodcalledwiththenextvalueavailablefromwhateverobservableit’ssubscribedto.Hence,itbeingapush-basednotificationmodel.
Thisduality[4]becomesclearerifwelookatboththeinterfacessidebyside:
Iterator<E>{Observer<T>{
booleanhasNext();voidonCompleted();
Enext();voidonError(Throwablee);
voidremove();voidonNext(Tt);
}}
Observablesprovidetheabilitytohaveproducerspushitemsasynchronouslytoconsumers.Afewexampleswillhelpsolidifyourunderstanding.
CreatingObservablesThischapterisallaboutReactiveExtensions,solet’sgoaheadandcreateaprojectcalledrx-playgroundthatwewillbeusinginourexploratorytour.WewilluseRxClojure(seehttps://github.com/ReactiveX/RxClojure),alibrarythatprovidesClojurebindingsforRxJava()(seehttps://github.com/ReactiveX/RxJava):
$leinnewrx-playground
OpentheprojectfileandaddadependencyonRxJava’sClojurebindings:
(defprojectrx-playground"0.1.0-SNAPSHOT"
:description"FIXME:writedescription"
:url"http://example.com/FIXME"
:license{:name"EclipsePublicLicense"
:url"http://www.eclipse.org/legal/epl-v10.html"}
:dependencies[[org.clojure/clojure"1.5.1"]
[io.reactivex/rxclojure"1.0.0"]])"]])
Now,fireupaREPLintheproject’srootdirectorysothatwecanstartcreatingsomeobservables:
$leinrepl
ThefirstthingweneedtodoisimportRxClojure,solet’sgetthisoutofthewaybytypingthefollowingintheREPL:
(require'[rx.lang.clojure.core:asrx])
(import'(rxObservable))
Thesimplestwaytocreateanewobservableisbycallingthejustreturnfunction:
(defobs(rx/return10))
Now,wecansubscribetoit:
(rx/subscribeobs
(fn[value]
(prn(str"Gotvalue:"value))))
Thiswillprintthestring"Gotvalue:10"totheREPL.
Thesubscribefunctionofanobservableallowsustoregisterhandlersforthreemainthingsthathappenthroughoutitslifecycle:newvalues,errors,oranotificationthattheobservableisdoneemittingvalues.ThiscorrespondstotheonNext,onError,andonCompletedmethodsoftheObserverinterface,respectively.
Intheprecedingexample,wearesimplysubscribingtoonNext,whichiswhywegetnotifiedabouttheobservable’sonlyvalue,10.
Asingle-valueObservableisn’tterriblyinterestingthough.Let’screateandinteractwithonethatemitsmultiplevalues:
(->(rx/seq->o[12345678910])
(rx/subscribeprn))
Thiswillprintthenumbersfrom1to10,inclusive,totheREPL.seq->oisawaytocreateobservablesfromClojuresequences.ItjustsohappensthattheprecedingsnippetcanberewrittenusingRx’sownrangeoperator:
(->(rx/range110)
(rx/subscribeprn))
Ofcourse,thisdoesn’tyetpresentanyadvantagestoworkingwithrawvaluesorsequencesinClojure.
Butwhatifweneedanobservablethatemitsanundefinednumberofintegersatagiveninterval?ThisbecomeschallengingtorepresentasasequenceinClojure,butRxmakesittrivial:
(import'(java.util.concurrentTimeUnit))
(rx/subscribe(Observable/interval100TimeUnit/MILLISECONDS)
prn-to-repl)
TipRxClojuredoesn’tyetprovidebindingstoallofRxJava’sAPI.Theintervalmethodisonesuchexample.We’rerequiredtouseinteroperabilityandcallthemethoddirectlyontheObservableclassfromRxJava.
Observable/intervaltakesasargumentsanumberandatimeunit.Inthiscase,wearetellingittoemitaninteger—startingfromzero—every100milliseconds.IfwetypethisinanREPL-connectededitor,however,twothingswillhappen:
Wewillnotseeanyoutput(dependingonyourREPL;thisistrueforEmacs)Wewillhavearoguethreademittingnumbersindefinitely
BothissuesarisefromthefactthatObservable/intervalisthefirstfactorymethodwehaveusedthatdoesn’temitvaluessynchronously.Instead,itreturnsanObservablethatdeferstheworktoaseparatethread.
Thefirstissueissimpleenoughtofix.Functionssuchasprnwillprinttowhateverthedynamicvar*out*isboundto.WhenworkingincertainREPLenvironmentssuchasEmacs’,thisisboundtotheREPLstream,whichiswhywecangenerallyseeeverythingweprint.
However,sinceRxisdeferringtheworktoaseparatethread,*out*isn’tboundtotheREPLstreamanymoresowedon’tseetheoutput.Inordertofixthis,weneedtocapturethecurrentvalueof*out*andbinditinoursubscription.ThiswillbeincrediblyusefulasweexperimentwithRxintheREPL.Assuch,let’screateahelperfunctionforit:
(defrepl-out*out*)
(defnprn-to-repl[&args]
(binding[*out*repl-out]
(applyprnargs)))
Thefirstthingwedoiscreateavarrepl-outthatcontainsthecurrentREPLstream.Next,wecreateafunctionprn-to-replthatworksjustlikeprn,exceptitusesthebindingmacrotocreateanewbindingfor*out*thatisvalidwithinthatscope.
Thisstillleavesuswiththeroguethreadproblem.NowistheappropriatetimetomentionthatthesubscribemethodfromanObservablereturnsasubscriptionobject.Byholdingontoareferencetoit,wecancallitsunsubscribemethodtoindicatethatwearenolongerinterestedinthevaluesproducedbythatobservable.
Puttingitalltogether,ourintervalexamplecanberewrittenlikethefollowing:
(defsubscription(rx/subscribe(Observable/interval100
TimeUnit/MILLISECONDS)
prn-to-repl))
(Thread/sleep1000)
(rx/unsubscribesubscription)
Wecreateanewintervalobservableandimmediatelysubscribetoit,justaswedidbefore.Thistime,however,weassigntheresultingsubscriptiontoalocalvar.Notethatitnowusesourhelperfunctionprn-to-repl,sowewillstartseeingvaluesbeingprintedtotheREPLstraightaway.
Next,wesleepthecurrent—theREPL—threadforasecond.ThisisenoughtimefortheObservabletoproducenumbersfrom0to9.That’sroughlywhentheREPLthreadwakesupandunsubscribesfromthatobservable,causingittostopemittingvalues.
CustomObservablesRxprovidesmanymorefactorymethodstocreateObservables(seehttps://github.com/ReactiveX/RxJava/wiki/Creating-Observables),butitisbeyondthescopeofthisbooktocoverthemall.
Nevertheless,sometimes,noneofthebuilt-infactoriesiswhatyouwant.Forsuchcases,Rxprovidesthecreatemethod.Wecanuseittocreateacustomobservablefromscratch.
Asanexample,we’llcreateourownversionofthejustobservableweusedearlierinthischapter:
(defnjust-obs[v]
(rx/observable*
(fn[observer]
(rx/on-nextobserverv)
(rx/on-completedobserver))))
(rx/subscribe(just-obs20)prn)
First,wecreateafunction,just-obs,whichimplementsourobservablebycallingtheobservable*function.
Whencreatinganobservablethisway,thefunctionpassedtoobservable*willgetcalledwithanobserverassoonasonesubscribestous.Whenthishappens,wearefreetodowhatevercomputation—andevenI/O—weneedinordertoproducevaluesandpushthemtotheobserver.
Weshouldremembertocalltheobserver’sonCompletedmethodwheneverwe’redoneproducingvalues.Theprecedingsnippetwillprint20totheREPL.
TipWhilecreatingcustomobservablesisfairlystraightforward,weshouldmakesureweexhaustthebuilt-infactoryfunctionsfirst,onlythenresortingtocreatingourown.
ManipulatingObservablesNowthatweknowhowtocreateobservables,weshouldlookatwhatkindsofinterestingthingswecandowiththem.Inthissection,wewillseewhatitmeanstotreatObservablesassequences.
We’llstartwithsomethingsimple.Let’sprintthesumofthefirstfivepositiveevenintegersfromanobservableofallintegers:
(rx/subscribe(->>(Observable/interval1TimeUnit/MICROSECONDS)
(rx/filtereven?)
(rx/take5)
(rx/reduce+))
prn-to-repl)
Thisisstartingtolookawfullyfamiliartous.Wecreateanintervalthatwillemitallpositiveintegersstartingatzeroevery1microsecond.Then,wefilterallevennumbersinthisobservable.Obviously,thisistoobigalisttohandle,sowesimplytakethefirstfiveelementsfromit.Finally,wereducethevalueusing+.Theresultis20.
Todrivehomethepointthatprogrammingwithobservablesreallyisjustlikeoperatingonsequences,wewilllookatonemoreexamplewherewewillcombinetwodifferentObservablesequences.OnecontainsthenamesofmusiciansI’mafanofandtheotherthenamesoftheirrespectivebands:
(defnmusicians[]
(rx/seq->o["JamesHetfield""DaveMustaine""KerryKing"]))
(defnbands[]
(rx/seq->o["Metallica""Megadeth""Slayer"]))
WewouldliketoprinttotheREPLastringoftheformatMusicianname–from:bandname.Anaddedrequirementisthatthebandnamesshouldbeprintedinuppercaseforimpact.
We’llstartbycreatinganotherobservablethatcontainstheuppercasedbandnames:
(defnuppercased-obs[]
(rx/map(fn[s](.toUpperCases))(bands)))
Whilenotstrictlynecessary,thismakesareusablepieceofcodethatcanbehandyinseveralplacesoftheprogram,thusavoidingduplication.Subscribersinterestedintheoriginalbandnamescankeepsubscribingtothebandsobservable.
Withthetwoobservablesinhand,wecanproceedtocombinethem:
(->(rx/mapvector
(musicians)
(uppercased-obs))
(rx/subscribe(fn[[musicianband]]
(prn-to-repl(strmusician"-from:"band)))))
Oncemore,thisexampleshouldfeelfamiliar.Thesolutionwewereafterwasawaytozip
thetwoobservablestogether.RxClojureprovideszipbehaviorthroughmap,muchlikeClojure’scoremapfunctiondoes.Wecallitwiththreearguments:thetwoobservablestozipandafunctionthatwillbecalledwithbothelements,onefromeachobservable,andshouldreturnanappropriaterepresentation.Inthiscase,wesimplyturnthemintoavector.
Next,inoursubscriber,wesimplydestructurethevectorinordertoaccessthemusicianandbandnames.WecanfinallyprintthefinalresulttotheREPL:
"JamesHetfield-from:METALLICA"
"DaveMustaine-from:MEGADETH"
"KerryKing-from:SLAYER"
FlatmapandfriendsIntheprevioussection,welearnedhowtotransformandcombineobservableswithoperationssuchasmap,reduce,andzip.However,thetwoobservablesabove—musiciansandbands—wereperfectlycapableofproducingvaluesontheirown.Theydidnotneedanyextrainput.
Inthissection,weexamineadifferentscenario:we’lllearnhowwecancombineobservables,wheretheoutputofoneistheinputofanother.WeencounteredflatmapbeforeinChapter1,WhatisReactiveProgramming?Ifyouhavebeenwonderingwhatitsroleis,thissectionaddressesexactlythat.
Here’swhatwearegoingtodo:givenanobservablerepresentingalistofallpositiveintegers,we’llcalculatethefactorialforallevennumbersinthatlist.Sincethelististoobig,we’lltakefiveitemsfromit.Theendresultshouldbethefactorialsof0,2,4,6,and8,respectively.
Thefirstthingweneedisafunctiontocalculatethefactorialofanumbern,aswellasourobservable:
(defnfactorial[n]
(reduce*(range1(incn))))
(defnall-positive-integers[]
(Observable/interval1TimeUnit/MICROSECONDS))
Usingsometypeofvisualaidwillbehelpfulinthissection,sowe’llstartwithamarblediagramrepresentingthepreviousobservable:
Themiddlearrowrepresentstimeanditflowsfromlefttoright.ThisdiagramrepresentsaninfiniteObservablesequence,asindicatedbytheuseofellipsisattheendofit.
Sincewe’recombiningalltheobservablesnow,we’llcreateonethat,givenanumber,emitsitsfactorialusingthehelperfunctiondefinedearlier.We’lluseRx’screatemethodforthispurpose:
(defnfact-obs[n]
(rx/observable*
(fn[observer]
(rx/on-nextobserver(factorialn))
(rx/on-completedobserver))))
Thisisverysimilartothejust-obsobservablewecreatedearlierinthischapter,exceptthatitcalculatesthefactorialofitsargumentandemitstheresult/factorialinstead,endingthesequenceimmediatelythereafter.Thefollowingdiagramillustrateshowitworks:
Wefeedthenumber5totheobservable,whichinturnemitsitsfactorial,120.Theverticalbarattheendofthetimelineindicatesthesequenceterminatesthen.
Runningthecodeconfirmsthatourfunctioniscorrect:
(rx/subscribe(fact-obs5)prn-to-repl)
;;120
Sofarsogood.Now,weneedtocombinebothobservablesinordertoachieveourgoal.ThisiswhereflatmapofRxcomesin.We’llfirstseeitinactionandthengetintotheexplanation:
(rx/subscribe(->>(all-positive-integers)
(rx/filtereven?)
(rx/flatmapfact-obs)
(rx/take5))
prn-to-repl)
Ifweruntheprecedingcode,itwillprintthefactorialsfor0,2,4,6,and8,justlikewewanted:
1
2
24
720
40320
Mostoftheprecedingcodesnippetshouldlookfamiliar.Thefirstthingwedoisfilterallevennumbersfromall-positive-numbers.Thisleavesuswiththefollowingobservablesequence:
Muchlikeall-positive-integers,this,too,isaninfiniteobservable.
However,thenextlineofourcodelooksalittleodd.Wecallflatmapandgiveitthefact-obsfunction.Afunctionweknowitselfreturnsanotherobservable.flatmapwillcallfact-obswitheachvalueitemits.fact-obswill,inturn,returnasingle-valueobservableforeachnumber.However,oursubscriberdoesn’tknowhowtodealwithobservables!It’ssimplyinterestedinthefactorials!
Thisiswhy,aftercallingfact-obstoobtainanobservable,flatmapflattensallofthemintoasingleobservablewecansubscribeto.Thisisquiteamouthful,solet’svisualizewhatthismeans:
Asyoucanseeintheprecedingdiagram,throughouttheexecutionofflatmap,weendupwithalistofobservables.However,wedon’tcareabouteachobservablebutratheraboutthevaluestheyemit.Flatmap,then,istheperfecttoolasitcombines—flattens—allofthemintotheobservablesequenceshownatthebottomofthefigure.
Youcanthinkofflatmapasmapcatforobservablesequences.
Therestofthecodeisstraightforward.Wesimplytakethefirstfiveelementsfromthisobservableandsubscribetoit,aswehavebeendoingsofar.
OnemoreflatmapfortheroadYoumightbewonderingwhatwouldhappeniftheobservablesequencewe’reflatmappingemittedmorethanonevalue.Whatthen?
We’llseeonelastexamplebeforewebeginthenextsectioninordertoillustratethebehaviorofflatMapinsuchcases.
Here’sanobservablethatemitsitsargumenttwice:
(defnrepeat-obs[n]
(rx/seq->o(repeat2n)))
Usingitisstraightforward:
(->(repeat-obs5)
(rx/subscribeprn-to-repl))
;;5
;;5
Aspreviously,we’llnowcombinethisobservablewiththeonewecreatedearlier,all-positive-integers.Beforereadingon,thinkaboutwhatyouexpecttheoutputtobefor,say,thefirstthreepositiveintegers.
Thecodeisasfollows:
(rx/subscribe(->>(all-positive-integers)
(rx/flatmaprepeat-obs)
(rx/take6))
prn-to-repl)
Andtheoutputisasfollows:
0
0
1
1
2
2
Theresultmightbeunexpectedforsomereaders.Let’shavealookatthemarblediagramforthisexampleandmakesureweunderstandhowitworks:
Eachtimerepeat-obsgetscalled,itemitstwovaluesandterminates.flatmapthencombinesthemallinasingleobservable,makingthepreviousoutputclearer.
Onelastthingworthmentioningaboutflatmap—andthetitleofthissection—isthatits“friends”refertotheseveralnamesbywhichflatmapisknown.
Forinstance,Rx.NETcallsitselectMany.RxJavaandScalacallitflatMap—thoughRxJavahasanaliasforitcalledmapMany.TheHaskellcommunitycallsitbind.Thoughtheyhavedifferentnames,thesefunctionssemanticsarethesameandarepartofahigher-orderabstractioncalledaMonad.Wedon’tneedtoknowanythingaboutMonadstoproceed.
Theimportantthingtokeepinmindisthatwhenyou’resittingatthebartalkingtoyourfriendsaboutCompositionalEventSystems,allthesenamesmeanthesamething.
ErrorhandlingAveryimportantaspectofbuildingreliableapplicationsisknowingwhattodowhenthingsgowrong.Itisnaivetoassumethatthenetworkisreliable,thathardwarewon’tfail,orthatwe,asdevelopers,won’tmakemistakes.
RxJavaembracesthisfactandprovidesarichsetofcombinatorstodealwithfailure,afewofwhichweexaminehere.
OnErrorLet’sgetstartedbycreatingabadlybehavedobservablethatalwaysthrowsanexception:
(defnexceptional-obs[]
(rx/observable*
(fn[observer]
(rx/on-nextobserver(throw(Exception."Oops.Somethingwent
wrong")))
(rx/on-completedobserver))))
Nowlet’swatchwhathappensifwesubscribetoit:
(rx/subscribe(->>(exceptional-obs)
(rx/mapinc))
(fn[v](prn-to-repl"resultis"v)))
;;ExceptionOops.Somethingwentwrongrx-playground.core/exceptional-
obs/fn--1505
Theexceptionthrownbyexceptional-obsisn’tcaughtanywheresoitsimplybubblesuptotheREPL.IfthiswasawebapplicationouruserswouldbepresentedwithawebservererrorsuchastheHTTPcode500–InternalServerError.Thoseuserswouldprobablynotuseoursystemagain.
Ideally,wewouldliketogetachancetohandlethisexceptiongracefully,possiblyrenderingafriendlyerrormessagethatwillletoursusersknowwecareaboutthem.
Aswehaveseenearlierinthechapter,thesubscribefunctioncantakeupto3functionsasarguments:
Thefirst,ortheonNexthandler,iscalledwhentheobservableemitsanewvalueThesecond,oronError,iscalledwhenevertheobservablethrowsanexceptionThethirdandlastfunction,oronComplete,iscalledwhentheobservablehascompletedandwillnotemitanynewitems
ForourpurposesweareinterestedintheonErrorhandler,andusingitisstraightforward:
(rx/subscribe(->>(exceptional-obs)
(rx/mapinc))
(fn[v](prn-to-repl"resultis"v))
(fn[e](prn-to-repl"erroris"e)))
;;"erroris"#<Exceptionjava.lang.Exception:Oops.Somethingwentwrong>
Thistime,insteadofthrowingtheexception,ourerrorhandlergetscalledwithit.Thisgivesustheopportunitytodisplayanappropriatemessagetoourusers.
CatchTheuseofonErrorgivesusamuchbetterexperienceoverallbutitisn’tveryflexible.
Let’simagineadifferentscenariowherewehaveanobservableretrievingdatafromthenetwork.Whatif,whenthisobserverfails,wewouldliketopresenttheuserwithacachedvalueinsteadofanerrormessage?
Thisiswherethecatchcombinatorcomesin.Itallowsustospecifyafunctiontobeinvokedwhentheobservablethrowsanexception,muchlikeOnErrordoes.
DifferentlyfromOnError,however,catchhastoreturnanewObservablethatwillbethenewsourceofitemsfromthemomenttheexceptionwasthrown:
(rx/subscribe(->>(exceptional-obs)
(rx/catchExceptione
(rx/return10))
(rx/mapinc))
(fn[v](prn-to-repl"resultis"v)))
;;"resultis"11
Inthepreviousexample,weareessentiallyspecifyingthat,wheneverexceptional-obsthrows,weshouldreturnthevalue10.Wearenotlimitedtosinglevalues,however.Infact,wecanuseanyObservablewelikeasthenewsource:
(rx/subscribe(->>(exceptional-obs)
(rx/catchExceptione
(rx/seq->o(range5)))
(rx/mapinc))
(fn[v](prn-to-repl"resultis"v)))
;;"resultis"1
;;"resultis"2
;;"resultis"3
;;"resultis"4
;;"resultis"5
RetryThelasterrorhandlingcombinatorwe’llexamineisretry.ThiscombinatorisusefulwhenweknowanerrororexceptionisonlytransientsoweshouldprobablygiveitanothershotbyresubscribingtotheObservable.
First,we’llcreateanobservablethatfailswhenitissubscribedtoforthefirsttime.However,thenexttimeitissubscribedto,itsucceedsandemitsanewitem:
(defnretry-obs[]
(let[errored(atomfalse)]
(rx/observable*
(fn[observer]
(if@errored
(rx/on-nextobserver20)
(do(reset!erroredtrue)
(throw(Exception."Oops.Somethingwentwrong"))))))))
Let’sseewhathappensifwesimplysubscribetoit:
(rx/subscribe(retry-obs)
(fn[v](prn-to-repl"resultis"v)))
;;ExceptionOops.Somethingwentwrongrx-playground.core/retry-obs/fn-
-1476
Asexpected,theexceptionsimplybubblesupasinourfirstexample.Howeverweknow—forthepurposesofthisexample—thatthisisatransientfailure.Let’sseewhatchangesifweuseretry:
(rx/subscribe(->>(retry-obs)
(.retry))
(fn[v](prn-to-repl"resultis"v)))
;;"resultis"20
Now,ourcodeisresponsibleforretryingtheObservableandasexpectedwegetthecorrectoutput.
It’simportanttonotethatretrywillattempttoresubscribeindefinitelyuntilitsucceeds.ThismightnotbewhatyouwantsoRxprovidesavariation,calledretryWith,whichallowsustospecifyapredicatefunctionthatcontrolswhenandifretryingshouldstop.
Alltheseoperatorsgiveusthetoolsweneedtobuildreliablereactiveapplicationsandweshouldalwayskeeptheminmindastheyare,withoutadoubt,agreatadditiontoourtoolbox.TheRxJavawikionthesubjectshouldbereferredtoformoreinformation:https://github.com/ReactiveX/RxJava/wiki/Error-Handling-Operators.
BackpressureAnotherissuewemightbefacedwithistheoneofobservablesthatproduceitemsfasterthanwecanconsume.Theproblemthatarisesinthisscenarioiswhattodowiththisever-growingbacklogofitems.
Asanexample,thinkaboutzippingtwoobservablestogether.Thezipoperator(ormapinRxClojure)willonlyemitanewvaluewhenallobservableshaveemittedanitem.
Soifoneoftheseobservablesisalotfasteratproducingitemsthantheothers,mapwillneedtobuffertheseitemsandwaitfortheothers,whichwillmostlikelycauseanerror,asshownhere:
(defnfast-producing-obs[]
(rx/mapinc(Observable/interval1TimeUnit/MILLISECONDS)))
(defnslow-producing-obs[]
(rx/mapinc(Observable/interval500TimeUnit/MILLISECONDS)))
(rx/subscribe(->>(rx/mapvector
(fast-producing-obs)
(slow-producing-obs))
(rx/map(fn[[xy]]
(+xy)))
(rx/take10))
prn-to-repl
(fn[e](prn-to-repl"erroris"e)))
;;"erroris"#<MissingBackpressureException
rx.exceptions.MissingBackpressureException>
Asseenintheprecedingcode,wehaveafastproducingobservablethatemitsitems500timesfasterthantheslowerObservable.Clearly,wecan’tkeepupwithitandsurelyenough,RxthrowsMissingBackpressureException.
Whatthisexceptionistellingusisthatthefastproducingobservabledoesn’tsupportanytypeofbackpressure—whatRxcallsReactivepullbackpressure—thatis,consumerscan’ttellittogoslower.ThankfullyRxprovidesuswithcombinatorsthatarehelpfulinthesescenarios.
SampleOnesuchcombinatorissample,whichallowsustosampleanobservableatagiveninterval,thusthrottlingthesourceobservable’soutput.Let’sapplyittoourpreviousexample:
(rx/subscribe(->>(rx/mapvector
(.sample(fast-producing-obs)200
TimeUnit/MILLISECONDS)
(slow-producing-obs))
(rx/map(fn[[xy]]
(+xy)))
(rx/take10))
prn-to-repl
(fn[e](prn-to-repl"erroris"e)))
;;204
;;404
;;604
;;807
;;1010
;;1206
;;1407
;;1613
;;1813
;;2012
TheonlychangeisthatwecallsampleonourfastproducingObservablebeforecallingmap.Wewillsampleitevery200milliseconds.
Byignoringallotheritemsemittedinthistimeslice,wehavemitigatedourinitialproblem,eventhoughtheoriginalObservabledoesn’tsupportanyformofbackpressure.
Thesamplecombinatorisonlyoneofthecombinatorsusefulinsuchcases.OthersincludethrottleFirst,debounce,buffer,andwindow.Onedrawbackofthisapproach,however,isthatalotoftheitemsgeneratedendupbeingignored.
Dependingonthetypeofapplicationwearebuilding,thismightbeanacceptablecompromise.Butwhatifweareinterestedinallitems?
BackpressurestrategiesIfanObservabledoesn’tsupportbackpressurebutwearestillinterestedinallitemsitemits,wecanuseoneofthebuilt-inbackpressurecombinatorsprovidedbyRx.
Asanexamplewewilllookatonesuchcombinator,onBackpressureBuffer:
(rx/subscribe(->>(rx/mapvector
(.onBackpressureBuffer(fast-producing-obs))
(slow-producing-obs))
(rx/map(fn[[xy]]
(+xy)))
(rx/take10))
prn-to-repl
(fn[e](prn-to-repl"erroris"e)))
;;2
;;4
;;6
;;8
;;10
;;12
;;14
;;16
;;18
;;20
Theexampleisverysimilartotheonewhereweusedsample,buttheoutputisfairlydifferent.Thistimewegetallitemsemittedbybothobservables.
TheonBackpressureBufferstrategyimplementsastrategythatsimplybuffersallitemsemittedbytheslowerObservable,emittingthemwhenevertheconsumerisready.Inourcase,thathappensevery500milliseconds.
OtherstrategiesincludeonBackpressureDropandonBackpressureBlock.
It’sworthnotingthatReactivepullbackpressureisstillworkinprogressandthebestwaytokeepuptodatewithprogressisontheRxJavawikionthesubject:https://github.com/ReactiveX/RxJava/wiki/Backpressure.
SummaryInthischapter,wetookadeepdiveintoRxJava,aportformMicrosoft’sReactiveExtensionsfrom.NET.Welearnedaboutitsmainabstraction,theobservable,andhowitrelatestoiterables.
Wealsolearnedhowtocreate,manipulate,andcombineobservablesinseveralways.Theexamplesshownherewerecontrivedtokeepthingssimple.Nevertheless,allconceptspresentedareextremelyusefulinrealapplicationsandwillcomeinhandyforournextchapter,whereweputthemtouseinamoresubstantialexample.
Finally,wefinishedbylookingaterrorhandlingandbackpressure,bothofwhichareimportantcharacteristicsofreliableapplicationsthatshouldalwaysbekeptinmind.
Chapter3.AsynchronousProgrammingandNetworkingSeveralbusinessapplicationsneedtoreacttoexternalstimuli—suchasnetworktraffic—asynchronously.Anexampleofsuchsoftwaremightbeadesktopapplicationthatallowsustotrackacompany’ssharepricesinthestockmarket.
Wewillbuildthisapplicationfirstusingamoretraditionalapproach.Indoingso,wewill:
BeabletoidentifyandunderstandthedrawbacksofthefirstdesignLearnhowtouseRxClojuretodealwithstatefulcomputationssuchasrollingaveragesRewritetheexampleinadeclarativefashionusingobservablesequences,thusreducingthecomplexityfoundinourfirstapproach
BuildingastockmarketmonitoringapplicationOurstockmarketprogramwillconsistofthreemaincomponents:
Afunctionsimulatinganexternalservicefromwhichwecanquerythecurrentprice—thiswouldlikelybeanetworkcallinarealsettingAschedulerthatpollstheprecedingfunctionatapredefinedintervalAdisplayfunctionresponsibleforupdatingthescreen
We’llstartbycreatinganewleiningenproject,wherethesourcecodeforourapplicationwilllive.Typethefollowingonthecommandlineandthenswitchintothenewlycreateddirectory:
leinnewstock-market-monitor
cdstock-market-monitor
Aswe’llbebuildingaGUIforthisapplication,goaheadandaddadependencyonSeesawtothedependenciessectionofyourproject.clj:
[seesaw"1.4.4"]
Next,createasrc/stock_market_monitor/core.cljfileinyourfavoriteeditor.Let’screateandconfigureourapplication’sUIcomponents:
(nsstock-market-monitor.core
(:require[seesaw.core:refer:all])
(:import(java.util.concurrentScheduledThreadPoolExecutor
TimeUnit)))
(native!)
(defmain-frame(frame:title"Stockpricemonitor"
:width200:height100
:on-close:exit))
(defprice-label(label"Price:-"))
(config!main-frame:contentprice-label)
Asyoucansee,theUIisfairlysimple.Itconsistsofasinglelabelthatwilldisplayacompany’sshareprice.WealsoimportedtwoJavaclasses,ScheduledThreadPoolExecutorandTimeUnit,whichwewilluseshortly.
Thenextthingweneedisourpollingmachinerysothatwecaninvokethepricingserviceonagivenschedule.We’llimplementthisviaathreadpoolsoasnottoblockthemainthread:
TipUserinterfaceSDKssuchasswinghavetheconceptofamain—orUI—thread.ThisisthethreadusedbytheSDKtorendertheUIcomponentstothescreen.Assuch,ifwe
haveblocking—orevensimplyslowrunning—operationsexecuteinthisthread,theuserexperiencewillbeseverelyaffected,hencetheuseofathreadpooltooffloadexpensivefunctioncalls.
(defpool(atomnil))
(defninit-scheduler[num-threads]
(reset!pool(ScheduledThreadPoolExecutor.num-threads)))
(defnrun-every[poolmillisf]
(.scheduleWithFixedDelaypool
f
0millisTimeUnit/MILLISECONDS))
(defnshutdown[pool]
(println"Shuttingdownscheduler…")
(.shutdownpool))
Theinit-schedulerfunctioncreatesScheduledThreadPoolExecutorwiththegivennumberofthreads.That’sthethreadpoolinwhichourperiodicfunctionwillrun.Therun-everyfunctionschedulesafunctionfinthegivenpooltorunattheintervalspecifiedbymillis.Finally,shutdownisafunctionthatwillbecalledonprogramterminationandshutdownthethreadpoolgracefully.
Therestoftheprogramputsallthesepartstogether:
(defnshare-price[company-code]
(Thread/sleep200)
(rand-int1000))
(defn-main[&args]
(show!main-frame)
(.addShutdownHook(Runtime/getRuntime)
(Thread.#(shutdown@pool)))
(init-scheduler1)
(run-every@pool500
#(->>(str"Price:"(share-price"XYZ"))
(text!price-label)
invoke-now)))
Theshare-pricefunctionsleepsfor200millisecondstosimulatenetworklatencyandreturnsarandomintegerbetween0and1,000representingthestock’sprice.
Thefirstlineofour-mainfunctionaddsashutdownhooktotheruntime.Thisallowsourprogramtointercepttermination—suchaspressingCtrl+Cinaterminalwindow—andgivesustheopportunitytoshutdownthethreadpool.
TipTheScheduledThreadPoolExecutorpoolcreatesnon-daemonthreadsbydefault.Aprogramcannotterminateifthereareanynon-daemonthreadsaliveinadditiontotheprogram’smainthread.Thisiswhytheshutdownhookisnecessary.
Next,weinitializetheschedulerwithasinglethreadandscheduleafunctiontobe
executedevery500milliseconds.Thisfunctionaskstheshare-pricefunctionforXYZ’scurrentpriceandupdatesthelabel.
TipDesktopapplicationsrequireallrenderingtobedoneintheUIthread.However,ourperiodicfunctionrunsonaseparatethreadandneedstoupdatethepricelabel.Thisiswhyweuseinvoke-now,whichisaSeesawfunctionthatschedulesitsbodytobeexecutedintheUIthreadassoonaspossible.
Let’sruntheprogrambytypingthefollowingcommandintheproject’srootdirectory:
leintrampolinerun-mstock-market-monitor.core
TipTrampoliningtellsleiningennottonestourprogram’sJVMwithinitsown,thusfreeingustohandleusesofCtrl+Courselvesthroughshutdownhooks.
Awindowliketheoneshowninthefollowingscreenshotwillbedisplayed,withthevaluesonitbeingupdatedasperthescheduleimplementedearlier:
Thisisafinesolution.Thecodeisrelativelystraightforwardandsatisfiesouroriginalrequirements.However,ifwelookatthebigpicture,thereisafairbitofnoiseinourprogram.Mostofitslinesofcodearedealingwithcreatingandmanagingathreadpool,which,whilenecessary,isn’tcentraltotheproblemwe’resolving—it’sanimplementationdetail.
We’llkeepthingsastheyareforthemomentandaddanewrequirement:rollingaverages.
RollingaveragesNowthatwecanseetheup-to-datestockpriceforagivencompany,itmakessensetodisplayarollingaverageofthepast,say,fivestockprices.Inarealscenario,thiswouldprovideanobjectiveviewofacompany’ssharetrendinthestockmarket.
Let’sextendourprogramtoaccommodatethisnewrequirement.
First,we’llneedtomodifyournamespacedefinition:
(nsstock-market-monitor.core
(:require[seesaw.core:refer:all])
(:import(java.util.concurrentScheduledThreadPoolExecutor
TimeUnit)
(clojure.langPersistentQueue)))
Theonlychangeisanewimportclause,forClojure’sPersistentQueueclass.Wewillbeusingthatlater.
We’llalsoneedanewlabeltodisplaythecurrentrunningaverage:
(defrunning-avg-label(label"Runningaverage:-"))
(config!main-frame:content
(border-panel
:northprice-label
:centerrunning-avg-label
:border5))
Next,weneedafunctiontocalculaterollingaverages.Arolling—ormoving—averageisacalculationinstatistics,whereyoutaketheaverageofasubsetofitemsinadataset.Thissubsethasafixedsizeanditshiftsforwardasdatacomesin.Thiswillbecomeclearwithanexample.
Supposeyouhavealistwithnumbersfrom1to10,inclusive.Ifweuse3asthesubsetsize,therollingaveragesareasfollows:
[12345678910]=>2.0
[12345678910]=>3.0
[12345678910]=>4.0
Thehighlightedpartsintheprecedingcodeshowthecurrentwindowbeingusedtocalculatethesubsetaverage.
Nowthatweknowwhatrollingaveragesare,wecanmoveontoimplementitinourprogram:
(defnroll-buffer[buffernumbuffer-size]
(let[buffer(conjbuffernum)]
(if(>(countbuffer)buffer-size)
(popbuffer)
buffer)))
(defnavg[numbers]
(float(/(reduce+numbers)
(countnumbers))))
(defnmake-running-avg[buffer-size]
(let[buffer(atomclojure.lang.PersistentQueue/EMPTY)]
(fn[n]
(swap!bufferroll-buffernbuffer-size)
(avg@buffer))))
(defrunning-avg(running-avg5))
Theroll-bufferfunctionisautilityfunctionthattakesaqueue,anumber,andabuffersizeasarguments.Itaddsthatnumbertothequeue,poppingtheoldestelementifthequeuegoesoverthebufferlimit,thuscausingitscontentstorollover.
Next,wehaveafunctionforcalculatingtheaverageofacollectionofnumbers.Wecasttheresulttofloatifthere’sanunevendivision.
Finally,thehigher-ordermake-running-avgfunctionreturnsastateful,singleargumentfunctionthatclosesoveranemptypersistentqueue.Thisqueueisusedtokeeptrackofthecurrentsubsetofdata.
Wethencreateaninstanceofthisfunctionbycallingitwithabuffersizeof5andsaveittotherunning-avgvar.Eachtimewecallthisnewfunctionwithanumber,itwilladdittothequeueusingtheroll-bufferfunctionandthenfinallyreturntheaverageoftheitemsinthequeue.
Thecodewehavewrittentomanagethethreadpoolwillbereusedasissoallthatislefttodoisupdateourperiodicfunction:
(defnworker[]
(let[price(share-price"XYZ")]
(->>(str"Price:"price)(text!price-label))
(->>(str"Runningaverage:"(running-avgprice))
(text!running-avg-label))))
(defn-main[&args]
(show!main-frame)
(.addShutdownHook(Runtime/getRuntime)
(Thread.#(shutdown@pool)))
(init-scheduler1)
(run-every@pool500
#(invoke-now(worker))))
Sinceourfunctionisn’taone-lineranymore,weabstractitawayinitsownfunctioncalledworker.Asbefore,itupdatesthepricelabel,butwehavealsoextendedittousetherunning-avgfunctioncreatedearlier.
We’rereadytoruntheprogramoncemore:
leintrampolinerun-mstock-market-monitor.core
Youshouldseeawindowliketheoneshowninthefollowingscreenshot:
YoushouldseethatinadditiontodisplayingthecurrentsharepriceforXYZ,theprogramalsokeepstrackandrefreshestherunningaverageofthestreamofprices.
IdentifyingproblemswithourcurrentapproachAsidefromthelinesofcoderesponsibleforbuildingtheuserinterface,ourprogramisroughly48lineslong.
Thecoreoftheprogramresidesintheshare-priceandavgfunctions,whichareresponsibleforqueryingthepriceserviceandcalculatingtheaverageofalistofnnumbers,respectively.Theyrepresentonlysixlinesofcode.Thereisalotofincidentalcomplexityinthissmallprogram.
Incidentalcomplexityiscomplexitycausedbycodethatisnotessentialtotheproblemathand.Inthisexample,wehavetwosourcesofsuchcomplexity—wearedisregardingUIspecificcodeforthisdiscussion:thethreadpoolandtherollingbufferfunction.Theyaddagreatdealofcognitiveloadtosomeonereadingandmaintainingthecode.
Thethreadpoolisexternaltoourproblem.Itisonlyconcernedwiththesemanticsofhowtoruntasksasynchronously.Therollingbufferfunctionspecifiesadetailedimplementationofaqueueandhowtouseittorepresenttheconcept.
Ideally,weshouldbeabletoabstractoverthesedetailsandfocusonthecoreofourproblem;CompositionalEventSystems(CES)allowsustodojustthat.
RemovingincidentalcomplexitywithRxClojureInChapter2,ALookatReactiveExtensions,welearnedaboutthebasicbuildingblocksofRxClojure,anopen-sourceCESframework.Inthissection,we’llusethisknowledgeinordertoremovetheincidentalcomplexityfromourprogram.Thiswillgiveusaclear,declarativewaytodisplaybothpricesandrollingaverages.
TheUIcodewe’vewrittensofarremainsunchanged,butweneedtomakesureRxClojureisdeclaredinthedependenciessectionofourproject.cljfile:
[io.reactivex/rxclojure"1.0.0"]
Then,ensurewerequirethefollowinglibrary:
(nsstock-market-monitor.core
(:require[rx.lang.clojure.core:asrx]
[seesaw.core:refer:all])
(:import(java.util.concurrentTimeUnit)
(rxObservable)))
Thewayweapproachtheproblemthistimeisalsodifferent.Let’stakealookatthefirstrequirement:itrequireswedisplaythecurrentpriceofacompany’sshareinthestockmarket.
Everytimewequerythepriceservice,wegeta—possiblydifferent—priceforthecompanyinquestion.AswesawinChapter2,ALookatReactiveExtensions,modelingthisasanobservablesequenceiseasy,sowe’llstartwiththat.We’llcreateafunctionthatgivesusbackastockpriceobservableforthegivencompany:
(defnmake-price-obs[company-code]
(rx/return(share-pricecompany-code)))
Thisisanobservablethatyieldsasinglevalueandterminates.It’sequivalenttothefollowingmarblediagram:
Partofthefirstrequirementisthatwequerytheserviceonapredefinedtimeinterval—every500millisecondsinthiscase.Thishintsatanobservablewehaveencounteredbefore,aptlynamedinterval.Inordertogetthepollingbehaviorwewant,weneedto
combinetheintervalandthepriceobservables.
Asyouprobablyrecall,flatmapisthetoolforthejobhere:
(rx/flatmap(fn[_](make-price-obs"XYZ"))
(Observable/interval500
TimeUnit/MILLISECONDS))
TheprecedingsnippetcreatesanobservablethatwillyieldthelateststockpriceforXYZevery500millisecondsindefinitely.Itcorrespondstothefollowingdiagram:
Infact,wecansimplysubscribetothisnewobservableandtestitout.Modifyyourmainfunctiontothefollowingsnippetandruntheprogram:
(defn-main[&args]
(show!main-frame)
(let[price-obs(rx/flatmap(fn[_](make-price-obs"XYZ"))
(Observable/interval500
TimeUnit/MILLISECONDS))]
(rx/subscribeprice-obs
(fn[price]
(text!price-label(str"Price:"price))))))
Thisisverycool!Wereplicatedthebehaviorofourfirstprogramwithonlyafewlinesofcode.Thebestpartisthatwedidnothavetoworryaboutthreadpoolsorschedulingactions.Bythinkingabouttheproblemintermsofobservablesequences,aswellascombiningexistingandnewobservables,wewereabletodeclarativelyexpresswhatwe
wanttheprogramtodo.
Thisalreadyprovidesgreatbenefitsinmaintainabilityandreadability.However,wearestillmissingtheotherhalfofourprogram:rollingaverages.
ObservablerollingaveragesItmightnotbeimmediatelyobvioushowwecanmodelrollingaveragesasobservables.Whatweneedtokeepinmindisthatprettymuchanythingwecanthinkofasasequenceofvalues,wecanprobablymodelasanobservablesequence.
Rollingaveragesarenodifferent.Let’sforgetforamomentthatthepricesarecomingfromanetworkcallwrappedinanobservable.Let’simaginewehaveallvalueswecareaboutinaClojurevector:
(defvalues(range10))
Whatweneedisawaytoprocessthesevaluesinpartitions—orbuffers—ofsize5insuchawaythatonlyasinglevalueisdroppedateachinteraction.InClojure,wecanusethepartitionfunctionforthispurpose:
(doseq[buffer(partition51values)]
(prnbuffer))
(01234)
(12345)
(23456)
(34567)
(45678)
...
Thesecondargumenttothepartitionfunctioniscalledastepanditistheoffsetofhowmanyitemsshouldbeskippedbeforestartinganewpartition.Here,wesetitto1inordertocreatetheslidingwindoweffectweneed.
Thebigquestionthenis:canwesomehowleveragepartitionwhenworkingwithobservablesequences?
ItturnsoutthatRxJavahasatransformercalledbufferjustforthispurpose.Thepreviousexamplecanberewrittenasfollows:
(->(rx/seq->o(vec(range10)))
(.buffer51)
(rx/subscribe
(fn[price]
(prn(str"Value:"price)))))
TipAsmentionedpreviously,notallRxJava’sAPIisexposedthroughRxClojure,sohereweneedtouseinteroptoaccessthebuffermethodfromtheobservablesequence.
Asbefore,thesecondargumenttobufferistheoffset,butit’scalledskipintheRxJavadocumentation.IfyourunthisattheREPLyou’llseethefollowingoutput:
"Value:[0,1,2,3,4]"
"Value:[1,2,3,4,5]"
"Value:[2,3,4,5,6]"
"Value:[3,4,5,6,7]"
"Value:[4,5,6,7,8]"
...
Thisisexactlywhatwewant.Theonlydifferenceisthatthebuffermethodwaitsuntilithasenoughelements—fiveinthiscase—beforeproceeding.
Now,wecangobacktoourprogramandincorporatethisideawithourmainfunction.Hereiswhatitlookslike:
(defn-main[&args]
(show!main-frame)
(let[price-obs(->(rx/flatmapmake-price-obs
(Observable/interval500
TimeUnit/MILLISECONDS))
(.publish))
sliding-buffer-obs(.bufferprice-obs51)]
(rx/subscribeprice-obs
(fn[price]
(text!price-label(str"Price:"price))))
(rx/subscribesliding-buffer-obs
(fn[buffer]
(text!running-avg-label(str"Runningaverage:"(avg
buffer)))))
(.connectprice-obs)))
Theprecedingsnippetworksbycreatingtwoobservables.Thefirstone,price-obs,wehadcreatedbefore.Thenewslidingbufferobservableiscreatedusingthebuffertransformeronprice-obs.
Wecan,then,independentlysubscribetoeachoneinordertoupdatethepriceandrollingaveragelabels.Runningtheprogramwilldisplaythesamescreenwe’veseenpreviously:
Youmighthavenoticedtwomethodcallswehadn’tseenbefore:publishandconnect.
Thepublishmethodreturnsaconnectableobservable.Thismeansthattheobservablewon’tstartemittingvaluesuntilitsconnectmethodhasbeencalled.Wedothisherebecausewewanttomakesurethatallthesubscribersreceiveallthevaluesemittedbytheoriginalobservable.
Inconclusion,withoutmuchadditionalcode,weimplementedallrequirementsinaconcise,declarativemannerthatiseasytomaintainandfollow.Wehavealsomadethepreviousroll-bufferfunctioncompletelyunnecessary.
ThefullsourcecodefortheCESversionoftheprogramisgivenhereforreference:
(nsstock-market-monitor.05frp-price-monitor-rolling-avg
(:require[rx.lang.clojure.core:asrx]
[seesaw.core:refer:all])
(:import(java.util.concurrentTimeUnit)
(rxObservable)))
(native!)
(defmain-frame(frame:title"Stockpricemonitor"
:width200:height100
:on-close:exit))
(defprice-label(label"Price:-"))
(defrunning-avg-label(label"Runningaverage:-"))
(config!main-frame:content
(border-panel
:northprice-label
:centerrunning-avg-label
:border5))
(defnshare-price[company-code]
(Thread/sleep200)
(rand-int1000))
(defnavg[numbers]
(float(/(reduce+numbers)
(countnumbers))))
(defnmake-price-obs[_]
(rx/return(share-price"XYZ")))
(defn-main[&args]
(show!main-frame)
(let[price-obs(->(rx/flatmapmake-price-obs
(Observable/interval500
TimeUnit/MILLISECONDS))
(.publish))
sliding-buffer-obs(.bufferprice-obs51)]
(rx/subscribeprice-obs
(fn[price]
(text!price-label(str"Price:"price))))
(rx/subscribesliding-buffer-obs
(fn[buffer]
(text!running-avg-label(str"Runningaverage:"(avg
buffer)))))
(.connectprice-obs)))
Notehowinthisversionoftheprogram,wedidn’thavetouseashutdownhook.ThisisbecauseRxClojurecreatesdaemonthreads,whichareautomaticallyterminatedoncetheapplicationexits.
SummaryInthischapter,wesimulatedareal-worldapplicationwithourstockmarketprogram.We’vewrittenitinasomewhattraditionalwayusingthreadpoolsandacustomqueueimplementation.WethenrefactoredittoaCESstyleusingRxClojure’sobservablesequences.
Theresultingprogramisshorter,simpler,andeasiertoreadonceyougetfamiliarwiththecoreconceptsofRxClojureandRxJava.
InthenextChapterwewillbeintroducedtocore.asyncinpreparationforimplementingourownbasicCESframework.
Chapter4.Introductiontocore.asyncLonggonearethedayswhenprogramswererequiredtodoonlyonethingatatime.Beingabletoperformseveraltasksconcurrentlyisatthecoreofthevastmajorityofmodernbusinessapplications.Thisiswhereasynchronousprogrammingcomesin.
Asynchronousprogramming—and,moregenerally,concurrency—isaboutdoingmorewithyourhardwareresourcesthanyoupreviouslycould.Itmeansfetchingdatafromthenetworkoradatabaseconnectionwithouthavingtowaitfortheresult.Or,perhaps,readinganExcelspreadsheetintomemorywhiletheusercanstilloperatethegraphicalinterface.Ingeneral,itimprovesasystem’sresponsiveness.
Inthischapter,wewilllookathowdifferentplatformshandlethisstyleofprogramming.Morespecifically,wewill:
Beintroducedtocore.async’sbackgroundandAPISolidifyourunderstandingofcore.asyncbyre-implementingthestockmarketapplicationintermsofitsabstractionsUnderstandhowcore.asyncdealswitherrorhandlingandbackpressureTakeabrieftourontransducers
AsynchronousprogrammingandconcurrencyDifferentplatformshavedifferentprogrammingmodels.Forinstance,JavaScriptapplicationsaresingle-threadedandhaveaneventloop.Whenmakinganetworkcall,itiscommontoregisteracallbackthatwillbeinvokedatalaterstage,whenthatnetworkcallcompleteseithersuccessfullyorwithanerror.
Incontrast,whenwe’reontheJVM,wecantakefulladvantageofmultithreadingtoachieveconcurrency.ItissimpletospawnnewthreadsviaoneofthemanyconcurrencyprimitivesprovidedbyClojure,suchasfutures.
However,asynchronousprogrammingbecomescumbersome.Clojurefuturesdon’tprovideanativewayforustobenotifiedoftheircompletionatalaterstage.Inaddition,retrievingvaluesfromanot-yet-completedfutureisablockingoperation.Thiscanbeseenclearlyinthefollowingsnippet:
(defndo-something-important[]
(let[f(future(do(prn"Calculating…")
(Thread/sleep10000)))]
(prn"Perhapsthefuturehasdoneitsjob?")
(prn@f)
(prn"Youwillonlyseethisinabout10seconds…")))
(do-something-important)
Thesecondcalltoprintdereferencesthefuture,causingthemainthreadtoblocksinceithasn’tfinishedyet.Thisiswhyyouonlyseethelastprintafterthethreadinwhichthefutureisrunninghasfinished.Callbackscan,ofcourse,besimulatedbyspawningaseparatethreadtomonitorthefirstone,butthissolutionisclunkyatbest.
AnexceptiontothelackofcallbacksisGUIprogramminginClojure.MuchlikeJavaScript,ClojureSwingapplicationsalsopossessaneventloopandcanrespondtouserinputandinvokelisteners(callbacks)tohandlethem.
Anotheroptionisrewritingthepreviousexamplewithacustomcallbackthatispassedintothefuture:
(defndo-something-important[callback]
(let[f(future(let[answer42]
(Thread/sleep10000)
(callbackanswer)))]
(prn"Perhapsthefuturehasdoneitsjob?")
(prn"Youshouldseethisalmostimmediatelyandthenin10secs…")
f))
(do-something-important(fn[answer]
(prn"Futureisdone.Answeris"answer)))
Thistimetheorderoftheoutputsshouldmakemoresense.However,ifwereturnthefuturefromthisfunction,wehavenowaytogiveitanothercallback.Wehavelostthe
abilitytoperformanactionwhenthefutureendsandarebacktohavingtodereferenceit,thusblockingthemainthreadagain—exactlywhatwewantedtoavoid.
TipJava8introducesanewclass,CompletableFuture,thatallowsregisteringacallbacktobeinvokedoncethefuturecompletes.Ifthat’sanoptionforyou,youcanuseinteroptomakeClojureleveragethenewclass.
Asyoumighthaverealized,CESiscloselyrelatedtoasynchronousprogramming:thestockmarketapplicationwebuiltinthepreviouschapterisanexampleofsuchaprogram.Themain—orUI—threadisneverblockedbytheObservablesfetchingdatafromthenetwork.Additionally,wewerealsoabletoregistercallbackswhensubscribingtothem.
Inmanyasynchronousapplications,however,callbacksarenotthebestwaytogo.Heavyuseofcallbackscanleadtowhatisknownascallbackhell.Clojureprovidesamorepowerfulandelegantsolution.
Inthenextfewsections,wewillexplorecore.async,aClojurelibraryforasynchronousprogramming,andhowitrelatestoReactiveProgramming.
core.asyncIfyou’veeverdoneanyamountofJavaScriptprogramming,youhaveprobablyexperiencedcallbackhell.Ifyouhaven’t,thefollowingcodeshouldgiveyouagoodidea:
http.get('api/users/find?name='+name,function(user){
http.get('api/orders?userId='+user.id,function(orders){
orders.forEach(function(order){
container.append(order);
});
});
});
Thisstyleofprogrammingcaneasilygetoutofhand—insteadofwritingmorenatural,sequentialstepstoachievingatask,thatlogicisinsteadscatteredacrossmultiplecallbacks,increasingthedeveloper’scognitiveload.
Inresponsetothisissue,theJavaScriptcommunityreleasedseveralpromiseslibrariesthataremeanttosolvetheissue.Wecanthinkofpromisesasemptyboxeswecanpassintoandreturnfromourfunctions.Atsomepointinthefuture,anotherprocessmightputavalueinsidethisbox.
Asanexample,theprecedingsnippetcanbewrittenwithpromiseslikethefollowing:
http.get('api/users/find?name='+name)
.then(function(user){
returnhttp.get('api/orders?userId='+user.id);
})
.then(function(orders){
orders.forEach(function(order){
container.append(order);
});
});
Theprecedingsnippetshowshowusingpromisescanflattenyourcallbackpyramid,buttheydon’teliminatecallbacks.ThethenfunctionisapublicfunctionofthepromisesAPI.Itisdefinitelyastepintherightdirectionasthecodeiscomposableandeasiertoread.
Aswetendtothinkinsequencesofsteps,however,wewouldliketowritethefollowing:
user=http.get('api/users/find?name='+name);
orders=http.get('api/orders?userId='+user.id);
orders.forEach(function(order){
container.append(order);
});
Eventhoughthecodelookssynchronous,thebehaviorshouldbenodifferentfromthepreviousexamples.Thisisexactlywhatcore.asyncletsusdoinbothClojureandClojureScript.
CommunicatingsequentialprocessesThecore.asynclibraryisbuiltonanoldidea.ThefoundationuponwhichitlieswasfirstdescribedbyTonyHoare—ofQuicksortfame—inhis1978paperCommunicatingSequentialProcesses(CSP;seehttp://www.cs.ucf.edu/courses/cop4020/sum2009/CSP-hoare.pdf).CSPhassincebeenextendedandimplementedinseverallanguages,thelatestofwhichbeingGoogle’sGoprogramminglanguage.
Itisbeyondthescopeofthisbooktogointothedetailsofthisseminalpaper,sowhatfollowsisasimplifieddescriptionofthemainideas.
InCSP,workismodeledusingtwomainabstractions:channelsandprocesses.CSPisalsomessage-drivenand,assuch,itcompletelydecouplestheproducerfromtheconsumerofthemessage.Itisusefultothinkofchannelsasblockingqueues.
Asimplisticapproachdemonstratingthesebasicabstractionsisasfollows:
(import'java.util.concurrent.ArrayBlockingQueue)
(defnproducer[c]
(prn"Takinganap")
(Thread/sleep5000)
(prn"Nowputtinganameinqueue…")
(.putc"Leo"))
(defnconsumer[c]
(prn"Attemptingtotakevaluefromqueuenow…")
(prn(str"Gotit.Hello"(.takec)"!")))
(defchan(ArrayBlockingQueue.10))
(future(consumerchan))
(future(producerchan))
RunningthiscodeintheREPLshouldshowusoutputsimilartothefollowing:
"Attemptingtotakevaluefromqueuenow…"
"Takinganap"
;;then5secondslater
"Nowputtinganameinquequeue…"
"Gotit.HelloLeo!"
Inordernottoblockourprogram,westartboththeconsumerandtheproducerintheirownthreadsusingafuture.Sincetheconsumerwasstartedfirst,wemostlikelywillseeitsoutputimmediately.However,assoonasitattemptstotakeavaluefromthechannel—orqueue—itwillblock.Itwillwaitforavaluetobecomeavailableandwillonlyproceedaftertheproducerisdonetakingitsnap—clearlyaveryimportanttask.
Now,let’scompareitwithasolutionusingcore.async.First,createanewleiningenprojectandaddadependencyonit:
[org.clojure/core.async"0.1.278.0-76b25b-alpha"]
Now,typethisintheREPLorinyourcorenamespace:
(nscore-async-playground.core
(:require[clojure.core.async:refer[gochan<!>!timeout]]))
(defnprn-with-thread-id[s]
(prn(strs"-Threadid:"(.getId(Thread/currentThread)))))
(defnproducer[c]
(go(prn-with-thread-id"Takinganap")
(<!(timeout5000))
(prn-with-thread-id"Nowputtinganameinquequeue…")
(>!c"Leo")))
(defnconsumer[c]
(go(prn-with-thread-id"Attemptingtotakevaluefromqueuenow…")
(prn-with-thread-id(str"Gotit.Hello"(<!c)"!"))))
(defc(chan))
(consumerc)
(producerc)
Thistimeweareusingahelperfunction,prn-with-thread-id,whichappendsthecurrentthreadIDtotheoutputstring.Iwillexplainwhyshortly,butapartfromthat,theoutputwillhavebeenequivalenttothepreviousone:
"Attemptingtotakevaluefromqueuenow…-Threadid:43"
"Takinganap-Threadid:44"
"Nowputtinganameinquequeue…-Threadid:48"
"Gotit.HelloLeo!-Threadid:48"
Structurally,bothsolutionslookfairlysimilar,butsinceweareusingquiteafewnewfunctionshere,let’sbreakitdown:
chanisafunctionthatcreatesacore.asyncchannel.Asmentionedpreviously,itcanbethoughtofasaconcurrentblockingqueueandisthemainabstractioninthelibrary.Bydefaultchancreatesanunboundedchannel,butcore.asyncprovidesmanymoreusefulchannelconstructors,afewofwhichwe’llbeusinglater.timeoutisanothersuchchannelconstructor.Itgivesusachannelthatwillwaitforagivenamountoftimebeforereturningniltothetakingprocess,closingitselfimmediatelyafterward.Thisisthecore.asyncequivalentofThread/sleep.Thefunctions>!and<!areusedtoputandtakevaluesfromachannel,respectively.Thecaveatisthattheyhavetobeusedinsideagoblock,aswewillexplainlater.goisamacrothattakesabodyofexpressions—whichformagoblock—andcreateslightweightprocesses.Thisiswherethemagichappens.Insideagoblock,anycallsto>!and<!thatwouldordinarilyblockwaitingforvaluestobeavailableinchannelsareinsteadparked.Parkingisaspecialtypeofblockingusedinternallyinthestatemachineofcore.async.TheblogpostbyHueyPetersencoversthisstatemachineindepth(seehttp://hueypetersen.com/posts/2013/08/02/the-state-machines-of-core-async/).
GoblocksaretheveryreasonforwhichIchosetoprintthethreadIDsinourexample.Ifwelookclosely,we’llrealizethatthelasttwostatementswereexecutedinthesamethread
—thisisn’ttrue100percentofthetimeasconcurrencyisinherentlynon-deterministic.Thisisafundamentaldifferencebetweencore.asyncandsolutionsusingthreads/futures.
Threadscanbeexpensive.OntheJVM,theirdefaultstacksizeis512kilobytes—configurableviathe-XssJVMstartupoption.Whendevelopingahighlyconcurrentsystem,creatingthousandsofthreadscanquicklydraintheresourcesofthemachinetheapplicationisrunningon.
core.asyncacknowledgesthislimitationandgivesuslightweightprocesses.Internally,theydoshareathreadpool,butinsteadofwastefullycreatingathreadpergoblock,threadsarerecycledandreusedwhenaput/takeoperationiswaitingforavaluetobecomeavailable.
TipAtthetimeofwriting,thethreadpoolusedbycore.asyncdefaultstothenumberofavailableprocessorsx2,+42.So,amachinewitheightprocessorswillhaveapoolwith58threads.
Therefore,itiscommonforcore.asyncapplicationstohavedozensofthousandsoflightweightprocesses.Theyareextremelycheaptocreate.
SincethisisabookonReactiveProgramming,thequestionthatmightbeinyourheadnowis:canwebuildreactiveapplicationsusingcore.async?Theshortanswerisyes,wecan!Toproveit,wewillrevisitourstockmarketapplicationandrewriteitusingcore.async.
Rewritingthestockmarketapplicationwithcore.asyncByusinganexamplewearefamiliarwith,weareabletofocusonthedifferencesbetweenallapproachesdiscussedsofar,withoutgettingsidetrackedwithnew,specificdomainrules.
Beforewediveintotheimplementation,let’squicklydoanoverviewofhowoursolutionshouldwork.
Justlikeinourpreviousimplementations,wehaveaservicefromwhichwecanqueryshareprices.Whereourapproachdiffers,however,isadirectconsequenceofhowcore.asyncchannelswork.
Onagivenschedule,wewouldliketowritethecurrentpricetoacore.asyncchannel.Thismightlooklikeso:
Thisprocesswillcontinuouslyputpricesintheoutchannel.Weneedtodotwothingswitheachprice:displayitanddisplaythecalculatedslidingwindow.Sincewelikeourfunctionsdecoupled,wewillusetwogoblocks,oneforeachtask:
Holdon.Thereseemstobesomethingoffwithourapproach.Oncewetakeapricefrom
theoutputchannel,itisnotavailableanylongertobetakenbyothergoblocks,so,insteadofcalculatingtheslidingwindowstartingwith10,ourfunctionendsupgettingthesecondvalue,20.Withthisapproach,wewillendupwithaslidingwindowthatcalculatesaslidingwindowwithroughlyeveryotheritem,dependingonhowconsistenttheinterleavingbetweenthegoblocksis.
Clearly,thisisnotwhatwewant,butithelpsusthinkabouttheproblemalittlemore.Thesemanticsofcore.asyncpreventusfromreadingavaluefromachannelmorethanonce.Mostofthetime,thisbehaviorisjustfine—especiallyifyouthinkofthemasqueues.Sohowcanweprovidethesamevaluetobothfunctions?
Tosolvethisproblem,wewilltakeadvantageofanotherchannelconstructorprovidedbycore.asynccalledbroadcast.Asthenameimplies,broadcastreturnsachannel,which,whenwrittento,writesitsvalueintothechannelspassedtoitasarguments.Effectively,thischangesourhigh-levelpicturetosomethinglikethefollowing:
Insummary,wewillhaveagoloopwritingpricestothisbroadcastchannel,whichwillthenforwarditsvaluestothetwochannelsfromwhichwewillbeoperating:pricesandtheslidingwindow.
Withthegeneralideainplace,wearereadytodiveintothecode.
ImplementingtheapplicationcodeWealreadyhaveaprojectdependingoncore.asyncthatwecreatedintheprevioussection,sowe’llbeworkingoffthat.Let’sstartbyaddinganextradependencyonseesawtoyourproject.cljfile:
:dependencies[[org.clojure/clojure"1.5.1"]
[org.clojure/core.async"0.1.278.0-76b25b-alpha"]
[seesaw"1.4.4"]]
Next,createafilecalledstock_market.cljinthesrcdirectoryandaddthisnamespacedeclaration:
(nscore-async-playground.stock-market
(:require[clojure.core.async
:refer[gochan<!>!timeoutgo-loopmap>]:asasync])
(:require[clojure.core.async.lab:refer[broadcast]])
(:use[seesaw.core]))
ThismightbeagoodpointtorestartyourREPLifyouhaven’tdoneso.Don’tworryaboutanyfunctionswehaven’tseenyet.We’llgetafeelfortheminthissection.
TheGUIcoderemainslargelyunchanged,sonoexplanationshouldbenecessaryforthenextsnippet:
(native!)
(defmain-frame(frame:title"Stockpricemonitor"
:width200:height100
:on-close:exit))
(defprice-label(label"Price:-"))
(defrunning-avg-label(label"Runningaverage:-"))
(config!main-frame:content
(border-panel
:northprice-label
:centerrunning-avg-label
:border5))
(defnshare-price[company-code]
(Thread/sleep200)
(rand-int1000))
(defnavg[numbers]
(float(/(reduce+numbers)
(countnumbers))))
(defnroll-buffer[buffervalbuffer-size]
(let[buffer(conjbufferval)]
(if(>(countbuffer)buffer-size)
(popbuffer)
buffer)))
(defnmake-sliding-buffer[buffer-size]
(let[buffer(atomclojure.lang.PersistentQueue/EMPTY)]
(fn[n]
(swap!bufferroll-buffernbuffer-size))))
(defsliding-buffer(make-sliding-buffer5))
Theonlydifferenceisthatnowwehaveasliding-bufferfunctionthatreturnsawindowofdata.Thisisincontrastwithouroriginalapplication,wheretherolling-avgfunctionwasresponsibleforbothcreatingthewindowandcalculatingtheaverage.Thisnewdesignismoregeneralasitmakesthisfunctioneasiertoreuse.Theslidinglogicisthesame,however.
Next,wehaveourmainapplicationlogicusingcore.async:
(defnbroadcast-at-interval[msecstask&ports]
(go-loop[out(applybroadcastports)]
(<!(timeoutmsecs))
(>!out(task))
(recurout)))
(defn-main[&args]
(show!main-frame)
(let[prices-ch(chan)
sliding-buffer-ch(map>sliding-buffer(chan))]
(broadcast-at-interval500#(share-price"XYZ")prices-chsliding-
buffer-ch)
(go-loop[]
(when-let[price(<!prices-ch)]
(text!price-label(str"Price:"price))
(recur)))
(go-loop[]
(when-let[buffer(<!sliding-buffer-ch)]
(text!running-avg-label(str"Runningaverage:"(avgbuffer)))
(recur)))))
Let’swalkthroughthecode.
Thefirstfunction,broadcast-at-interval,isresponsibleforcreatingthebroadcastingchannel.Itreceivesavariablenumberofarguments:anumberofmillisecondsdescribingtheinterval,thefunctionrepresentingthetasktobeexecuted,andasequenceofoneofmoreoutputchannels.Thesechannelsareusedtocreatethebroadcastingchanneltowhichthegoloopwillbewritingprices.
Next,wehaveourmainfunction.Theletblockiswheretheinterestingbitsare.Aswediscussedinourhigh-leveldiagrams,weneedtwooutputchannels:oneforpricesandonefortheslidingwindow.Theyarebothcreatedinthefollowing:
...
(let[prices-ch(chan)
sliding-buffer-ch(map>sliding-buffer(chan))]
...
prices-chshouldbeself-explanatory;however,sliding-buffer-chisusingafunctionwehaven’tencounteredbefore:map>.Thisisyetanotherusefulchannelconstructorin
core.async.Ittakestwoarguments:afunctionandatargetchannel.Itreturnsachannelthatappliesthisfunctiontoeachvaluebeforewritingittothetargetchannel.Anexamplewillhelpillustratehowitworks:
(defc(map>sliding-buffer(chan10)))
(go(doseq[n(range10)]
(>!cn)))
(go(doseq[n(range10)]
(prn(vec(<!c)))))
;;[0]
;;[01]
;;[012]
;;[0123]
;;[01234]
;;[12345]
;;[23456]
;;[34567]
;;[45678]
;;[56789]
Thatis,wewriteapricetothechannelandgetaslidingwindowontheotherend.Finally,wecreatethetwogoblockscontainingthesideeffects.Theyloopindefinitely,gettingvaluesfrombothchannelsandupdatingtheuserinterface.
Youcanseeitinactionbyrunningtheprogramfromtheterminal:
$leinrun-mcore-async-playground.stock-market
ErrorhandlingBackinChapter2,ALookatReactiveExtensions,welearnedhowReactiveExtensionstreatserrorsandexceptions.Itprovidesarichsetofcombinatorstodealwithexceptionalcasesandarestraightforwardtouse.
Despitebeingapleasuretoworkwith,core.asyncdoesn’tshipwithmuchsupportforexceptionhandling.Infact,ifwewriteourcodewithonlythehappypathinmindwedon’tevenknowanerroroccurred!
Let’shavealookatanexample:
(defnget-data[]
(throw(Exception."Badthingshappen!")))
(defnprocess[]
(let[result(chan)]
;;dosomeprocessing…
(go(>!result(get-data)))
result))
Intheprecedingsnippet,weintroducedtwofunctions:
get-datasimulatesafunctionthatfetchesdatafromthenetworkoranin-memorycache.Inthiscaseitsimplythrowsanexception.processisafunctionthatdependsonget-datatodosomethinginterestingandputstheresultintoachannel,whichisreturnedattheend.
Let’swatchwhathappenswhenweputthistogether:
(go(let[result(<!(->>(process"data")
(map>#(*%%))
(map>#(prn%))))]
(prn"resultis:"result)))
Nothinghappens.Zero,zip,zilch,nada.
Thisispreciselytheproblemwitherrorhandlingincore.async:bydefault,ourexceptionsareswallowedbythegoblockasitrunsonaseparatethread.Weareleftinthisstatewherewedon’treallyknowwhathappened.
Notallislost,however.DavidNolenoutlinedonhisblogapatternfordealingwithsuchasynchronousexceptions.Itonlyrequiresafewextralinesofcode.
Westartbydefiningahelperfunctionandmacro—thiswouldprobablyliveinautilitynamespacewerequireanywhereweusecore.async:
(defnthrow-err[e]
(when(instance?Throwablee)(throwe))
e)
(defmacro<?[ch]
`(throw-err(async/<!~ch)))
Thethrow-errfunctionreceivesavalueand,ifit’sasubclassofThrowable,itisthrown.Otherwise,itissimplyreturned.
Themacro<?isessentiallyadrop-inreplacementfor<!.Infact,ituses<!togetthevalueoutofthechannelbutpassesittothrow-errfirst.
Withtheseutilitiesinplace,weneedtomakeacoupleofchanges,firsttoourprocessfunction:
(defnprocess[]
(let[result(chan)]
;;dosomeprocessing…
(go(>!result(try(get-data)
(catchExceptione
e))))
result))
Theonlychangeisthatwewrappedget-datainatry/catchblock.Lookcloselyatthecatchblock:itsimplyreturnstheexception.
Thisisimportantasweneedtoensuretheexceptiongetsputintothechannel.
Next,weupdateourconsumercode:
(go(try(let[result(<?(->>(process"data")
(map>#(*%%))
(map>#(prn%))))]
(prn"resultis:"result))
(catchExceptione
(prn"Oops,anerrorhappened!Webetterdosomethingaboutit
here!"))))
;;"Oops,anerrorhappened!Webetterdosomethingaboutithere!"
Thistimeweuse<?inplaceof<!.Thismakessenseasitwillrethrowanyexceptionsfoundinthechannel.Asaresultwecannowuseasimpletry/catchtoregaincontroloverourexceptions.
BackpressureThemainmechanismbywhichcore.asyncallowsforcoordinatingbackpressureisbuffering.core.asyncdoesn’tallowunboundedbuffersasthiscanbeasourceofbugsandaresourcehog.
Instead,wearerequiredtothinkhardaboutourapplication’suniqueneedsandchooseanappropriatebufferingstrategy.
FixedbufferThisisthesimplestformofbuffering.Itisfixedtoachosennumbern,allowingproducerstoputitemsinthechannelwithouthavingtowaitforconsumers:
(defresult(chan(buffer5)))
(go-loop[]
(<!(async/timeout1000))
(when-let[x(<!result)]
(prn"Gotvalue:"x)
(recur)))
(go(doseq[n(range5)]
(>!resultn))
(prn"Doneputtingvalues!")
(close!result))
;;"Doneputtingvalues!"
;;"Gotvalue:"0
;;"Gotvalue:"1
;;"Gotvalue:"2
;;"Gotvalue:"3
;;"Gotvalue:"4
Intheprecedingexample,wecreatedabufferofsize5andstartedagolooptoconsumevaluesfromit.Thegoloopusesatimeoutchanneltodelayitsstart.
Then,westartanothergoblockthatputsnumbersfrom0to4intotheresultchannelandprintstotheconsoleonceit’sdone.
Bythen,thefirsttimeoutwillhaveexpiredandwewillseethevaluesprintedtotheREPL.
Nowlet’swatchwhathappensifthebufferisn’tlargeenough:
(defresult(chan(buffer2)))
(go-loop[]
(<!(async/timeout1000))
(when-let[x(<!result)]
(prn"Gotvalue:"x)
(recur)))
(go(doseq[n(range5)]
(>!resultn))
(prn"Doneputtingvalues!")
(close!Result))
;;"Gotvalue:"0
;;"Gotvalue:"1
;;"Gotvalue:"2
;;"Doneputtingvalues!"
;;"Gotvalue:"3
;;"Gotvalue:"4
Thistimeourbuffersizeis2buteverythingelseisthesame.Asyoucanseethegoloopfinishesmuchlaterasitattemptedtoputanothervalueintheresultchannelandwas
blocked/parkedsinceitsbufferwasfull.
Aswithmostthings,thismightbeOKbutifwearenotwillingtoblockafastproducerjustbecausewecan’tconsumeitsitemsfastenough,wemustlookforanotheroption.
DroppingbufferAdroppingbufferalsohasafixedsize.However,insteadofblockingproducerswhenitisfull,itsimplyignoresanynewitemsasshownhere:
(defresult(chan(dropping-buffer2)))
(go-loop[]
(<!(async/timeout1000))
(when-let[x(<!result)]
(prn"Gotvalue:"x)
(recur)))
(go(doseq[n(range5)]
(>!resultn))
(prn"Doneputtingvalues!")
(close!result))
;;"Doneputtingvalues!"
;;"Gotvalue:"0
;;"Gotvalue:"1
Asbefore,westillhaveabufferofsizetwo,butthistimetheproducerendsquicklywithoutevergettingblocked.Thedropping-buffersimplyignoredallitemsoveritslimit.
SlidingbufferAdrawbackofdroppingbuffersisthatwemightnotbeprocessingthelatestitemsatagiventime.Forthetimeswhereprocessingthelatestinformationisamust,wecanuseaslidingbuffer:
(defresult(chan(sliding-buffer2)))
(go-loop[]
(<!(async/timeout1000))
(when-let[x(<!result)]
(prn"Gotvalue:"x)
(recur)))
(go(doseq[n(range5)]
(>!resultn))
(prn"Doneputtingvalues!")
(close!result))
;;"Doneputtingvalues!"
;;"Gotvalue:"3
;;"Gotvalue:"4
Asbefore,weonlygettwovaluesbuttheyarethelatestonesproducedbythegoloop.
Whenthelimitoftheslidingbufferisoverrun,core.asyncdropstheoldestitemstomakeroomforthenewestones.Iendupusingthisbufferingstrategymostofthetime.
TransducersBeforewefinishupwithourcore.asyncportionofthebook,itwouldbeunwiseofmenottomentionwhatiscomingupinClojure1.7aswellashowthisaffectscore.async.
Atthetimeofthiswriting,Clojure’slatestreleaseis1.7.0-alpha5—andeventhoughitisanalpharelease,alotofpeople—myselfincluded—arealreadyusingitinproduction.
Assuch,afinalversioncouldbejustaroundthecornerandperhapsbythetimeyoureadthis,1.7finalwillbeoutalready.
Oneofthebigchangesinthisupcomingreleaseistheintroductionoftransducers.Wewillnotcoverthenutsandboltsofitherebutratherfocusonwhatitmeansatahigh-levelwithexamplesusingbothClojuresequencesandcore.asyncchannels.
IfyouwouldliketoknowmoreIrecommendCarinMeier’sGreenEggsandTransducersblogpost(http://gigasquidsoftware.com/blog/2014/09/06/green-eggs-and-transducers/).It’sagreatplacetostart.
Additionally,theofficialClojuredocumentationsiteonthesubjectisanotherusefulresource(http://clojure.org/transducers).
Let’sgetstartedbycreatinganewleiningenproject:
$leinnewcore-async-transducers
Now,openyourproject.cljfileandmakesureyouhavetherightdependencies:
...
:dependencies[[org.clojure/clojure"1.7.0-alpha5"]
[org.clojure/core.async"0.1.346.0-17112a-alpha"]]
...
Next,fireupaREPLsessionintheprojectrootandrequirecore.async,whichwewillbeusingshortly:
$leinrepl
user>(require'[clojure.core.async:refer[gochanmap<filter<into>!<!
go-loopclose!pipe]])
Wewillstartwithafamiliarexample:
(->>(range10)
(mapinc);;createsanewsequence
(filtereven?);;createsanewsequence
(prn"resultis"))
;;"resultis"(246810)
TheprecedingsnippetisstraightforwardandhighlightsaninterestingpropertyofwhathappenswhenweapplycombinatorstoClojuresequences:eachcombinatorcreatesanintermediatesequence.
Inthepreviousexample,weendedupwiththreeintotal:theonecreatedbyrange,theonecreatedbymap,andfinallytheonecreatedbyfilter.Mostofthetime,thiswon’t
reallybeanissuebutforlargesequencesthismeansalotofunnecessaryallocation.
StartinginClojure1.7,thepreviousexamplecanbewrittenlikeso:
(defxform
(comp(mapinc)
(filtereven?)));;nointermediatesequencecreated
(->>(range10)
(sequencexform)
(prn"resultis"))
;;"resultis"(246810)
TheClojuredocumentationdescribestransducersascomposablealgorithmictransformations.Let’sseewhythatis.
Inthenewversion,awholerangeofthecoresequencecombinators,suchasmapandfilter,havegainedanextraarity:ifyoudon’tpassitacollection,itinsteadreturnsatransducer.
Inthepreviousexample,(mapinc)returnsatransducerthatknowshowtoapplythefunctioninctoelementsofasequence.Similarly,(filtereven?)returnsatransducerthatwilleventuallyfilterelementsofasequence.Neitherofthemdoanythingyet,theysimplyreturnfunctions.
Thisisinterestingbecausetransducersarecomposable.Webuildlargerandmorecomplextransducersbyusingsimplefunctioncomposition:
(defxform
(comp(mapinc)
(filtereven?)))
Oncewehaveourtransducerready,wecanapplyittoacollectioninafewdifferentways.Forthisexample,wechosesequenceasitwillreturnalazysequenceoftheapplicationsofthegiventransducertotheinputsequence:
(->>(range10)
(sequencexform)
(prn"resultis"))
;;"resultis"(246810)
Aspreviouslyhighlighted,thiscodedoesnotcreateintermediatesequences;transducersextracttheverycoreofthealgorithmictransformationathandandabstractsitawayfromhavingtodealwithsequencesdirectly.
Transducersandcore.asyncWemightnowbeaskingourselves“Whatdotransducershavetodowithcore.async?”
Itturnsoutthatoncewe’reabletoextractthecoreofthesetransformationsandputthemtogetherusingsimplefunctioncomposition,thereisnothingstoppingusfromusingtransducerswithdatastructuresotherthansequences!
Let’srevisitourfirstexampleusingstandardcore.asyncfunctions:
(defresult(chan10))
(deftransformed
(->>result
(map<inc);;createsanewchannel
(filter<even?);;createsanewchannel
(into[])))
(go
(prn"resultis"(<!transformed)))
(go
(doseq[n(range10)]
(>!resultn))
(close!result))
;;"resultis"[246810]
Thiscodeshouldlookfamiliarbynow:it’sthecore.asyncequivalentofthesequence-onlyversionshownearlier.Asbefore,wehaveunnecessaryallocationshereaswell,exceptthatthistimewe’reallocatingchannels.
Withthenewsupportfortransducers,core.asynccantakeadvantageofthesametransformationdefinedearlier:
(defresult(chan10))
(defxform
(comp(mapinc)
(filtereven?)));;nointermediatechannelscreated
(deftransformed(->>(piperesult(chan10xform))
(into[])))
(go
(prn"resultis"(<!transformed)))
(go
(doseq[n(range10)]
(>!resultn))
(close!result))
;;"resultis"[246810]
Thecoderemainslargelyunchangedexceptwenowusethesamexformtransformationdefinedearlierwhencreatinganewchannel.It’simportanttonotethatwedidnothavetousecore.asynccombinators—infactalotofthesecombinatorshavebeendeprecatedandwillberemovedinfutureversionsofcore.async.
Thefunctionsmapandfilterusedtodefinexformarethesameonesweusedpreviously,thatis,theyarecoreClojurefunctions.
Thisisthenextbigadvantageofusingtransducers:byremovingtheunderlyingdatastructurefromtheequationviatransducers,librariessuchascore.asynccanreuseClojure’scorecombinatorstopreventunnecessaryallocationandcodeduplication.
It’snottoofarfetchedtoimagineotherframeworkslikeRxClojurecouldtakeadvantageoftransducersaswell.Allofthemwouldbeabletousethesamecorefunctionacrosssubstantiallydifferentdatastructuresandcontexts:sequences,channels,andObervables.
TipTheconceptofextractingtheessenceofcomputationsdisregardingtheirunderlyingdatastructuresisanexcitingtopicandhasbeenseenbeforeintheHaskellcommunity,althoughtheydealwithlistsspecifically.
TwopapersworthmentioningonthesubjectareStreamFusion[11]byDuncanCoutts,RomanLeshchinskiyandDonStewartandTransformingprogramstoeliminatetrees[12]byPhilipWadler.Therearesomeoverlapssothereadermightfindtheseinteresting.
SummaryBynow,Ihopetohaveprovedthatyoucanwritereactiveapplicationsusingcore.async.It’sanextremelypowerfulandflexibleconcurrencymodelwitharichAPI.Ifyoucandesignyoursolutionintermsofqueues,mostlikelycore.asyncisthetoolyouwanttoreachfor.
ThisversionofthestockmarketapplicationisshorterandsimplerthantheversionusingonlythestandardJavaAPIwedevelopedearlierinthisbook—forinstance,wedidn’thavetoworryaboutthreadpools.Ontheotherhand,itfeelslikeitisalittlemorecomplexthantheversionimplementedusingReactiveExtensionsinChapter3,AsynchronousProgrammingandNetworking.
Thisisbecausecore.asyncoperatesatalowerlevelofabstractionwhencomparedtootherframeworks.Thisbecomesespeciallyobviousinourapplicationaswehadtoworryaboutcreatingbroadcastingchannels,goloops,andsoon—allofwhichcanbeconsideredincidentalcomplexity,notdirectlyrelatedtotheproblemathand.
core.asyncdoes,however,provideanexcellentfoundationforbuildingourownCESabstractions.Thisiswhatwewillbeexploringnext.
Chapter5.CreatingYourOwnCESFrameworkwithcore.asyncInthepreviouschapter,itwasalludedtothatcore.asyncoperatesatalowerlevelofabstractionwhencomparedtootherframeworkssuchasRxClojureorRxJava.
Thisisbecausemostofthetimewehavetothinkcarefullyaboutthechannelswearecreatingaswellaswhattypesandsizesofbufferstouse,whetherweneedpub/subfunctionality,andsoon.
Notallapplicationsrequiresuchlevelofcontrol,however.Nowthatwearefamiliarwiththemotivationsandmainabstractionsofcore.asyncwecanembarkintowritingaminimalCESframeworkusingcore.asyncastheunderlyingfoundation.
Bydoingso,weavoidhavingtothinkaboutthreadpoolmanagementastheframeworktakescareofthatforus.
Inthischapter,wewillcoverthefollowingtopics:
BuildingaCESframeworkusingcore.asyncasitsunderlyingconcurrencystrategyBuildinganapplicationthatusesourCESframeworkUnderstandingthetrade-offsofthedifferentapproachespresentedsofar
AminimalCESframeworkBeforewegetstartonthedetails,weshoulddefineatahighlevelwhatminimalmeans.
Let’sstartwiththetwomainabstractionsourframeworkwillprovide:behaviorsandeventstreams.
IfyoucanrecallfromChapter1,WhatisReactiveProgramming?,behaviorsrepresentcontinuous,time-varyingvaluessuchastimeoramousepositionbehavior.Eventstreams,ontheotherhand,representdiscreteoccurrencesatapointintimeT,suchaskeypress.
Next,weshouldthinkaboutwhatkindsofoperationswewouldliketosupport.Behaviorsarefairlysimplesoattheveryminimumweneedto:
CreatenewbehaviorsRetrievethecurrentvalueofabehaviorConvertabehaviorintoaneventstream
Eventstreamshavemoreinterestinglogicinplayandweshouldatleastsupporttheseoperations:
Push/deliveravaluedownthestreamCreateastreamfromagivenintervalTransformthestreamwiththemapandfilteroperationsCombinestreamswithflatmapSubscribetoastream
ThisisasmallsubsetbutbigenoughtodemonstratetheoverallarchitectureofaCESframework.Oncewe’redone,we’lluseittobuildasimpleexample.
ClojureorClojureScript?Herewe’llshiftgearsandaddanotherrequirementtoourlittlelibrary:itshouldworkbothinClojureandClojureScript.Atfirst,thismightsoundlikeatoughrequirement.However,rememberthatcore.async—thefoundationofourframework—worksbothontheJVMandinJavaScript.Thismeanswehavealotlessworktodotomakeithappen.
Itdoesmean,however,thatweneedtobecapableofproducingtwoartifactsfromourlibrary:ajarfileandaJavaScriptfile.Luckily,thereareleiningenplugins,whichhelpusdothatandwewillbeusingacoupleofthem:
lein-cljsbuild(seehttps://github.com/emezeske/lein-cljsbuild):LeiningenplugintomakebuildingClojureScripteasycljx(seehttps://github.com/lynaghk/cljx):ApreprocessorusedtowriteportableClojurecodebases,thatis,writeasinglefileandoutputboth.cljand.cljsfiles
Youdon’tneedtounderstandtheselibrariesingreatdetail.Weareonlyusingtheirbasicfunctionalityandwillbeexplainingthebitsandpiecesasweencounterthem.
Let’sgetstartedbycreatinganewleiningenproject.We’llcallourframeworkrespondent—oneofthemanysynonymsforthewordreactive:
$leinnewrespondent
Weneedtomakeafewchangestotheproject.cljfiletoincludethedependenciesandconfigurationswe’llbeusing.First,makesuretheprojectdependencieslooklikethefollowing:
:dependencies[[org.clojure/clojure"1.5.1"]
[org.clojure/core.async"0.1.303.0-886421-alpha"]
[org.clojure/clojurescript"0.0-2202"]]
Thereshouldbenosurpriseshere.Stillintheprojectfile,addthenecessaryplugins:
:plugins[[com.keminglabs/cljx"0.3.2"]
[lein-cljsbuild"1.0.3"]]
Thesearethepluginsthatwe’vementionedpreviously.Bythemselvestheydon’tdomuch,however,andneedtobeconfigured.
Forcljx,addthefollowingtotheprojectfile:
:cljx{:builds[{:source-paths["src/cljx"]
:output-path"target/classes"
:rules:clj}
{:source-paths["src/cljx"]
:output-path"target/classes"
:rules:cljs}]}
:hooks[cljx.hooks]
Theprevioussnippetdeservessomeexplanation.cljxallowsustowritecodethatisportablebetweenClojureandClojureScriptbyplacingannotationsitspreprocessorcanunderstand.Wewillseelaterwhattheseannotationslooklike,butthischunkof
configurationtellscljxwheretofindtheannotatedfilesandwheretooutputthemoncethey’reprocessed.
Forexample,basedontheprecedingrules,ifwehaveafilecalledsrc/cljx/core.cljxandwerunthepreprocessorwewillendupwiththetarget/classes/core.cljandtarget/classes/core.cljsoutputfiles.Thehooks,ontheotherhand,aresimplyaconvenientwaytoautomaticallyruncljxwheneverwestartaREPLsession.
Thenextpartoftheconfigurationisforcljsbuild:
:cljsbuild
{:builds[{:source-paths["target/classes"]
:compiler{:output-to"target/main.js"}}]}
cljsbuildprovidesleiningentaskstocompileClojuresriptsourcecodeintoJavaScript.Weknowfromourprecedingcljxconfigurationthatthesource.cljsfileswillbeundertarget/classes,soherewe’resimplytellingcljsbuildtocompileallClojureScriptfilesinthatdirectoryandspitthecontentstotarget/main.js.Thisisthelastpieceneededfortheprojectfile.
Goaheadanddeletethesefilescreatedbytheleiningentemplateaswewon’tbeusingthem:
$rmsrc/respondent/core.clj
$rmtest/respondent/core_test.clj
Then,createanewcore.cljxfileundersrc/cljx/respondent/andaddthefollowingnamespacedeclaration:
(nsrespondent.core
(:refer-clojure:exclude[filtermapdeliver])
#+clj
(:import[clojure.langIDeref])
#+clj
(:require[clojure.core.async:asasync
:refer[gogo-loopchan<!>!timeout
map>filter>close!multtapuntap]])
#+cljs
(:require[cljs.core.async:asasync
:refer[chan<!>!timeoutmap>filter>
close!multtapuntap]])
#+cljs
(:require-macros[respondent.core:refer[behavior]]
[cljs.core.async.macros:refer[gogo-loop]]))
Here,westartseeingcljxannotations.cljxissimplyatextpreprocessor,sowhenitisprocessingafileusingcljrules—asseenintheconfiguration—itwillkeepthes-expressionsprecededbytheannotation#+cljintheoutputfile,whileremovingtheonesprefixedby#+cljs.Thereverseprocesshappenswhenusingcljsrules.
ThisisnecessarybecausemacrosneedtobecompiledontheJVM,sotheyhavetobe
includedseparatelyusingthe:require-macrosnamespaceoptionwhenusingClojureScript.Don’tworryaboutthecore.asyncfunctionswehaven’tencounteredbefore;theywillbeexplainedasweusethemtobuildourframework.
Also,notehowweareexcludingfunctionsfromtheClojurestandardAPIaswewishtousethesamenamesanddon’twantanyundesirednamingcollisions.
Thissectionsetusupwithanewprojectandthepluginsandconfigurationsneededforourframework.We’rereadytostartimplementingit.
DesigningthepublicAPIOneoftherequirementsforbehaviorsweagreedonistheabilitytoturnagivenbehaviorintoaneventstream.Acommonwayofdoingthisisbysamplingabehaviorataspecificinterval.Ifwetakethemousepositionbehaviorasanexample,bysamplingiteveryxsecondswegetaneventstream,whichwillemitthecurrentmousepositionatdiscretepointsintime.
Thisleadstothefollowingprotocol:
(defprotocolIBehavior
(sample[binterval]
"TurnsthisBehaviorintoanEventStreamfromthesampledvaluesatthe
giveninterval"))
Ithasasinglefunction,sample,whichwedescribedintheprecedingcode.Therearemorethingsweneedtodowithabehavior,butfornowthiswillsuffice.
OurnextmainabstractionisEventStream,which—basedontherequirementsseenpreviously—leadstothefollowingprotocol:
(defprotocolIEventStream
(map[sf]
"Returnsanewstreamcontainingtheresultofapplyingf
tothevaluesins")
(filter[spred]
"Returnsanewstreamcontainingtheitemsfroms
forwhichpredreturnstrue")
(flatmap[sf]
"TakesafunctionffromvaluesinstoanewEventStream.
ReturnsanEventStreamcontainingvaluesfromallunderlyingstreams
combined.")
(deliver[svalue]
"Deliversavaluetothestreams")
(completed?[s]
"Returnstrueifthisstreamhasstoppedemittingvalues.False
otherwise."))
Thisgivesusafewbasicfunctionstotransformandqueryaneventstream.Itdoesleaveouttheabilitytosubscribetoastream.Don’tworry,Ididn’tforgetit!
Although,itiscommontosubscribetoaneventstream,theprotocolitselfdoesn’tmandateitandthisisbecausetheoperationfitsbestinitsownprotocol:
(defprotocolIObservable
(subscribe[obsf]"Registeracallbacktobeinvokedwhentheunderlying
sourcechanges.
Returnsatokenthesubscribercanusetocancelthesubscription."))
Asfarassubscriptionsgo,itisusefultohaveawayofunsubscribingfromastream.Thiscanbeimplementedinacoupleofways,butdocstringoftheprecedingfunctionhintsataspecificone:atokenthatcanbeusedtounsubscribefromastream.Thisleadstoourlastprotocol:
ImplementingtokensThetokentypeisthesimplestinthewholeframeworkasithasgotasinglefunctionwithastraightforwardimplementation:
(deftypeToken[ch]
IToken
(dispose[_]
(close!ch)))
Itsimplycloseswhateverchannelitisgiven,stoppingeventsfromflowingthroughsubscriptions.
ImplementingeventstreamsTheeventstreamimplementation,ontheotherhand,isthemostcomplexinourframework.We’lltackleitgradually,implementingandexperimentingaswego.
First,let’slookatourmainconstructorfunction,event-stream:
(defnevent-stream
"Createsandreturnsaneweventstream.Youcanoptionallyprovidean
existing
core.asyncchannelasthesourceforthenewstream"
([]
(event-stream(chan)))
([ch]
(let[multiple(multch)
completed(atomfalse)]
(EventStream.chmultiplecompleted))))
ThedocstringshouldbesufficienttounderstandthepublicAPI.Whatmightnotbeclear,however,iswhatalltheconstructorargumentsmean.Fromlefttoright,theargumentstoEventStreamare:
ch:Thisisthecore.asyncchannelbackingthisstream.multiple:Thisisawaytobroadcastinformationfromonechanneltomanyotherchannels.It’sacore.asyncconceptwewillbeexplainingshortly.completed:ABooleanflagindicatingwhetherthiseventstreamhascompletedandwillnotemitanynewvalues.
Fromtheimplementation,youcanseethatthemultipleiscreatedfromthechannelbackingthestream.multipleworkskindoflikeabroadcast.Considerthefollowingexample:
(defin(chan))
(defmultiple(multin))
(defout-1(chan))
(tapmultipleout-1)
(defout-2(chan))
(tapmultipleout-2)
(go(>!in"Singleput!"))
(go(prn"Gotfromout-1"(<!out-1)))
(go(prn"Gotfromout-2"(<!out-2)))
Intheprevioussnippet,wecreateaninputchannel,in,andmultofitcalledmultiple.Then,wecreatetwooutputchannels,out-1andout-2,whicharebothfollowedbyacalltotap.Thisessentiallymeansthatwhatevervaluesarewrittentoinwillbetakenbymultipleandwrittentoanychannelstappedintoitasthefollowingoutputshows:
"Gotfromout-1""Singleput!"
"Gotfromout-2""Singleput!"
ThiswillmakeunderstandingtheEventStreamimplementationeasier.
Next,let’shavealookatwhataminimalimplementationoftheEventStreamlookslikethefollowing—makesuretheimplementationgoesbeforetheconstructorfunctiondescribedearlier:
(declareevent-stream)
(deftypeEventStream[channelmultiplecompleted]
IEventStream
(map[_f]
(let[out(map>f(chan))]
(tapmultipleout)
(event-streamout)))
(deliver[_value]
(if(=value::complete)
(do(reset!completedtrue)
(go(>!channelvalue)
(close!channel)))
(go(>!channelvalue))))
IObservable
(subscribe[thisf]
(let[out(chan)]
(tapmultipleout)
(go-loop[]
(let[value(<!out)]
(when(andvalue(not=value::complete))
(fvalue)
(recur))))
(Token.out))))
Fornow,wehavechosentoimplementonlythemapanddeliverfunctionsfromtheIEventStreamprotocol.Thisallowsustodelivervaluestothestreamaswellastransformthosevalues.
However,thiswouldnotbeveryusefulifwecouldnotretrievethevaluesdelivered.ThisiswhywealsoimplementthesubscribefunctionfromtheIObservableprotocol.
Inanutshell,mapneedstotakeavaluefromtheinputstream,applyafunctiontoit,andsendittothenewlycreatedstream.Wedothisbycreatinganoutputchannelthattapsoncurrentmultiple.Wethenusethischanneltobacktheneweventstream.
Thedeliverfunctionsimplyputsthevalueintothebackingchannel.Ifthevalueisthenamespacedkeyword::complete,weupdatethecompletedatomandclosethebackingchannel.Thisensuresthestreamwillnotemitanyothervalues.
Finally,wehavethesubscribefunction.Thewaysubscribersarenotifiedisbyusinganoutputchanneltappedtobackingmultiple.Weloopindefinitelycallingthesubscribingfunctionwheneveranewvalueisemitted.
Wefinishbyreturningatoken,whichwillclosetheoutputchanneloncedisposed,causingthego-looptostop.
Let’smakesurethatallthismakessensebyexperimentingwithacoupleofexamplesintheREPL:
(defes1(event-stream))
(subscribees1#(prn"firsteventstreamemitted:"%))
(deliveres110)
;;"firsteventstreamemitted:"10
(defes2(mapes1#(*2%)))
(subscribees2#(prn"secondeventstreamemitted:"%))
(deliveres120)
;;"firsteventstreamemitted:"20
;;"secondeventstreamemitted:"40
Excellent!Wehaveaminimal,workingimplementationofourIEventStreamprotocol!
Thenextfunctionwe’llimplementisfilteranditisverysimilartomap:
(filter[_pred]
(let[out(filter>pred(chan))]
(tapmultipleout)
(event-streamout)))
Theonlydifferenceisthatweusethefilter>functionandpredshouldbeaBooleanfunction:
(defes1(event-stream))
(defes2(filteres1even?))
(subscribees1#(prn"firsteventstreamemitted:"%))
(subscribees2#(prn"secondeventstreamemitted:"%))
(deliveres12)
(deliveres13)
(deliveres14)
;;"firsteventstreamemitted:"2
;;"secondeventstreamemitted:"2
;;"firsteventstreamemitted:"3
;;"firsteventstreamemitted:"4
;;"secondeventstreamemitted:"4
Aswewitness,es2onlyemitsanewvalueifandonlyifthatvalueisanevennumber.
TipIfyouarefollowingalong,typingtheexamplesstepbystep,youwillneedtorestartyourREPLwheneverweaddnewfunctionstoanydeftypedefinition.ThisisbecausedeftypegeneratesandcompilesaJavaclasswhenevaluated.Assuch,simplyreloadingthenamespacewon’tbeenough.
Alternatively,youcanuseatoolsuchastools.namespace(seehttps://github.com/clojure/tools.namespace)thataddressessomeoftheseREPLreloadinglimitations.
Movingdownourlist,wenowhaveflatmap:
(flatmap[_f]
(let[es(event-stream)
out(chan)]
(tapmultipleout)
(go-loop[]
(when-let[a(<!out)]
(let[mb(fa)]
(subscribemb(fn[b]
(deliveresb)))
(recur))))
es))
We’veencounteredthisoperatorbeforewhensurveyingReactiveExtensions.Thisiswhatourdocstringsaysaboutit:
TakesafunctionffromvaluesinstoanewEventStream.
ReturnsanEventStreamcontainingvaluesfromallunderlyingstreamscombined.
Thismeansflatmapneedstocombineallthepossibleeventstreamsintoasingleoutputeventstream.Asbefore,wetapanewchanneltothemultiplestream,butthenweloopovertheoutputchannel,applyingftoeachoutputvalue.
However,aswesaw,fitselfreturnsaneweventstream,sowesimplysubscribetoit.Wheneverthefunctionregisteredinthesubscriptiongetscalled,wedeliverthatvaluetotheoutputeventstream,effectivelycombiningallstreamsintoasingleone.
Considerthefollowingexample:
(defnrange-es[n]
(let[es(event-stream(chann))]
(doseq[n(rangen)]
(deliveresn))
es))
(defes1(event-stream))
(defes2(flatmapes1range-es))
(subscribees1#(prn"firsteventstreamemitted:"%))
(subscribees2#(prn"secondeventstreamemitted:"%))
(deliveres12)
;;"firsteventstreamemitted:"2
;;"secondeventstreamemitted:"0
;;"secondeventstreamemitted:"1
(deliveres13)
;;"firsteventstreamemitted:"3
;;"secondeventstreamemitted:"0
;;"secondeventstreamemitted:"1
;;"secondeventstreamemitted:"2
Wehaveafunction,range-es,thatreceivesanumbernandreturnsaneventstreamthatemitsnumbersfrom0ton.Asbefore,wehaveastartingstream,es1,andatransformedstreamcreatedwithflatmap,es2.
Wecanseefromtheprecedingoutputthatthestreamcreatedbyrange-esgetsflattenedintoes2allowingustoreceiveallvaluesbysimplysubscribingtoitonce.
ThisleavesuswithsinglefunctionfromIEventStreamlefttoimplement:
(completed?[_]@completed)
completed?simplyreturnsthecurrentvalueofthecompletedatom.Wearenowreadytoimplementbehaviors.
ImplementingbehaviorsIfyourecall,theIBehaviorprotocolhasasinglefunctioncalledsamplewhosedocstringstates:TurnsthisBehaviorintoanEventStreamfromthesampledvaluesatthegiveninterval.
Inordertoimplementsample,wewillfirstcreateausefulhelperfunctionthatwewillcallfrom-interval:
(defnfrom-interval
"Createsandreturnsaneweventstreamwhichemitsvaluesatthegiven
interval.
Ifnootherargumentsaregiven,thevaluesstartat0andincrementby
oneateachdelivery.
Ifgivenseedandsuccitemitsseedandappliessucctoseedtoget
thenextvalue.Itthenappliessucctothepreviousresultandsoon."
([msecs]
(from-intervalmsecs0inc))
([msecsseedsucc]
(let[es(event-stream)]
(go-loop[timeout-ch(timeoutmsecs)
valueseed]
(when-not(completed?es)
(<!timeout-ch)
(deliveresvalue)
(recur(timeoutmsecs)(succvalue))))
es)))
Thedocstringfunctionshouldbeclearenoughatthisstage,butwewouldliketoensureweunderstanditsbehaviorcorrectlybytryingitattheREPL:
(defes1(from-interval500))
(defes1-token(subscribees1#(prn"Got:"%)))
;;"Got:"0
;;"Got:"1
;;"Got:"2
;;"Got:"3
;;...
(disposees1-token)
Asexpected,es1emitsintegersstartingatzeroat500-millisecondintervals.Bydefault,itwouldemitnumbersindefinitely;therefore,wekeepareferencetothetokenreturnedbycallingsubscribe.
Thiswaywecandisposeitwheneverwe’redone,causinges-1tocompleteandstopemittingitems.
Next,wecanfinallyimplementtheBehaviortype:
(deftypeBehavior[f]
IBehavior
(sample[_interval]
(from-intervalinterval(f)(fn[&args](f))))
IDeref
(#+cljderef#+cljs-deref[_]
(f)))
(defmacrobehavior[&body]
`(Behavior.#(do~@body)))
Abehavioriscreatedbypassingitafunction.Youcanthinkofthisfunctionasageneratorresponsibleforgeneratingthenextvalueinthiseventstream.
Thisgeneratorfunctionwillbecalledwheneverwe(1)dereftheBehavioror(2)attheintervalgiventosample.
ThebehaviormacroisthereforconvenienceandallowsustocreateanewBehaviorwithoutwrappingthebodyinafunctionourselves:
(deftime-behavior(behavior(System/nanoTime)))
@time-behavior
;;201003153977194
@time-behavior
;;201005133457949
Intheprecedingexample,wedefinedtime-behaviorthatalwayscontainsthecurrentsystemtime.Wecanthenturnthisbehaviorintoastreamofdiscreteeventsbyusingthesamplefunction:
(deftime-stream(sampletime-behavior1500))
(deftoken(subscribetime-stream#(prn"Timeis"%)))
;;"Timeis"201668521217402
;;"Timeis"201670030219351
;;...
(disposetoken)
TipAlwaysremembertokeepareferencetothesubscriptiontokenwhendealingwithinfinitestreamssuchastheonescreatedbysampleandfrom-interval,orelseyoumightincurundesiredmemoryleaks.
Congratulations!Wehaveaworking,minimalCESframeworkusingcore.async!
Wedidn’tproveitworkswithClojureScript,however,whichwasoneofthemainrequirementsearlyon.That’sokay.WewillbetacklingthatsoonbydevelopingasimpleClojureScriptapplication,whichmakesuseofournewframework.
Inordertodothis,weneedtodeploytheframeworktoourlocalMavenrepository.Fromtheprojectroot,typethefollowingleincommand:
$leininstall
Rewritingsrc/cljxtotarget/classes(clj)withfeatures#{clj}and0
transformations.
Rewritingsrc/cljxtotarget/classes(cljs)withfeatures#{cljs}and1
transformations.
Exercise5.1ExtendourcurrentEventStreamimplementationtoincludeafunctioncalledtake.ItworksmuchlikeClojure’scoretakefunctionforsequences:itwilltakenitemsfromtheunderlyingeventstreamafterwhichitwillstopemittingitems.
Asampleinteraction,whichtakesthefirstfiveitemsemittedfromtheoriginaleventstream,isshownhere:
(defes1(from-interval500))
(deftake-es(takees15))
(subscribetake-es#(prn"Takevalues:"%))
;;"Takevalues:"0
;;"Takevalues:"1
;;"Takevalues:"2
;;"Takevalues:"3
;;"Takevalues:"4
TipKeepingsomestatemightbeusefulhere.Atomscanhelp.Additionally,trytothinkofawaytodisposeofanyunwantedsubscriptionsrequiredbythesolution.
Exercise5.2Inthisexercise,wewilladdafunctioncalledzipthatzipstogetheritemsemittedfromtwodifferenteventstreamsintoavector.
Asampleinteractionwiththezipfunctionisasfollows:
(defes1(from-interval500))
(defes2(map(from-interval500)#(*%2)))
(defzipped(zipes1es2))
(deftoken(subscribezipped#(prn"Zippedvalues:"%)))
;;"Zippedvalues:"[00]
;;"Zippedvalues:"[12]
;;"Zippedvalues:"[24]
;;"Zippedvalues:"[36]
;;"Zippedvalues:"[48]
(disposetoken)
TipForthisexercise,weneedawaytoknowwhenwehaveenoughitemstoemitfrombotheventstreams.Managingthisinternalstatecanbetrickyatfirst.Clojure’sreftypesand,inparticular,dosync,canbeofuse.
ArespondentapplicationThischapterwouldnotbecompleteifwedidn’tgothroughthewholedevelopmentlifecycleofdeployingandusingthenewframeworkinanewapplication.Thisisthepurposeofthissection.
Theapplicationwewillbuildisextremelysimple.Allitdoesistrackthepositionofthemouseusingthereactiveprimitiveswebuiltintorespondent.
Tothatend,wewillbeusingtheexcellentleintemplatecljs-start(seehttps://github.com/magomimmo/cljs-start),createdbyMimmoCosenzatohelpdevelopersgetstartedwithClojureScript.
Let’sgetstarted:
leinnewcljs-startrespondent-app
Next,let’smodifytheprojectfiletoincludethefollowingdependencies:
[clojure-reactive-programming/respondent"0.1.0-SNAPSHOT"]
[prismatic/dommy"0.1.2"]
Thefirstdependencyisself-explanatory.It’ssimplyourownframework.dommyisaDOMmanipulationlibraryforClojureScript.We’llbrieflyuseitwhenbuildingourwebpage.
Next,editthedev-resources/public/index.htmlfiletomatchthefollowing:
<!doctypehtml>
<htmllang="en">
<head>
<metacharset="utf-8">
<title>Example:trackingmouseposition</title>
<!--[ifltIE9]>
<scriptsrc="http://html5shiv.googlecode.com/svn/trunk/html5.js">
</script>
<![endif]-->
</head>
<body>
<divid="test">
<h1>Mouse(x,y)coordinates:</h1>
</div>
<divid="mouse-xy">
(0,0)
</div>
<scriptsrc="js/respondent_app.js"></script>
</body>
</html>
Intheprecedingsnippet,wecreatedanewdivelement,whichwillcontainthemouseposition.Itdefaultsto(0,0).
Thelastpieceofthepuzzleismodifyingsrc/cljs/respondent_app/core.cljstomatchthefollowing:
(nsrespondent-app.core
(:require[respondent.core:asr]
[dommy.core:asdommy])
(:use-macros
[dommy.macros:only[sel1]]))
(defmouse-pos-stream(r/event-stream))
(set!(.-onmousemovejs/document)
(fn[e]
(r/delivermouse-pos-stream[(.-pageXe)(.-pageYe)])))
(r/subscribemouse-pos-stream
(fn[[xy]]
(dommy/set-text!(sel1:#mouse-xy)
(str"("x","y")"))))
Thisisourmainapplicationlogic.Itcreatesaneventstreamtowhichwedeliverthecurrentmousepositionfromtheonmousemoveeventofthebrowserwindow.
Later,wesimplysubscribetoitandusedommytoselectandsetthetextofthedivelementweaddedpreviously.
Wearenowreadytousetheapp!Let’sstartbycompilingClojureScript:
$leincompile
Thisshouldtakeafewseconds.Ifalliswell,thenextthingtodoistostartaREPLsessionandstartuptheserver:
$leinrepl
user=>(run)
Now,pointyourbrowsertohttp://localhost:3000/anddragthemousearoundtoseeitscurrentposition.
Congratulationsonsuccessfullydeveloping,deploying,andusingyourownCESframework!
CESversuscore.asyncAtthisstage,youmightbewonderingwhenyoushouldchooseoneapproachovertheother.Afterall,asdemonstratedatthebeginningofthischapter,wecouldusecore.asynctodoeverythingwehavedoneusingrespondent.
Itallcomesdowntousingtherightlevelofabstractionforthetaskathand.
core.asyncgivesusmanylowlevelprimitivesthatareextremelyusefulwhenworkingwithprocesses,whichneedtotalktoeachother.Thecore.asyncchannelsworkasconcurrentblockingqueuesandareanexcellentsynchronizationmechanisminthesescenarios.
However,itmakesothersolutionshardertoimplement:forinstance,channelsaresingle-takebydefault,soifwehavemultipleconsumersinterestedinthevaluesputinsideachannel,wehavetoimplementthedistributionourselvesusingtoolssuchasmultandtap.
CESframeworks,ontheotherhand,operateatahigherlevelofabstractionandworkwithmultiplesubscribersbydefault.
Additionally,core.asyncreliesonsideeffects,ascanbeseenbytheuseoffunctionssuchas>!insidegoblocks.FrameworkssuchasRxClojurepromotestreamtransformationsbytheuseofpurefunctions.
Thisisnottosaytherearen’tsideeffectsinCESframeworks.Theremostdefinitelyare.However,asaconsumerofthelibrary,thisismostlyhiddenfromoureyes,allowingustothinkofmostofourcodeassimplesequencetransformations.
Inconclusion,differentapplicationdomainswillbenefitmoreorlessfromeitherapproach—sometimestheycanbenefitfromboth.Weshouldthinkhardaboutourapplicationdomainandanalyzethetypesofsolutionsandidiomsdevelopersaremostlikelytodesign.Thiswillpointusinthedirectionofbetterabstractionforwhateverapplicationwearedevelopingatagiventime.
SummaryInthischapter,wedevelopedourveryownCESframework.Bydevelopingourownframework,wehavesolidifiedourunderstandingofbothCESandhowtoeffectivelyusecore.async.
Theideathatcore.asynccouldbeusedasthefoundationofaCESframeworkisn’tmine,however.JamesReeves(seehttps://github.com/weavejester)—creatoroftheroutinglibraryCompojure(seehttps://github.com/weavejester/compojure)andmanyotherusefulClojurelibraries—alsosawthesamepotentialandsetofftowriteReagi(seehttps://github.com/weavejester/reagi),aCESlibrarybuiltontopofcore.async,similarinspirittotheonewedevelopedinthischapter.
Hehasputalotmoreeffortintoit,makingitamorerobustoptionforapureClojureframework.We’llbelookingatitinthenextchapter.
Chapter6.BuildingaSimpleClojureScriptGamewithReagiInthepreviouschapter,welearnedhowaframeworkforCompositionalEventSystems(CES)worksbybuildingourownframework,whichwecalledrespondent.Itgaveusagreatinsightintothemainabstractionsinvolvedinsuchapieceofsoftwareaswellasagoodoverviewofcore.async,Clojure’slibraryforasynchronousprogrammingandthefoundationofourframework.
Respondentisbutatoyframework,however.Wepaidlittleattentiontocross-cuttingconcernssuchasmemoryefficiencyandexceptionhandling.Thatisokayasweuseditasavehicleforlearningmoreabouthandlingandcomposingeventsystemswithcore.async.Additionally,itsdesignisintentionallysimilartoReagi’sdesign.
Inthischapter,wewill:
LearnaboutReagi,aCESframeworkbuiltontopofcore.asyncUseReagitobuildtherudimentsofaClojureScriptgamethatwillteachushowtohandleuserinputinacleanandmaintainablewayBrieflycompareReagitootherCESframeworksandgetafeelforwhentouseeachone
SettinguptheprojectHaveyoueverplayedAsteroids?Ifyouhaven’t,AsteroidsisanarcadespaceshooterfirstreleasedbyAtariin1979.InAsteroids,youarethepilotofashipflyingthroughspace.Asyoudoso,yougetsurroundedbyasteroidsandflyingsaucersyouhavetoshootanddestroy.
Developingthewholegameinonechapteristooambitiousandwoulddistractusfromthesubjectofthisbook.Wewilllimitourselvestomakingsurewehaveashiponthescreenwecanflyaroundaswellasshootspacebulletsintothevoid.Bytheendofthischapter,wewillhavesomethingthatlookslikewhatisshowninthefollowingscreenshot:
Togetstarted,wewillcreateanewClojureScriptprojectusingthesameleiningentemplateweusedinthepreviouschapter,cljs-start(seehttps://github.com/magomimmo/cljs-start):
leinnewcljs-startreagi-game
Next,addthefollowingdependenciestoyourprojectfile:
[org.clojure/clojurescript"0.0-2138"]
[reagi"0.10.0"]
[rm-hull/monet"0.1.12"]
Thelastdependency,monet(seehttps://github.com/rm-hull/monet),isaClojureScriptlibraryyoucanusetoworkwithHTML5Canvas.Itisahigh-levelwrapperontopoftheCanvasAPIandmakesinteractingwithitalotsimpler.
Beforewecontinue,it’sprobablyagoodideatomakesureoursetupisworkingproperly.Changeintotheprojectdirectory,startaClojureREPL,andthenstarttheembeddedwebserver:
cdreagi-game/
leinrepl
CompilingClojureScript.
Compiling"dev-resources/public/js/reagi_game.js"from("src/cljs"
"test/cljs""dev-resources/tools/repl")...
user=>(run)
2014-06-1419:21:40.381:INFO:oejs.Server:jetty-7.6.8.v20121106
2014-06-1419:21:40.403:INFO:oejs.AbstractConnector:Started
[email protected]:3000
#<Serverorg.eclipse.jetty.server.Server@51f6292b>
ThiswillcompiletheClojureScriptsourcefilestoJavaScriptandstartthesamplewebserver.Inyourbrowser,navigatetohttp://localhost:3000/.Ifyouseesomethinglikethefollowing,wearegoodtogo:
AswewillbeworkingwithHTML5Canvas,weneedanactualcanvastorenderto.Let’supdateourHTMLdocumenttoincludethat.It’slocatedunderdev-resources/public/index.html:
<!doctypehtml>
<htmllang="en">
<head>
<metacharset="utf-8">
<title>bREPLConnection</title>
<!--[ifltIE9]>
<scriptsrc="http://html5shiv.googlecode.com/svn/trunk/html5.js">
</script>
<![endif]-->
</head>
<body>
<canvasid="canvas"width="800"height="600"></canvas>
<scriptsrc="js/reagi_game.js"></script>
</body>
</html>
WehaveaddedacanvasDOMelementtoourdocument.Allrenderingwillhappeninthiscontext.
GameentitiesOurgamewillhaveonlytwoentities:onerepresentingthespaceshipandtheotherrepresentingbullets.Tobetterorganizethecode,wewillputallentity-relatedcodeinitsownfile,src/cljs/reagi_game/entities.cljs.Thisfilewillalsocontainsomeoftherenderinglogic,sowe’llneedtorequiremonet:
(nsreagi-game.entities
(:require[monet.canvas:ascanvas]
[monet.geometry:asgeom]))
Next,we’lladdafewhelperfunctionstoavoidrepeatingourselvestoomuch:
(defnshape-x[shape]
(->shape:posderef:x))
(defnshape-y[shape]
(->shape:posderef:y))
(defnshape-angle[shape]
@(:angleshape))
(defnshape-data[xyangle]
{:pos(atom{:xx:yy})
:angle(atomangle)})
Thefirstthreefunctionsaresimplyashorterwayofgettingdataoutofourshapedatastructure.Theshape-datafunctioncreatesastructure.Notethatweareusingatoms,oneofClojure’sreferencetypes,torepresentashape’spositionandangle.
Thisway,wecansafelypassourshapedataintomonet’srenderingfunctionsandstillbeabletoupdateitinaconsistentway.
Nextupisourshipconstructorfunction.Thisiswherethebulkoftheinteractionwithmonethappens:
(defnship-entity[ship]
(canvas/entity{:x(shape-xship)
:y(shape-yship)
:angle(shape-angleship)}
(fn[value]
(->value
(assoc:x(shape-xship))
(assoc:y(shape-yship))
(assoc:angle(shape-angleship))))
(fn[ctxval]
(->ctx
canvas/save
(canvas/translate(:xval)(:yval))
(canvas/rotate(:angleval))
(canvas/begin-path)
(canvas/move-to500)
(canvas/line-to0-15)
(canvas/line-to015)
(canvas/fill)
canvas/restore))))
Thereisquiteabitgoingon,solet’sbreakitdown.
canvas/entityisamonetconstructorandexpectsyoutoprovidethreeargumentsthatdescribeourship:itsinitialx,ycoordinatesandangle,anupdatefunctionthatgetscalledinthedrawloop,andadrawfunctionthatisresponsibleforactuallydrawingtheshapeontothescreenaftereachupdate.
Theupdatefunctionisfairlystraightforward:
(fn[value]
(->value
(assoc:x(shape-xship))
(assoc:y(shape-yship))
(assoc:angle(shape-angleship))))
Wesimplyupdateitsattributestothecurrentvaluesfromtheship’satoms.
Thenextfunction,responsiblefordrawing,interactswithmonet’sAPImoreheavily:
(fn[ctxval]
(->ctx
canvas/save
(canvas/translate(:xval)(:yval))
(canvas/rotate(:angleval))
(canvas/begin-path)
(canvas/move-to500)
(canvas/line-to0-15)
(canvas/line-to015)
(canvas/fill)
canvas/restore))
Westartbysavingthecurrentcontextsothatwecanrestorethingssuchasdrawingstyleandcanvaspositioninglater.Next,wetranslatethecanvastotheship’sx,ycoordinatesandrotateitaccordingtoitsangle.Wethenstartdrawingourshape,atriangle,andfinishbyrestoringoursavedcontext.
Thenextfunctionalsocreatesanentity,ourbullet:
(declaremove-forward!)
(defnmake-bullet-entity[monet-canvaskeyshape]
(canvas/entity{:x(shape-xshape)
:y(shape-yshape)
:angle(shape-angleshape)}
(fn[value]
(when(not
(geom/contained?
{:x0:y0
:w(.-width(:canvasmonet-canvas))
:h(.-height(:canvasmonet-canvas))}
{:x(shape-xshape)
:y(shape-yshape)
:r5}))
(canvas/remove-entitymonet-canvaskey))
(move-forward!shape)
(->value
(assoc:x(shape-xshape))
(assoc:y(shape-yshape))
(assoc:angle(shape-angleshape))))
(fn[ctxval]
(->ctx
canvas/save
(canvas/translate(:xval)(:yval))
(canvas/rotate(:angleval))
(canvas/fill-style"red")
(canvas/circle{:x10:y0:r5})
canvas/restore))))
Asbefore,let’sinspecttheupdateanddrawingfunctions.We’llstartwithupdate:
(fn[value]
(when(not
(geom/contained?
{:x0:y0
:w(.-width(:canvasmonet-canvas))
:h(.-height(:canvasmonet-canvas))}
{:x(shape-xshape)
:y(shape-yshape)
:r5}))
(canvas/remove-entitymonet-canvaskey))
(move-forward!shape)
(->value
(assoc:x(shape-xshape))
(assoc:y(shape-yshape))
(assoc:angle(shape-angleshape))))
Bulletshavealittlemorelogicintheirupdatefunction.Asyoufirethemfromtheship,wemightcreatehundredsoftheseentities,soit’sagoodpracticetogetridofthemassoonastheygooffthevisiblecanvasarea.That’sthefirstthingthefunctiondoes:itusesgeom/contained?tocheckwhethertheentityiswithinthedimensionsofthecanvas,removingitwhenitisn’t.
Differentfromtheship,however,bulletsdon’tneeduserinputinordertomove.Oncefired,theymoveontheirown.That’swhythenextthingwedoiscallmove-forward!Wehaven’timplementedthisfunctionyet,sowehadtodeclareitbeforehand.We’llgettoit.
Oncethebullet’scoordinatesandanglehavebeenupdated,wesimplyreturnthenewentity.
Thedrawfunctionisabitsimplerthantheship’sversionmostlyduetoitsshapebeingsimpler;it’sjustaredcircle:
(fn[ctxval]
(->ctx
canvas/save
(canvas/translate(:xval)(:yval))
(canvas/rotate(:angleval))
(canvas/fill-style"red")
(canvas/circle{:x10:y0:r5})
canvas/restore))
Now,we’llmoveontothefunctionsresponsibleforupdatingourshape’scoordinatesandangle,startingwithmove!:
(defspeed200)
(defncalculate-x[angle]
(*speed(/(*(Math/cosangle)
Math/PI)
180)))
(defncalculate-y[angle]
(*speed(/(*(Math/sinangle)
Math/PI)
180)))
(defnmove![shapef]
(let[pos(:posshape)]
(swap!pos(fn[xy]
(->xy
(update-in[:x]
#(f%(calculate-x
(shape-angleshape))))
(update-in[:y]
#(f%(calculate-y
(shape-angleshape)))))))))
Tokeepthingssimple,boththeshipandbulletsusethesamespeedvaluetocalculatetheirpositioning,heredefinedas200.
move!takestwoarguments:theshapemapandafunctionf.Thisfunctionwilleitherbethe+(plus)orthe-(minus)function,dependingonwhetherwe’removingforwardorbackward,respectively.Next,itupdatestheshape’sx,ycoordinatesusingsomebasictrigonometry.
Ifyou’rewonderingwhywearepassingtheplusandminusfunctionsasarguments,it’sallaboutnotrepeatingourselves,asthenexttwofunctionsshow:
(defnmove-forward![shape]
(move!shape+))
(defnmove-backward![shape]
(move!shape-))
Withmovementtakencareof,thenextstepistowritetherotationfunctions:
(defnrotate![shapef]
(swap!(:angleshape)#(f%(/(/Math/PI3)20))))
(defnrotate-right![shape]
(rotate!shape+))
(defnrotate-left![shape]
(rotate!shape-))
Sofar,we’vegotshipmovementcovered!Butwhatgoodisourshipifwecan’tfirebullets?Let’smakesurewehavethatcoveredaswell:
(defnfire![monet-canvasship]
(let[entity-key(keyword(gensym"bullet"))
data(shape-data(shape-xship)
(shape-yship)
(shape-angleship))
bullet(make-bullet-entitymonet-canvas
entity-key
data)]
(canvas/add-entitymonet-canvasentity-keybullet)))
Thefire!functiontakestwoarguments:areferencetothegamecanvasandtheship.Itthencreatesanewbulletbycallingmake-bullet-entityandaddsittothecanvas.
NotehowweuseClojure’sgensymfunctiontocreateauniquekeyforthenewentity.Weusethiskeytoremoveanentityfromthegame.
Thisconcludesthecodefortheentitiesnamespace.
Tipgensymisquiteheavilyusedinwritinghygienicmacrosasyoucanbesurethatthegeneratedsymbolswillnotclashwithanylocalbindingsbelongingtothecodeusingthemacro.Macrosarebeyondthescopeofthisbook,butyoumightfindthisseriesofmacroexercisesusefulinthelearningprocess,athttps://github.com/leonardoborges/clojure-macros-workshop.
PuttingitalltogetherWe’renowreadytoassembleourgame.Goaheadandopenthecorenamespacefile,src/cljs/reagi_game/core.cljs,andaddthefollowing:
(nsreagi-game.core
(:require[monet.canvas:ascanvas]
[reagi.core:asr]
[clojure.set:asset]
[reagi-game.entities:asentities
:refer[move-forward!move-backward!rotate-left!rotate-
right!fire!]]))
Thefollowingsnippetsetsupvariousdatastructuresandreferenceswe’llneedinordertodevelopthegame:
(defcanvas-dom(.getElementByIdjs/document"canvas"))
(defmonet-canvas(canvas/initcanvas-dom"2d"))
(defship
(entities/shape-data(/(.-width(:canvasmonet-canvas))2)
(/(.-height(:canvasmonet-canvas))2)
0))
(defship-entity(entities/ship-entityship))
(canvas/add-entitymonet-canvas:ship-entityship-entity)
(canvas/draw-loopmonet-canvas)
Westartbycreatingmonet-canvasfromareferencetoourcanvasDOMelement.Wethencreateourshipdata,placingitatthecenterofthecanvas,andaddtheentitytomonet-canvas.Finally,westartadraw-loop,whichwillhandleouranimationsusingthebrowser’snativecapabilities—internallyitcallswindow.requestAnimationFrame(),ifavailable,butitfallsbacktowindow.setTimemout()otherwise.
Ifyouweretotrytheapplicationnow,thiswouldbeenoughtodrawtheshiponthemiddleofthescreen,butnothingelsewouldhappenaswehaven’tstartedhandlinguserinputyet.
Asfarasuserinputgoes,we’reconcernedwithafewactions:
Shipmovement:rotation,forward,andbackwardFiringtheship’sgunPausingthegame
Toaccountfortheseactions,we’lldefinesomeconstantsthatrepresenttheASCIIcodesofthekeysinvolved:
(defUP38)
(defRIGHT39)
(defDOWN40)
(defLEFT37)
(defFIRE32);;space
(defPAUSE80);;lower-caseP
Thisshouldlooksensibleasweareusingthekeystraditionallyusedforthesetypesofactions.
ModelinguserinputaseventstreamsOneofthethingsdiscussedintheearlierchaptersisthatifyoucanthinkofeventsasalistofthingsthathaven’thappenedyet;youcanprobablymodelitasaneventstream.Inourcase,thislistiscomposedbythekeystheplayerpressesduringthegameandcanbevisualizedlikeso:
Thereisacatchthough.Mostgamesneedtohandlesimultaneouslypressedkeys.
Sayyou’reflyingthespaceshipforwards.Youdon’twanttohavetostopitinordertorotateittotheleftandthencontinuemovingforwards.Whatyouwantistopressleftatthesametimeyou’repressingupandhavetheshiprespondaccordingly.
Thishintsatthefactthatweneedtobeabletotellwhethertheplayeriscurrentlypressingmultiplekeys.TraditionallythisisdoneinJavaScriptbykeepingtrackofwhichkeysarebeinghelddowninamap-likeobject,usingflags.Somethingsimilartothefollowingsnippet:
varkeysPressed={};
document.addEventListener('keydown',function(e){
keysPressed[e.keyCode]=true;
},false);
document.addEventListener('keyup',function(e){
keysPressed[e.keyCode]=false;
},false);
Then,laterinthegameloop,youwouldcheckwhethertherearemultiplekeysbeingpressed:
functiongameLoop(){
if(keyPressed[UP]&&keyPressed[LEFT]){
//updateshipposition
}
//...
}
Whilethiscodeworks,itreliesonmutatingthekeysPressedobjectwhichisn’tideal.
Additionally,withasetupsimilartotheprecedingone,thekeysPressedobjectisglobaltotheapplicationasitisneededbothinthekeyup/keydowneventhandlersaswellasinthegameloopitself.
Infunctionalprogramming,westrivetoeliminateorreducetheamountofglobalmutable
stateinordertowritereadable,maintainablecodethatislesserror-prone.Wewillapplytheseprincipleshere.
AsseenintheprecedingJavaScriptexample,wecanregistercallbackstobenotifiedwheneverakeyuporkeydowneventhappens.Thisisusefulaswecaneasilyturnthemintoeventstreams:
(defnkeydown-stream[]
(let[out(r/events)]
(set!(.-onkeydownjs/document)
#(r/deliverout[::down(.-keyCode%)]))
out))
(defnkeyup-stream[]
(let[out(r/events)]
(set!(.-onkeyupjs/document)
#(r/deliverout[::up(.-keyCode%)]))
out))
Bothkeydown-streamandkeyup-streamreturnanewstreamtowhichtheydelivereventswhenevertheyhappen.Eacheventistaggedwithakeyword,sowecaneasilyidentifyitstype.
Wewouldliketohandlebothtypesofeventssimultaneouslyandassuchweneedawaytocombinethesetwostreamsintoasingleone.
Therearemanywaysinwhichwecancombinestreams,forexample,usingoperatorssuchaszipandflatmap.Forthisinstance,however,weareinterestedinthemergeoperator.mergecreatesanewstreamthatemitsvaluesfrombothstreamsastheyarrive:
Thisgivesusenoughtostartcreatingourstreamofactivekeys.Basedonwhatwehavediscussedsofar,ourstreamlookssomethinglikethefollowingatthemoment:
(defactive-keys-stream
(->>(r/merge(keydown-stream)(keyup-stream))
...
))
Tokeeptrackofwhichkeysarecurrentlypressed,wewilluseaClojureScriptset.Thiswaywedon’thavetoworryaboutsettingflagstotrueorfalse—wecansimplyperformstandardsetoperationsandadd/removekeysfromthedatastructure.
Thenextthingweneedisawaytoaccumulatethepressedkeysintothissetasneweventsareemittedfromthemergedstream.
Infunctionalprogramming,wheneverwewishtoaccumulateoraggregatesometypeofdataoverasequenceofvalues,weusereduce.
Most—ifnotall—CESframeworkshavethisfunctionbuilt-in.RxJavacallsitscan.Reagi,ontheotherhand,callsitreduce,makingitintuitivetofunctionalprogrammersingeneral.
Thatisthefunctionwewillusetofinishtheimplementationofactive-keys-stream:
(defactive-keys-stream
(->>(r/merge(keydown-stream)(keyup-stream))
(r/reduce(fn[acc[event-typekey-code]]
(condp=event-type
::down(conjacckey-code)
::up(disjacckey-code)
acc))
#{})
(r/sample25)))
r/reducetakesthreearguments:areducingfunction,anoptionalinitial/seedvalue,andthestreamtoreduceover.
Ourseedvalueisanemptysetasinitiallytheuserhasn’tyetpressedanykeys.Then,ourreducingfunctioncheckstheeventtype,removingoraddingthekeyfrom/tothesetasappropriate.
Asaresult,whatwehaveisastreamliketheonerepresentedasfollows:
WorkingwiththeactivekeysstreamThegroundworkwe’vedonesofarwillmakesurewecaneasilyhandlegameeventsinacleanandmaintainableway.Themainideabehindhavingastreamrepresentingthegamekeysisthatnowwecanpartitionitmuchlikewewouldanormallist.
Forinstance,ifwe’reinterestedinalleventswherethekeypressedisUP,wewouldrunthefollowingcode:
(->>active-keys-stream
(r/filter(partialsome#{UP}))
(r/map(fn[_](.logjs/console"Pressedup…"))))
Similarly,foreventsinvolvingtheFIREkey,wecoulddothefollowing:
(->>active-keys-stream
(r/filter(partialsome#{FIRE}))
(r/map(fn[_](.logjs/console"Pressedfire…"))))
ThisworksbecauseinClojure,setscanbeusedaspredicates.WecanquicklyverifythisattheREPL:
user>(defnumbers#{121314})
#'user/numbers
user>(some#{12}numbers)
12
user>(some#{15}numbers)
nil
Byrepresentingtheeventsasastream,wecaneasilyoperateonthemusingfamiliarsequencefunctionssuchasmapandfilter.
Writingcodelikethis,however,isalittlerepetitive.Thetwopreviousexamplesareprettymuchsayingsomethingalongtheselines:filteralleventsmatchingagivenpredicatepredandthenmaptheffunctionoverthem.Wecanabstractthispatterninafunctionwe’llcallfilter-map:
(defnfilter-map[predf&args]
(->>active-keys-stream
(r/filter(partialsomepred))
(r/map(fn[_](applyfargs)))))
Withthishelperfunctioninplace,itbecomeseasytohandleourgameactions:
(filter-map#{FIRE}fire!monet-canvasship)
(filter-map#{UP}move-forward!ship)
(filter-map#{DOWN}move-backward!ship)
(filter-map#{RIGHT}rotate-right!ship)
(filter-map#{LEFT}rotate-left!ship)
TheonlythingmissingnowistakingcareofpausingtheanimationswhentheplayerpressesthePAUSEkey.Wefollowthesamelogicasabove,butwithaslightchange:
(defnpause![_]
(if@(:updating?monet-canvas)
(canvas/stop-updatingmonet-canvas)
(canvas/start-updatingmonet-canvas)))
(->>active-keys-stream
(r/filter(partialsome#{PAUSE}))
(r/throttle100)
(r/mappause!))
Monetmakesaflagavailablethattellsuswhetheritiscurrentlyupdatingtheanimationstate.Weusethatasacheapmechanismto“pause”thegame.
Notethatactive-keys-streampusheseventsastheyhappenso,ifauserisholdingabuttondownforanyamountoftime,wewillgetmultipleeventsforthatkey.Assuch,wewouldprobablygetmultipleoccurrencesofthePAUSEkeyinaveryshortamountoftime.Thiswouldcausethegametofranticallystop/start.Inordertopreventthisfromhappening,wethrottlethefilteredstreamandignoreallPAUSEeventsthathappeninawindowshorterthan100milliseconds.
Tomakesurewedidn’tmissanything,thisiswhatoursrc/cljs/reagi_game/core.cljsfileshouldlooklike,infull:
(nsreagi-game.core
(:require[monet.canvas:ascanvas]
[reagi.core:asr]
[clojure.set:asset]
[reagi-game.entities:asentities
:refer[move-forward!move-backward!rotate-left!rotate-
right!fire!]]))
(defcanvas-dom(.getElementByIdjs/document"canvas"))
(defmonet-canvas(canvas/initcanvas-dom"2d"))
(defship(entities/shape-data(/(.-width(:canvasmonet-canvas))2)
(/(.-height(:canvasmonet-canvas))2)
0))
(defship-entity(entities/ship-entityship))
(canvas/add-entitymonet-canvas:ship-entityship-entity)
(canvas/draw-loopmonet-canvas)
(defUP38)
(defRIGHT39)
(defDOWN40)
(defLEFT37)
(defFIRE32);;space
(defPAUSE80);;lower-caseP
(defnkeydown-stream[]
(let[out(r/events)]
(set!(.-onkeydownjs/document)#(r/deliverout[::down(.-keyCode
%)]))
out))
(defnkeyup-stream[]
(let[out(r/events)]
(set!(.-onkeyupjs/document)#(r/deliverout[::up(.-keyCode%)]))
out))
(defactive-keys-stream
(->>(r/merge(keydown-stream)(keyup-stream))
(r/reduce(fn[acc[event-typekey-code]]
(condp=event-type
::down(conjacckey-code)
::up(disjacckey-code)
acc))
#{})
(r/sample25)))
(defnfilter-map[predf&args]
(->>active-keys-stream
(r/filter(partialsomepred))
(r/map(fn[_](applyfargs)))))
(filter-map#{FIRE}fire!monet-canvasship)
(filter-map#{UP}move-forward!ship)
(filter-map#{DOWN}move-backward!ship)
(filter-map#{RIGHT}rotate-right!ship)
(filter-map#{LEFT}rotate-left!ship)
(defnpause![_]
(if@(:updating?monet-canvas)
(canvas/stop-updatingmonet-canvas)
(canvas/start-updatingmonet-canvas)))
(->>active-keys-stream
(r/filter(partialsome#{PAUSE}))
(r/throttle100)
(r/mappause!))
Thiscompletesthecodeandwe’renowreadytohavealookattheresults.
Ifyoustillhavetheserverrunningfromearlierinthischapter,simplyexittheREPL,startitagain,andstarttheembeddedwebserver:
leinrepl
CompilingClojureScript.
Compiling"dev-resources/public/js/reagi_game.js"from("src/cljs"
"test/cljs""dev-resources/tools/repl")...
user=>(run)
2014-06-1419:21:40.381:INFO:oejs.Server:jetty-7.6.8.v20121106
2014-06-1419:21:40.403:INFO:oejs.AbstractConnector:Started
[email protected]:3000
#<Serverorg.eclipse.jetty.server.Server@51f6292b>
ThiswillcompilethelatestversionofourClojureScriptsourcetoJavaScript.
Alternatively,youcanleavetheREPLrunningandsimplyaskcljsbuildtoauto-compilethesourcecodefromanotherterminalwindow:
leincljsbuildauto
Compiling"dev-resources/public/js/reagi_game.js"from("src/cljs"
"test/cljs""dev-resources/tools/repl")...
Successfullycompiled"dev-resources/public/js/reagi_game.js"in13.23869
seconds.
Nowyoucanpointyourbrowsertohttp://localhost:3000/andflyaroundyourspaceship!Don’tforgettoshootsomebulletsaswell!
ReagiandotherCESframeworksBackinChapter4,Introductiontocore.async,wehadanoverviewofthemaindifferencesbetweencore.asyncandCES.Anotherquestionthatmighthaveariseninthischapteristhis:howdowedecidewhichCESframeworktouse?
Theanswerislessclearthanbeforeandoftendependsonthespecificsofthetoolbeinglookedat.Wehavelearnedabouttwosuchtoolssofar:ReactiveExtensions(encompassingRxJS,RxJava,andRxClojure)andReagi.
ReactiveExtensions(Rx)isamuchmorematureframework.Itsfirstversionforthe.NETplatformwasreleasedin2011andtheideasinithavesinceevolvedsubstantially.
Additionally,portsforotherplatformssuchasRxJavaarebeingheavilyusedinproductionbybignamessuchasNetflix.
AdrawbackofRxisthatifyouwouldliketouseitbothinthebrowserandontheserver,youhavetousetwoseparateframeworks,RxJSandRxJava,respectively.WhiletheydosharethesameAPI,theyaredifferentcodebases,whichcanincurbugsthatmighthavebeensolvedinoneportbutnotyetinanother.
ForClojuredevelopers,italsomeansrelyingmoreoninteroperabilitytointeractwiththefullAPIofRx.
Reagi,ontheotherhand,isanewplayerinthisspacebutbuildsonthesolidfoundationlaidoutbycore.async.ItisfullydevelopedinClojureandsolvesthein-browser/on-serverissuebycompilingtobothClojureandClojureScript.
Reagialsoallowsseamlessintegrationwithcore.asyncviafunctionssuchasportandsubscribe,whichallowchannelstobecreatedfromeventstreams.
Moreover,theuseofcore.asyncinClojureScriptapplicationsisbecomingubiquitous,sochancesareyoualreadyhaveitasadependency.ThismakesReagianattractiveoptionforthetimeswhenweneedahigherlevelofabstractionthantheoneprovidedbycore.async.
SummaryInthischapter,welearnedhowwecanusethetechniquesfromreactiveprogrammingwehavelearnedsofarinordertowritecodethatiscleanerandeasiertomaintain.Todoso,weinsistedonthinkingaboutasynchronouseventssimplyaslistsandsawhowthatwayofthinkinglendsitselfquiteeasilytobeingmodeledasaneventstream.Allourgamehastodo,then,isoperateonthesestreamsusingfamiliarsequenceprocessingfunctions.
WealsolearnedthebasicsofReagi,aframeworkforCESsimilartotheonewecreatedinChapter4,Introductiontocore.async,butthatismorefeaturerichandrobust.
Inthenextchapter,wewilltakeabreakfromCESandseehowamoretraditionalreactiveapproachbasedondataflowscanbeuseful.
Chapter7.TheUIasaFunctionSofarwehavetakenajourneythroughmanagingcomplexitybyefficientlyhandlingandmodelingasynchronousworkflowsintermsofstreamsofdata.Inparticular,Chapter4,Introductiontocore.asyncandChapter5,CreatingYourOwnCESFrameworkwithcore.asyncexploredwhat’sinvolvedinlibrariesthatprovideprimitivesandcombinatorsforCompositionalEventSystems.WealsobuiltasimpleClojureScriptapplicationthatmadeuseofourframework.
Onethingyoumighthavenoticedisthatnoneoftheexamplessofarhavedealtwithwhathappenstothedataoncewearereadytopresentittoourusers.It’sstillanopenquestionthatwe,asapplicationdevelopers,needtoanswer.
Inthischapter,wewilllookatonewaytohandleReactiveUserInterfacesinwebapplicationsusingReact(seehttp://facebook.github.io/react/),amodernJavaScriptframeworkdevelopedbyFacebook,aswellas:
LearnhowReactrendersuserinterfacesefficientlyBeintroducedtoOm,aClojureScriptinterfacetoReactLearnhowOmleveragespersistentdatastructuresforperformanceDeveloptwofullyworkingClojureScriptapplicationswithOm,includingtheuseofcore.asyncforintercomponentcommunication
TheproblemwithcomplexwebUIsWiththeriseofsingle-pagewebapplications,itbecameamusttobeabletomanagethegrowthandcomplexityofaJavaScriptcodebase.ThesameappliestoClojureScript.
Inanefforttomanagethiscomplexity,aplethoraofJavaScriptMVCframeworkshaveemergedsuchasAngularJS,Backbone.js,Ember.js,andKnockoutJStonameafew.
Theyareverydifferent,butshareafewcommonfeatures:
Givesingle-pageapplicationsmorestructurebyprovidingmodels,views,controllers,templates,andsoonProvideclient-sideroutingTwo-waydatabinding
Inthischapter,we’llbefocusingonthelastgoal.
Two-waydatabindingisabsolutelycrucialifwearetodevelopevenamoderatelycomplexsingle-pagewebapplication.Here’showitworks.
Supposewe’redevelopingaphonebookapplication.Morethanlikely,wewillhaveamodel—orentity,map,whathaveyou—thatrepresentsacontact.Thecontactmodelmighthaveattributessuchasname,phonenumber,ande-mailaddress.
Ofcourse,thisapplicationwouldnotbeallthatusefulifuserscouldn’tupdatecontactinformation,sowewillneedaformwhichdisplaysthecurrentdetailsforacontactandletsyouupdatethecontact’sinformation.
ThecontactmodelmighthavebeenloadedviaanAJAXrequestandthenmighthaveusedexplicitDOMmanipulationcodetodisplaytheform.Thiswouldlooksomethinglikethefollowingpseudo-code:
functioneditContact(contactId){
contactService.get(contactId,function(data){
contactForm.setName(data.name);
contactForm.setPhone(data.phone);
contactForm.setEmail(data.email);
})
}
Butwhathappenswhentheuserupdatessomeone’sinformation?Weneedtostoreitsomehow.Onclickingonsave,afunctionsuchasthefollowingwoulddothetrick,assumingyou’reusingjQuery:
$("save-button").click(function(){
contactService.update(contactForm.serialize(),function(){
flashMessage.set("ContactUpdated.")
})
Thisseeminglyharmlesscodeposesabigproblem.Thecontactmodelforthisparticularpersonisnowoutofdate.Ifwewerestilldevelopingwebapplicationstheoldway,wherewereloadthepageateveryupdate,thiswouldn’tbeaproblem.However,thewholepointofsingle-pagewebapplicationsistoberesponsive,soitkeepsalotofstateontheclient,
anditisimportanttokeepourmodelssyncedwithourviews.
Thisiswheretwo-waydatabindingcomesin.AnexamplefromAngularJSwouldlooklikethefollowing:
//JS
//intheController
$scope.contact={
name:'LeonardoBorges',
phone'+61xxxxxxxxx',
email:'[email protected]'
}
<!--HTML-->
<!--intheView-->
<form>
<inputtype="text"name="contactName"ng-model="contact.name"/>
<inputtype="text"name="contactPhone"ng-model="contact.phone"/>
<inputtype="text"name="contactEmail"ng-model="contact.email"/>
</form>
Angularisn’tthetargetofthischapter,soIwon’tdigintothedetails.Allweneedtoknowfromthisexampleisthat$scopeishowwetellAngulartomakeourcontactmodelavailabletoourviews.Intheview,thecustomattributeng-modeltellsAngulartolookupthatpropertyinthescope.Thisestablishesatwo-wayrelationshipinsuchawaythatwhenyourmodeldatachangesinthescope,AngularrefreshestheUI.Similarly,iftheusereditstheform,Angularupdatesthemodel,keepingeverythinginsync.
Thereare,however,twomainproblemswiththisapproach:
Itcanbeslow.ThewayAngularandfriendsimplementtwo-waydatabindingis,roughlyspeaking,byattachingeventhandlersandwatcherstoviewbothcustom-attributesandmodelattributes.Forcomplexenoughuserinterfaces,youwillstartnoticingthattheUIbecomesslowertorender,diminishingtheuserexperience.Itreliesheavilyonmutation.Asfunctionalprogrammers,westrivetolimitsideeffectstoaminimum.
Theslownessthatcomeswiththisandsimilarapproachesistwo-fold:firstly,AngularJSandfriendshaveto“watch”allpropertiesofeverymodelinthescopeinordertotrackupdates.Oncetheframeworkdeterminesthatdatahaschangedinthemodel,itthenlooksuppartsoftheUI,whichdependonthatinformation—suchasthefragmentsusingng-modelabove—andthenitre-rendersthem.
Secondly,theDOMistheslowestpartofmostsingle-pagewebapplications.Ifwethinkaboutitforamoment,theseframeworksaretriggeringdozensorperhapshundredsofDOMeventhandlersinordertokeepthedatainsync,eachofwhichendsupupdatinganode—orseveral—intheDOM.
Don’ttakemywordforitthough.IranasimplebenchmarktocompareapurecalculationversuslocatingaDOMelementandupdatingitsvaluetotheresultofthesaidcalculation.Herearetheresults—I’veusedJSPerftorunthebenchmark,andtheseresultsarefor
Chrome37.0.2062.94onMacOSXMavericks(seehttp://jsperf.com/purefunctions-vs-dom):
document.getElementsByName("sum")[0].value=1+2
//Operationspersecond:2,090,202
1+2
//Operationspersecond:780,538,120
UpdatingtheDOMisordersofmagnitudeslowerthanperformingasimplecalculation.Itseemslogicalthatwewouldwanttodothisinthemostefficientmannerpossible.
However,ifwedon’tkeepourdatainsync,we’rebackatsquareone.Thereshouldbeawaybywhichwecandrasticallyreducetheamountofrenderingbeingdone,whileretainingtheconvenienceoftwo-waydatabinding.Canwehaveourcakeandeatittoo?
EnterReact.jsAswe’llseeinthischapter,theanswertothequestionposedintheprevioussectionisaresoundingyesand,asyoumighthaveguessed,itinvolvesReact.js.
Butwhatmakesitspecial?
It’swisetostartwithwhatReactisnot.ItisnotanMVCframeworkandassuchitisnotareplacementforthelikesofAngularJS,Backbone.js,andsoon.ReactfocusessolelyontheVinMVC,andpresentsarefreshinglydifferentwaytothinkaboutuserinterfaces.Wemusttakeaslightdetourinordertoexplorehowitdoesthat.
LessonsfromfunctionalprogrammingAsfunctionalprogrammers,wedon’tneedtobeconvincedofthebenefitsofimmutability.Weboughtintothepremiselongago.However,shouldwenotbeabletouseimmutabilityefficiently,itwouldnothavebecomecommonplaceinfunctionalprogramminglanguages.
WeoweittothehugeamountofresearchthatwentintoPurelyFunctionalDataStructures—firstbyOkasakiinhisbookofthesametitle(seehttp://www.amazon.com/Purely-Functional-Structures-Chris-Okasaki/dp/0521663504/ref=sr_1_1?ie=UTF8&qid=1409550695&sr=8-1&keywords=purely+functional+data+structures)andthenimprovedbyothers.
Withoutit,ourprogramswouldbeballooning,bothinspaceandruntimecomplexity.
Thegeneralideaisthatgivenadatastructure,theonlywaytoupdateitisbycreatingacopyofitwiththedesireddeltaapplied:
(conj[123]4);;[1234]
Intheprecedingimage,wehaveasimplisticviewofhowconjoperates.Ontheleft,youhavetheunderlyingdatastructurerepresentingthevectorwewishtoupdate.Ontheright,wehavethenewlycreatedvector,which,aswecansee,sharessomestructurewiththepreviousvector,aswellascontainingournewitem.
TipInreality,theunderlyingdatastructureisatreeandtherepresentationwassimplifiedforthepurposesofthisbook.IhighlyrecommendreferringtoOkasaki’sbookshouldthereaderwantmoredetailsonhowpurelyfunctionaldatastructureswork.
Additionally,thesefunctionsareconsideredpure.Thatis,itrelateseveryinputtoasingleoutputanddoesnothingelse.Thisis,infact,remarkablysimilartohowReacthandlesuserinterfaces.
IfwethinkofaUIasavisualrepresentationofadatastructure,whichreflectsthecurrentstateofourapplication,wecan,withouttoomucheffort,thinkofUIupdatesasasimple
functionwhoseinputistheapplicationstateandtheoutputisaDOMrepresentation.
You’llhavenoticedIdidn’tsaytheoutputisrenderingtotheDOM—thatwouldmakethefunctionimpureasrenderingisclearlyasideeffect.Itwouldalsomakeitjustasslowasthealternatives.
ThisDOMrepresentationisessentiallyatreeofDOMnodesthatmodelhowyourUIshouldlook,andnothingelse.
ReactcallsthisrepresentationaVirtualDOM,androughlyspeaking,insteadofwatchingindividualbitsandpiecesofapplicationstatethattriggeraDOMre-renderuponchange,ReactturnsyourUIintoafunctiontowhichyougivethewholeapplicationstate.
Whenyougivethisfunctionthenewupdatedstate,ReactrendersthatstatetotheVirtualDOM.RemembertheVirtualDOMissimplyadatastructure,sotherenderingisextremelyfast.Onceit’sdone,Reactdoesoneoftwothings:
ItcommitstheVirtualDOMtotheactualDOMifthisisthefirstrender.Otherwise,itcomparesthenewVirtualDOMwiththecurrentVirtualDOM,cachedfromthepreviousrenderoftheapplication.ItthenusesanefficientdiffalgorithmtocomputetheminimumsetofchangesrequiredtoupdatetherealDOM.Finally,itcommitsthisdeltatotheDOM.
WithoutdiggingintothenutsandboltsofReact,thisisessentiallyhowitisimplementedandthereasonitisfasterthanthealternatives.Conceptually,Reacthitsthe“refresh”buttonwheneveryourapplicationstatechanges.
AnothergreatbenefitisthatbythinkingofyourUIasafunctionfromapplicationstatetoaVirtualDOM,werecoversomeofthereasoningwe’reabletodowhenworkingwithimmutabledatastructuresinfunctionallanguages.
Intheupcomingsections,wewillunderstandwhythisisabigwinforusClojuredevelopers.
ClojureScriptandOmWhyhaveIspentsixpagestalkingaboutJavaScriptandReactinaClojurebook?IpromiseI’mnottryingtowasteyourprecioustime;wesimplyneededsomecontexttounderstandwhat’stocome.
OmisaClojureScriptinterfacetoReact.jsdevelopedbytheprolificandamazingindividualDavidNolen,fromCognitect.Yes,hehasalsodevelopedcore.logic,core.match,andtheClojureScriptcompiler.That’showprolific.ButIdigress.
WhenFacebookreleasedReact,Davidimmediatelysawthepotentialand,moreimportantly,howtotakeadvantageoftheassumptionsweareabletomakewhenprogramminginClojure,themostimportantofwhichisthatdatastructuresdon’tchange.
Reactprovidesseveralcomponentlife-cyclefunctionsthatallowdeveloperstocontrolvariouspropertiesandbehaviors.Oneinparticular,shouldComponentUpdate,isusedtodecidewhetheracomponentneedstobere-rendered.
Reacthasabigchallengehere.JavaScriptisinherentlymutable,soitisextremelyhard,whencomparingVirtualDOMTrees,toidentifywhichnodeshavechangedinanefficientway.ReactemploysafewheuristicsinordertoavoidO(n3)worst-caseperformanceandisabletodoitinO(n)mostofthetime.Sinceheuristicsaren’tperfect,wecanchoosetoprovideourownimplementationofshouldComponentUpdateandtakeadvantageoftheknowledgewepossesswhenrenderingacomponent.
ClojureScript,ontheotherhand,usesimmutabledatastructures.Assuch,OmprovidesthesimplestandmostefficientimplementationpossibleforshouldComponentUpdate:asimplereferenceequalitycheck.
Becausewe’realwaysdealingwithimmutabledatastructures,inordertoknowwhethertwotreesarethesame,allweneedtodoiscomparewhethertheirrootsarethesame.Iftheyare,we’redone.Otherwise,descendandrepeattheprocess.ThisisguaranteedtoyieldO(logn)runtimecomplexityandallowsOmtoalwaysrendertheUIfromtherootefficiently.
Ofcourse,performanceisn’ttheonlythingthat’sgoodaboutOm—wewillnowexplorewhatmakesanOmapplication.
BuildingasimpleContactsapplicationwithOmThischapterhasbeenverytextheavysofar.It’stimewegetourhandsdirtyandbuildasimpleOmapplication.Sincewetalkedaboutcontactsbefore,that’swhatwewillstartwith.
ThemaindriverbehindReactandOmistheabilitytobuildhighlyreusable,self-containedcomponentsand,assuch,eveninasimpleContactsapplication,wewillhavemultiplecomponentsworkinginconcerttoachieveacommongoal.
Thisiswhatourusersshouldbeabletodointheapplication:
DisplayalistofcontactscurrentlyinstorageDisplaythedetailsofagivencontactEditthedetailsofaspecificcontact
Andoncewe’redone,itwilllooklikethefollowing:
TheContactsapplicationstateAsmentionedpreviously,Om/ReactwilleventuallyrendertheDOMbasedonourapplicationstate.We’llbeusingdatathat’sinmemorytokeeptheexamplesimple.Here’swhatourapplicationstatewilllooklike:
(defapp-state
(atom{:contacts{1{:id1
:name"JamesHetfield"
:email"[email protected]"
:phone"+1XXXXXXXXX"}
2{:id2
:name"AdamDarski"
:email"[email protected]"
:phone"+48XXXXXXXXX"}}
:selected-contact-id[]
:editing[false]}))
ThereasonwekeepthestateinanatomisthatOmusesthattore-rendertheapplicationifweswap!orreset!it,forinstance,ifweloadsomedatafromtheserveraftertheapplicationhasbeenrenderedforthefirsttime.
Thedatainthestateitselfshouldbemostlyself-explanatory.Wehaveamapcontainingallcontacts,akeyrepresentingwhetherthereiscurrentlyacontactselected,andaflagthatindicateswhetherwearecurrentlyeditingtheselectedcontact.Whatmightlookoddisthatboth:selected-contact-idand:editingkeyspointtoavector.Justbearwithmeforamoment;thereasonforthiswillbecomeclearshortly.
Nowthatwehaveadraftofourapplicationstate,it’stimewethinkabouthowthestatewillflowthroughthedifferentcomponentsinourapp.Apictureisworthathousandwords,sothefollowingdiagramshowsthehigh-levelarchitecturethroughwhichourdatawillflow:
Intheprecedingimage,eachfunctioncorrespondstoanOmcomponent.Attheveryleast,theytakesomepieceofdataastheirinitialstate.Whatisinterestinginthisimageisthataswedescendintoourmorespecializedcomponents,theyrequestlessstatethanthemaincomponent,contacts-app.Forinstance,thecontacts-viewcomponentneedsallcontactsaswellastheIDoftheselectedcontact.Thedetails-panel-viewcomponent,ontheotherhand,onlyneedsthecurrentlyselectedcontact,andwhetherit’sbeingeditedornot.ThisisacommonpatterninOmandweusuallywanttoavoidover-sharingtheapplicationstate.
Witharoughunderstandingofourhigh-levelarchitecture,wearereadytostartbuildingourContactsapplication.
SettinguptheContactsprojectOnceagain,wewillusealeiningentemplatetohelpusgetstarted.Thistimewe’llbeusingom-start(seehttps://github.com/magomimmo/om-start-template),alsobyMimmoCosenza(seehttps://github.com/magomimmo).Typethisintheterminaltocreateabaseprojectusingthistemplate:
leinnewom-startcontacts
cdcontacts
Next,let’sopentheproject.cljfileandmakesurewehavethesameversionsforthevariousdifferentdependenciesthetemplatepullsin.Thisisjustsothatwedon’thaveanysurpriseswithincompatibleversions:
...
:dependencies[[org.clojure/clojure"1.6.0"]
[org.clojure/clojurescript"0.0-2277"]
[org.clojure/core.async"0.1.338.0-5c5012-alpha"]
[om"0.7.1"]
[com.facebook/react"0.11.1"]]
...
Tovalidatethenewprojectskeleton,stillintheterminal,typethefollowingtoauto-compileyourClojureScriptsourcefiles:
leincljsbuildauto
CompilingClojureScript.
Compiling"dev-resources/public/js/contacts.js"from("src/cljs""dev-
resources/tools/repl")...
Successfullycompiled"dev-resources/public/js/contacts.js"in9.563
seconds.
Now,weshouldseethetemplatedefault“HelloWorld”pageifweopenthedev-resources/public/index.htmlfileinthebrowser.
ApplicationcomponentsThenextthingwe’lldoisopenthesrc/cljs/contacts/core.cljsfile,whichiswhereourapplicationcodewillgo,andmakesureitlookslikethefollowingsothatwehaveacleanslatewiththeappropriatenamespacedeclaration:
(nscontacts.core
(:require[om.core:asom:include-macrostrue]
[om.dom:asdom:include-macrostrue]))
(enable-console-print!)
(defapp-state
(atom{:contacts{1{:id1
:name"JamesHetfield"
:email"[email protected]"
:phone"+1XXXXXXXXX"}
2{:id2
:name"AdamDarski"
:email"[email protected]"
:phone"+48XXXXXXXXX"}}
:selected-contact-id[]
:editing[false]}))
(om/root
contacts-app
app-state
{:target(.js/document(getElementById"app"))})
EveryOmapplicationstartswitharootcomponentcreatedbytheom/rootfunction.Ittakesasargumentsafunctionrepresentingacomponent—contacts-app—theinitialstateoftheapplication—app-state—andamapofoptionsofwhichtheonlyonewecareaboutis:target,whichtellsOmwheretomountourrootcomponentontheDOM.
Inthisinstance,itwillmountonaDOMelementwhoseIDisapp.Thiselementwasgiventousbytheom-starttemplateandislocatedinthedev-resources/public/index.htmlfile.
Ofcourse,thiscodewon’tcompileyet,aswedon’thavethecontacts-apptemplate.Let’ssolvethatandcreateitabovetheprecedingdeclaration—we’reimplementingthecomponentsbottom-up:
(defncontacts-app[dataowner]
(reify
om/IRender
(render[this]
(let[[selected-id:asselected-id-cursor]
(:selected-contact-iddata)]
(dom/divnil
(om/buildcontacts-view
{:contacts(:contactsdata)
:selected-id-cursorselected-id-cursor})
(om/builddetails-panel-view
{:contact(get-indata[:contacts
selected-id])
:editing-cursor(:editingdata)}))))))
Thissnippetintroducesanumberofnewfeaturesandterminology,soitdeservesafewparagraphs.
Whendescribingom/root,wesawthatitsfirstargumentmustbeanOmcomponent.Thecontact-appfunctioncreatesonebyreifyingtheom/IRenderprotocol.Thisprotocolcontainsasinglefunction—render—whichgetscalledwhentheapplicationstatechanges.
TipClojureusesreifytoimplementprotocolsorJavainterfacesonthefly,withouttheneedtocreateanewtype.YoucanreadmoreaboutthisonthedatatypespageoftheClojuredocumentationathttp://clojure.org/datatypes.
TherenderfunctionmustreturnanOm/ReactcomponentorsomethingReactknowshowtorender—suchasaDOMrepresentationofthecomponent.Theargumentstocontacts-apparestraightforward:dataisthecomponentstateandowneristhebackingReactcomponent.
Movingdownthesourcefile,intheimplementationofrender,wehavethefollowing:
(let[[selected-id:asselected-id-cursor]
(:selected-contact-iddata)]
...)
Ifwerecallfromourapplicationstate,thevalueof:selected-contact-idis,atthisstage,anemptyvector.Here,then,wearedestructuringthisvectorandgivingitaname.Whatyoumightbewonderingnowiswhyweboundthevectortoavariablenamedselected-id-cursor.Thisistoreflectthefactthatatthispointinthelifecycleofacomponent,selected-id-cursorisn’tavectoranylongerbutratheritisacursor.
OmcursorsOnceom/rootcreatesourrootcomponent,sub-componentsdon’thavedirectaccesstothestateatomanylonger.Instead,componentsreceiveacursorcreatedfromtheapplicationstate.
Cursorsaredatastructuresthatrepresentaplaceintheoriginalstateatom.Youcanusecursorstoread,delete,update,orcreateavaluewithnoknowledgeoftheoriginaldatastructure.Let’staketheselected-id-cursorcursorasanexample:
Atthetop,wehaveouroriginalapplicationstate,whichOmturnsintoacursor.Whenwerequestthe:selected-contact-idkeyfromit,Omgivesusanothercursorrepresentingthatparticularplaceinthedatastructure.Itjustsohappensthatitsvalueistheemptyvector.
WhatisinterestingaboutthiscursoristhatifweupdateitsvalueusingoneofOm’sstatetransitionfunctionssuchasom/transact!andom/update!—wewillexplaintheseshortly—itknowshowtopropagatethechangeupthetreeandallthewaybacktotheapplicationstateatom.
Thisisimportantbecauseaswehavebrieflystatedbefore,itiscommonpracticetohaveourmorespecializedcomponentsdependonspecificpartsoftheapplicationstaterequiredforitscorrectoperation.
Byusingcursors,wecaneasilypropagatechangeswithoutknowingwhattheapplicationstatelookslike,thusavoidingtheneedtoaccesstheglobalstate.
TipYoucanthinkofcursorsaszippers.Conceptually,theyserveasimilarpurposebuthavedifferentAPIs.
FillingintheblanksMovingdownthecontacts-appcomponent,wenowhavethefollowing:
(dom/divnil
(om/buildcontacts-view
{:contacts(:contactsdata)
:selected-id-cursorselected-id-cursor})
(om/builddetails-panel-view
{:contact(get-indata[:contacts
selected-id])
:editing-cursor(:editingdata)}))
ThedomnamespacecontainsthinwrappersaroundReact’sDOMclasses.It’sessentiallythedatastructurerepresentingwhattheapplicationwilllooklike.Next,weseetwoexamplesofhowwecancreateOmcomponentsinsideanotherOmcomponent.Weusetheom/buildfunctionforthatandcreatethecontacts-viewanddetails-panel-viewcomponents.Theom/buildfunctiontakesasargumentsthecomponentfunction,thecomponentstate,and,optionally,amapofoptionswhicharen’timportantforthisexample.
Atthispoint,wehavealreadystartedtolimitthestatewewillpassintothesub-componentsbycreatingsub-cursors.
Accordingtothesourcecode,thenextcomponentweshouldlookatiscontacts-view.Hereitisinfull:
(defncontacts-view[{:keys[contactsselected-id-cursor]}owner]
(reify
om/IRender
(render[_]
(dom/div#js{:style#js{:float"left"
:width"50%"}}
(applydom/ulnil
(om/build-allcontact-summary-view(valscontacts)
{:shared{:selected-id-cursorselected-
id-cursor}}))))))
Hopefully,thesourceofthiscomponentlooksalittlemorefamiliarnow.Asbefore,wereifyom/IRendertoprovideaDOMrepresentationofourcomponent.Itcomprisesasingledivelement.Thistimewegiveasthesecondargumenttodom/divahash-maprepresentingHTMLattributes.Weareusingsomeinlinestyles,butideallywewoulduseanexternalstylesheet.
TipIfyouarenotfamiliarwiththe#js{…}syntax,it’ssimplyareadermacrothatexpandsto(clj->js{…})inordertoconvertaClojureScripthash-mapintoaJavaScriptobject.Theonlythingtowatchforisthatitisnotrecursive,asevidencedbythenesteduseof#js.
Thethirdargumenttodom/divisslightlymorecomplexthanwhatwehaveseensofar:
(applydom/ulnil
(om/build-allcontact-summary-view(valscontacts)
{:shared{:selected-id-cursorselected-
id-cursor}}))
Eachcontactwillberepresentedbyali(listitem)HTMLnode,sowestartbywrappingtheresultintoadom/ulelement.Then,weuseom/build-alltobuildalistofcontact-summary-viewcomponents.Omwill,inturn,callom/buildforeachcontactinvalscontacts.
Lastly,weusethethirdargumenttoom/build-all—theoptionsmap—todemonstratehowwecansharestatebetweencomponentswithouttheuseofglobalstate.We’llseehowthat’susedinthenextcomponent,contact-summary-view:
(defncontact-summary-view[{:keys[namephone]:ascontact}owner]
(reify
om/IRender
(render[_]
(dom/li#js{:onClick#(select-contact!@contact
(om/get-sharedowner
:selected-id-cursor))}
(dom/spannilname)
(dom/spannilphone)))))
Ifwethinkofourapplicationasatreeofcomponents,wehavenowreachedoneofitsleaves.Thiscomponentsimplyreturnsadom/linodewiththecontact’snameandphoneinit,wrappedindom/spannodes.
Italsoinstallsahandlertothedom/lionClickevent,whichwecanusetoupdatethestatecursor.
Weuseom/get-sharedtoaccessthesharedstateweinstalledearlierandpasstheresultingcursorintoselect-contact!Wealsopassthecurrentcontact,but,ifyoulookclosely,wehavetoderefitfirst:
@contact
ThereasonforthisisthatOmdoesn’tallowustomanipulatecursorsoutsideoftherenderphase.Byderefingthecursor,wehaveitsmostrecentunderlyingvalue.Nowselect-contact!hasallitneedstoperformtheupdate:
(defnselect-contact![contactselected-id-cursor]
(om/update!selected-id-cursor0(:idcontact)))
Wesimplyuseom/update!tosetthevalueoftheselected-id-cursorcursoratindex0totheidofthecontact.Asmentionedpreviously,thecursortakescareofpropagatingthechange.
TipYoucanthinkofom/update!asthecursorsversionofclojure.core/reset!usedinatoms.Conversely,thesameappliestoom/transact!andclojure.core/swap!,respectively.
Wearemovingatagoodpace.It’stimewelookatthenextcomponent,details-panel-
view:
(defndetails-panel-view[dataowner]
(reify
om/IRender
(render[_]
(dom/div#js{:style#js{:float"right"
:width"50%"}}
(om/buildcontact-details-viewdata)
(om/buildcontact-details-form-viewdata)))))
Thiscomponentshouldnowlookfairlyfamiliar.Allitdoesisbuildtwoothercomponents,contact-details-viewandcontact-details-form-view:
(defncontact-details-view[{{:keys[namephoneemailid]:ascontact}
:contact
editing:editing-cursor}
owner]
(reify
om/IRender
(render[_]
(dom/div#js{:style#js{:display(if(getediting0)"none""")}}
(dom/h2nil"Contactdetails")
(ifcontact
(dom/divnil
(dom/h3#js{:style#js{:margin-bottom"0px"}}
(:namecontact))
(dom/spannil(:phonecontact))(dom/brnil)
(dom/spannil(:emailcontact))(dom/brnil)
(dom/button#js{:onClick#(om/update!editing0
true)}
"Edit"))
(dom/spannil"Nocontactselected"))))))
Thecontact-details-viewcomponentreceivestwopiecesofstate:thecontactandtheeditingflag.Ifwehaveacontact,wesimplyrenderthecomponent.However,weusetheeditingflagtohideit,ifweareeditingit.Thisissothatwecanshowtheeditforminthenextcomponent.WealsoinstallanonClickhandlertotheEditbuttonsothatwecanupdatetheeditingcursor.
Thecontact-details-form-viewcomponentreceivesthesameargumentsbutrendersthefollowingforminstead:
(defncontact-details-form-view[{{:keys[namephoneemailid]:ascontact}
:contact
editing:editing-cursor}
owner]
(reify
om/IRender
(render[_]
(dom/div#js{:style#js{:display(if(getediting0)"""none")}}
(dom/h2nil"Contactdetails")
(ifcontact
(dom/divnil
(dom/input#js{:type"text"
:valuename
:onChange#(update-
contact!%contact:name)})
(dom/input#js{:type"text"
:valuephone
:onChange#(update-
contact!%contact:phone)})
(dom/input#js{:type"text"
:valueemail
:onChange#(update-
contact!%contact:email)})
(dom/button#js{:onClick#(om/update!editing0
false)}
"Save"))
(dom/divnil"Nocontactselected"))))))
Thisisthecomponentresponsibleforactuallyupdatingthecontactinformationbasedontheform.Itdoessobycallingupdate-contact!withtheJavaScriptevent,thecontactcursor,andthekeyrepresentingtheattributetobeupdated:
(defnupdate-contact![econtactkey]
(om/update!contactkey(..e-target-value)))
Asbefore,wesimplyuseom/update!insteadofom/transact!aswearesimplyreplacingthevalueofthecursorattributewiththecurrentvalueoftheformfieldwhichtriggeredtheevente.
NoteIfyou’renotfamiliarwiththe..syntax,it’ssimplyaconveniencemacroforJavaandJavaScriptinteroperability.Thepreviousexampleexpandsto:
(.(.e-target)-value)
ThisandotherinteroperabilityoperatorsaredescribedintheJavaInteroppageoftheClojurewebsite(seehttp://clojure.org/java_interop).
Thisisit.Makesureyourcodeisstillcompiling—orifyouhaven’tyet,starttheauto-compilationbytypingthefollowingintheterminal:
leincljsbuildauto
Then,openupdev-resources/public/index.htmlagaininyourbrowserandtakeourContactsappforaspin!Noteinparticularhowtheapplicationstateisalwaysinsyncwhileyoueditthecontactattributes.
Ifthereareanyissuesatthisstage,makesurethesrc/cljs/contacts/core.cljsfilematchesthecompanioncodeforthisbook.
IntercomponentcommunicationInourpreviousexample,thecomponentswebuiltcommunicatedwitheachotherexclusivelythroughtheapplicationstate,bothforreadingandtransactingdata.Whilethisapproachworks,itisnotalwaysthebestexceptforverysimpleusecases.Inthissection,wewilllearnanalternatewayofperformingthiscommunicationusingcore.asyncchannels.
Theapplicationwewillbuildisasupersimplevirtualagileboard.Ifyou’veheardofit,it’ssimilartoTrello(seehttps://trello.com/).Ifyouhaven’t,fearnot,it’sessentiallyataskmanagementwebapplicationinwhichyouhavecardsthatrepresenttasksandyoumovethembetweencolumnssuchasBacklog,InProgress,andDone.
Bytheendofthissection,theapplicationwilllooklikethefollowing:
We’lllimitourselvestoasinglefeature:movingcardsbetweencolumnsbydragginganddroppingthem.Let’sgetstarted.
CreatinganagileboardwithOmWe’realreadyfamiliarwiththeom-start(seehttps://github.com/magomimmo/om-start-template)leiningentemplate,andsincethereisnoreasontochangeit,that’swhatwewillusetocreateourproject—whichIcalledom-pmforOmProjectManagement:
leinnewom-startom-pm
cdom-pm
Asbefore,weshouldensurewehavetherightdependenciesinourproject.cljfile:
:dependencies[[org.clojure/clojure"1.6.0"]
[org.clojure/clojurescript"0.0-2511"]
[org.om/om"0.8.1"]
[org.clojure/core.async"0.1.346.0-17112a-alpha"]
[com.facebook/react"0.12.2"]]
Nowvalidatethatweareingoodshapebymakingsuretheprojectcompilesproperly:
leincljsbuildauto
CompilingClojureScript.
Compiling"dev-resources/public/js/om_pm.js"from("src/cljs""dev-
resources/tools/repl")...
Successfullycompiled"dev-resources/public/js/om_pm.js"in13.101seconds.
Next,openthesrc/cljs/om_pm/core.cljsfileandaddthenamespacesthatwewillbeusingtobuildtheapplication:
(nsom-pm.core
(:require[om.core:asom:include-macrostrue]
[om.dom:asdom:include-macrostrue]
[cljs.core.async:refer[put!chan<!]]
[om-pm.util:refer[set-transfer-data!get-transfer-data!move-
card!]])
(:require-macros[cljs.core.async.macros:refer[gogo-loop]]))
Themaindifferencethistimeisthatwearerequiringcore.asyncfunctionsandmacros.Wedon’tyethaveanom-pm.utilnamespace,butwe’llgettothatattheend.
TheboardstateIt’stimewethinkwhatourapplicationstatewilllooklike.Ourmainentityinthisapplicationisthecard,whichrepresentsataskandhastheattributesid,title,anddescription.Wewillstartbydefiningacoupleofcards:
(defcards[{:id1
:title"Groceriesshopping"
:description"Almondmilk,mixednuts,eggs…"}
{:id2
:title"Expenses"
:description"Submitlastclient'sexpensereport"}])
Thisisn’tourapplicationstateyet,butratherapartofit.Anotherimportantpieceofstateisawaytotrackwhichcardsareonwhichcolumns.Tokeepthingssimple,wewillworkwithonlythreecolumns:Backlog,InProgress,andDone.Bydefault,allcardsstartoutinthebacklog:
(defapp-state
(atom{:cardscards
:columns[{:title"Backlog"
:cards(mapv:idcards)}
{:title"InProgress"
:cards[]}
{:title"Done"
:cards[]}]}))
Thisisallthestateweneed.Columnshavea:titleanda:cardsattribute,whichcontainstheIDsofallcardsinthatcolumn.
Additionally,wewillhaveahelperfunctiontomakefindingcardsmoreconvenient:
(defncard-by-id[id]
(first(filterv#(=id(:id%))cards)))
TipBewareoflazysequences
YoumighthavenoticedtheuseofmapvinsteadofmapforretrievingthecardsIDs.Thisisasubtlebutimportantdifference:mapislazybydefault,butOmcanonlycreatecursorsformapsandvectors.Usingmapvgivesusavectorback,avoidinglazinessaltogether.
Hadwenotdonethat,OmwouldconsiderthelistofIDsasanormalvalueandwewouldnotbeabletotransactit.
ComponentsoverviewTherearemanywaystosliceupanOmapplicationintocomponents,andinthissection,wewillpresentonewayaswewalkthrougheachcomponent’simplementation.
Theapproachwewillfollowissimilartoourpreviousapplicationinthatfromthispointon,wepresentthecomponentsbottom-up.
Beforeweseeourfirstcomponent,however,weshouldstartwithOm’sownrootcomponent:
(om/rootproject-viewapp-state
{:target(.js/document(getElementById"app"))})
Thisgivesusahintastowhatournextcomponentwillbe,project-view:
(defnproject-view[appowner]
(reify
om/IInitState
(init-state[_]
{:transfer-chan(chan)})
om/IWillMount
(will-mount[_]
(let[transfer-chan(om/get-stateowner:transfer-chan)]
(go-loop[]
(let[transfer-data(<!transfer-chan)]
(om/transact!app:columns
#(move-card!%transfer-data))
(recur)))))
om/IRenderState
(render-state[thisstate]
(dom/divnil
(applydom/ulnil
(om/build-allcolumn-view(:columnsapp)
{:shared{:cards(:cardsapp)}
:init-statestate}))))))
LifecycleandcomponentlocalstateThepreviouscomponentisfairlydifferentfromtheoneswehaveseensofar.Morespecifically,itimplementstwonewprotocols:om/IInitStateandom/IWillMount.Additionally,wedroppedom/IRenderaltogetherinfavorofom/IRenderState.Beforeweexplainwhatthesenewprotocolsaregoodfor,weneedtodiscussourhigh-leveldesign.
Theproject-viewcomponentisourapplication’smainentrypointandreceivesthewholeapplicationstateasitsfirstargument.AsinourearlierContactsapplication,ittheninstantiatestheremainingcomponentswiththedatatheyneed.
DifferentfromtheContactsexample,however,itcreatesacore.asyncchannel—transfer-chan—whichworksasamessagebus.Theideaisthatwhenwedragacardfromonecolumnanddropitonanother,oneofourcomponentswillputatransfereventinthischannelandletsomeoneelse—mostlikelyagoblock—performtheactualmoveoperation.
Thisisdoneinthefollowingsnippettakenfromthecomponentshownearlier:
om/IInitState
(init-state[_]
{:transfer-chan(chan)})
ThiscreateswhatOmcallsthecomponentlocalstate.Itusesadifferentlifecycleprotocol,om/IInitState,whichisguaranteedtobecalledonlyonce.Afterall,weneedasinglechannelforthiscomponent.init-stateshouldreturnamaprepresentingthelocalstate.
Nowthatwehavethechannel,weneedtoinstallago-looptohandlemessagessenttoit.Forthispurpose,weuseadifferentprotocol:
om/IWillMount
(will-mount[_]
(let[transfer-chan(om/get-stateowner:transfer-chan)]
(go-loop[]
(let[transfer-data(<!transfer-chan)]
(om/transact!app:columns#(move-card!%transfer-data))
(recur)))))
Likethepreviousprotocol,om/IWillMountisalsoguaranteedtobecalledonceinthecomponentlifecycle.ItiscalledwhenitisabouttobemountedintotheDOMandistheperfectplacetoinstallthego-loopintoourchannel.
TipWhencreatingcore.asyncchannelsinOmapplications,itisimportanttoavoidcreatingtheminsidelife-cyclefunctionsthatarecalledmultipletimes.Besidesnon-deterministicbehavior,thisisasourceofmemoryleaks.
Wegetholdofitfromthecomponentlocalstateusingtheom/get-statefunction.Oncewegetamessage,wetransactthestate.Wewillseewhattransfer-datalookslikeveryshortly.
Wecompletethecomponentbyimplementingitsrenderfunction:
...
om/IRenderState
(render-state[thisstate]
(dom/divnil
(applydom/ulnil
(om/build-allcolumn-view(:columnsapp)
{:shared{:cards(:cardsapp)}
:init-statestate}))))
...
Theom/IRenderStatefunctionservesthesamepurposeofom/IRender,thatis,itshouldreturntheDOMrepresentationofwhatthecomponentshouldlooklike.However,itdefinesadifferentfunction,render-state,whichreceivesthecomponentlocalstateasitssecondargument.Thisstatecontainsthemapwecreatedduringtheinit-statephase.
RemainingcomponentsNext,wewillbuildmultiplecolumn-viewcomponents,onepercolumn.Eachofthemreceivesthelistofcardsfromtheapplicationstateastheirsharedstate.WewillusethattoretrievethecarddetailsfromtheIDswestoreineachcolumn.
Wealsousethe:init-statekeytoinitializethelocalstateofeachcolumnviewwithourchannel,sinceallcolumnsneedareferencetoit.Here’swhatthecomponentlookslike:
(defncolumn-view[{:keys[titlecards]}owner]
(reify
om/IRenderState
(render-state[this{:keys[transfer-chan]}]
(dom/div#js{:style#js{:border"1pxsolidblack"
:float"left"
:height"100%"
:width"320px"
:padding"10px"}
:onDragOver#(.preventDefault%)
:onDrop#(handle-drop%transfer-chantitle)}
(dom/h2niltitle)
(applydom/ul#js{:style#js{:list-style-type"none"
:padding"0px"}}
(om/build-all(partialcard-viewtitle)
(mapvcard-by-idcards)))))))
Thecodeshouldlookfairlyfamiliaratthispoint.WeusedinlineCSSintheexampletokeepitsimple,butinarealapplication,wewouldprobablyhaveusedanexternalstylesheet.
Weimplementrender-stateoncemoretoretrievethetransferchannel,whichwillbeusedwhenhandlingtheonDropJavaScriptevent.ThiseventisfiredbythebrowserwhenauserdropsadraggableDOMelementontothiscomponent.handle-droptakescareofthatlikeso:
(defnhandle-drop[etransfer-chancolumn-title]
(.preventDefaulte)
(let[data{:card-id
(js/parseInt(get-transfer-data!e"cardId"))
:source-column
(get-transfer-data!e"sourceColumn")
:destination-column
column-title}]
(put!transfer-chandata)))
Thisfunctioncreatesthetransferdata—amapwiththekeys:card-id,:source-column,and:destination-column—whichiseverythingweneedtomovethecardsbetweencolumns.Finally,weput!itintothetransferchannel.
Next,webuildanumberorcard-viewcomponents.Asmentionedpreviously,Omcan’tcreatecursorsfromlazysequences,soweusefiltervtogiveeachcard-viewavectorcontainingtheirrespectivecards.Let’sseeitssource:
(defncard-view[column{:keys[idtitledescription]:ascard}owner]
(reify
om/IRender
(render[this]
(dom/li#js{:style#js{:border"1pxsolidblack"}
:draggabletrue
:onDragStart(fn[e]
(set-transfer-data!e"cardId"id)
(set-transfer-data!e"sourceColumn"
column))}
(dom/spanniltitle)
(dom/pnildescription)))))
Asthiscomponentdoesn’tneedanylocalstate,wegobacktousingtheIRenderprotocol.Additionally,wemakeitdraggableandinstallaneventhandlerontheonDragStartevent,whichwillbetriggeredwhentheuserstartsdraggingthecard.
Thiseventhandlersetsthetransferdata,whichweusefromhandle-drop.
Wehaveglossedoverthefactthatthesecomponentsuseafewutilityfunctions.That’sOK,aswewillnowdefinetheminanewnamespace.
UtilityfunctionsGoaheadandcreateanewfileundersrc/cljs/om_pm/calledutil.cljsandaddthefollowingnamespacedeclaration:
(nsom-pm.util)
Forconsistency,wewilllookatthefunctionsbottom-up,startingwithmove-card!:
(defncolumn-idx[titlecolumns]
(first(keep-indexed(fn[idxcolumn]
(when(=title(:titlecolumn))
idx))
columns)))
(defnmove-card![columns{:keys[card-idsource-columndestination-
column]}]
(let[from(column-idxsource-columncolumns)
to(column-idxdestination-columncolumns)]
(->columns
(update-in[from:cards](fn[cards]
(remove#{card-id}cards)))
(update-in[to:cards](fn[cards]
(conjcardscard-id))))))
Themove-card!functionreceivesacursorforthecolumnsinourapplicationstateandsimplymovescard-idbetweenthesourceanddestination.Youwillnoticewedidn’tneedanyaccesstocore.asyncorOmspecificfunctions,whichmeansthisfunctionispureandthereforeeasytotest.
Next,wehavethefunctionsthathandletransferdata:
(defnset-transfer-data![ekeyvalue]
(.setData(->e.-nativeEvent.-dataTransfer)
keyvalue))
(defnget-transfer-data![ekey]
(->(->e.-nativeEvent.-dataTransfer)
(.getDatakey)))
ThesefunctionsuseJavaScriptinteroperabilitytointeractwithHTML’sDataTransfer(seehttps://developer.mozilla.org/en-US/docs/Web/API/DataTransfer)object.Thisishowbrowserssharedatarelatedtodraganddropevents.
Now,let’ssimplysavethefileandmakesurethecodecompilesproperly.Wecanfinallyopendev-resources/public/index.htmlinthebrowserandplayaroundwiththeproductofourwork!
ExercisesInthisexercise,wewillmodifytheom-pmprojectwecreatedintheprevioussection.Theobjectiveistoaddkeyboardshortcutssothatpoweruserscanoperatetheagileboardmoreefficiently.
Theshortcutstobesupportedare:
Theup,down,left,andrightarrowkeys:Theseallowtheusertonavigatethroughthecards,highlightingthecurrentoneThenandpkeys:Theseareusedtomovethecurrentcardtothenext(right)orprevious(left)column,respectively
Thekeyinsighthereistocreateanewcore.asyncchannel,whichwillcontainkeypressevents.Theseeventswillthentriggertheactionsoutlinedpreviously.WecanusetheGoogleclosurelibrarytolistenforevents.Justaddthefollowingrequiretotheapplicationnamespace:
(:require[goog.events:asevents])
Then,usethisfunctiontocreateachannelfromDOMevents:
(defnlisten[eltype]
(let[c(chan)]
(events/listeneltype#(put!c%))
c))
Theactuallogicofmovingthecardsaroundbasedonkeyboardshortcutscanbeimplementedinanumberofways,sodon’tforgettocompareyoursolutionwiththeanswersprovidedinthisbook’scompanioncode.
SummaryInthischapter,wesawadifferentapproachonhowtohandlereactivewebinterfacesbyOmandReact.Inturn,theseframeworksmakethispossibleandpainlessbyapplyingfunctionalprogrammingprinciplessuchasimmutabilityandpersistentdatastructuresforefficientrendering.
WealsolearnedtothinktheOmwaybystructuringourapplicationsasaseriesoffunctions,whichreceivestateandoutputaDOMrepresentationofstatechanges.
Additionally,wesawthatbystructuringapplicationstatetransitionsthroughcore.asyncchannels,weseparatethepresentationlogicfromthecode,whichwillactuallyperformthework,makingourcomponentseveneasiertoreasonabout.
Inthenextchapter,wewillturntoanoftenoverlookedyetusefultoolforcreatingreactiveapplications:Futures.
Chapter8.FuturesThefirststeptowardsreactiveapplicationsistobreakoutofsynchronousprocessing.Ingeneral,applicationswastealotoftimewaitingforthingstohappen.Maybewearewaitingonanexpensivecomputation—say,calculatingthe1000thFibonaccinumber.Perhapswearewaitingforsomeinformationtobewrittentothedatabase.Wecouldalsobewaitingforanetworkcalltoreturn,bringingusthelatestrecommendationsfromourfavoriteonlinestore.
Regardlessofwhatwe’rewaitingfor,weshouldneverblockclientsofourapplication.Thisiscrucialtoachievetheresponsivenesswedesirewhenbuildingreactivesystems.
Inanagewhereprocessingcoresareabundant—myMacBookProhaseightprocessorcores—blockingAPIsseverelyunderutilizestheresourceswehaveatourdisposal.
Asweapproachtheendofthisbook,itisappropriatetostepbackalittleandappreciatethatnotallclassesofproblemsthatdealwithconcurrent,asynchronouscomputationsrequirethemachineryofframeworkssuchasRxJavaorcore.async.
Inthischapter,wewilllookatanotherabstractionthathelpsusdevelopconcurrent,asynchronousapplications:futures.Wewilllearnabout:
TheproblemsandlimitationswithClojure’simplementationoffuturesAnalternativetoClojure’sfuturesthatprovidesasynchronous,composablesemanticsHowtooptimizeconcurrencyinthefaceofblockingIO
ClojurefuturesThefirststeptowardfixingthisissue—thatis,topreventapotentiallylong-runningtaskfromblockingourapplication—istocreatenewthreads,whichdotheworkandwaitforittocomplete.Thisway,wekeeptheapplication’smainthreadfreetoservemoreclients.
Workingdirectlywiththreads,however,istediousanderror-prone,soClojure’scorelibraryincludesfutures,whichareextremelysimpletouse:
(deff(clojure.core/future
(println"doingsomeexpensivework…")
(Thread/sleep5000)
(println"done")
10))
(println"You'llseemebeforethefuturefinishes")
;;doingsomeexpensivework…
;;You'llseemebeforethefuturefinishes
;;done
Intheprecedingsnippet,weinvoketheclojure.core/futuremacrowithabodysimulatinganexpensivecomputation.Inthisexample,itsimplysleepsfor5secondsbeforereturningthevalue10.Astheoutputdemonstrates,thisdoesnotblockthemainthread,whichisfreetoservemoreclients,pickworkitemsfromaqueue,orwhathaveyou.
Ofcourse,themostinterestingcomputations,suchastheexpensiveone,returnresultswecareabout.ThisiswherethefirstlimitationofClojurefuturesbecomesapparent.Ifweattempttoretrievetheresultofafuture—byderefingit—beforeithascompleted,thecallingthreadwillblockuntilthefuturereturnsavalue.Tryrunningthefollowingslightlymodifiedversionoftheprevioussnippet:
(deff(clojure.core/future
(println"doingsomeexpensivework…")
(Thread/sleep5000)
(println"done")
10))
(println"You'llseemebeforethefuturefinishes")
@f
(println"Icouldbedoingsomethingelse.InsteadIhadtowait")
;;doingsomeexpensivework…
;;You'llseemebeforethefuturefinishes
;;5SECONDSLATER
;;done
;;Icouldbedoingsomethingelse.Instead,Ihadtowait
Theonlydifferencenowisthatweimmediatelytrytoderefthefutureafterwecreateit.Sincethefutureisn’tdone,wesittherewaitingfor5secondsuntilitreturnsitsvalue.Onlythenisourprogramallowedtocontinue.
Ingeneral,thisposesaproblemwhenbuildingmodularsystems.Often,along-running
operationliketheonedescribedearlierwouldbeinitiatedwithinaspecificmoduleorfunction,andhandedovertothenextlogicalstepforfurtherprocessing.
Clojurefuturesdon’tallowustoscheduleafunctiontobeexecutedwhenthefuturefinishesinordertoperformsuchfurtherprocessing.Thisisanimportantfeatureinbuildingreactivesystems.
FetchingdatainparallelTounderstandbettertheissuesoutlinedintheprevioussection,let’sbuildamorecomplexexamplethatfetchesdataaboutoneofmyfavoritemovies,TheLordoftheRings.
Theideaisthatgiventhemovie,wewishtoretrieveitsactorsand,foreachactor,retrievethemoviestheyhavebeenapartof.Wealsowouldliketofindoutmoreinformationabouteachactor,suchastheirspouses.
Additionally,wewillmatcheachactor’smovieagainstthelistoftopfivemoviesinordertohighlightthemassuch.Finally,theresultwillbeprintedtothescreen.
Fromtheproblemstatement,weidentifythefollowingtwomaincharacteristicswewillneedtoaccountfor:
SomeofthesetasksneedtobeperformedinparallelTheyestablishdependenciesoneachother
Togetstarted,let’screateanewleiningenproject:
leinnewclj-futures-playground
Next,openthecorenamespacefileinsrc/clj_futures_playground/core.cljandaddthedatawewillbeworkingwith:
(nsclj-futures-playground.core
(:require[clojure.pprint:refer[pprint]]))
(defmovie
{:name"LordofTheRings:TheFellowshipofTheRing"
:cast["CateBlanchett"
"ElijahWood"
"LivTyler"
"OrlandoBloom"]})
(defactor-movies
[{:name"CateBlanchett"
:movies["LordofTheRings:TheFellowshipofTheRing"
"LordofTheRings:TheReturnofTheKing"
"TheCuriousCaseofBenjaminButton"]}
{:name"ElijahWood"
:movies["EternalSunshineoftheSpotlessMind"
"GreenStreetHooligans"
"TheHobbit:AnUnexpectedJourney"]}
{:name"LivTyler"
:movies["LordofTheRings:TheFellowshipofTheRing"
"LordofTheRings:TheReturnofTheKing"
"Armageddon"]}
{:name"OrlandoBloom"
:movies["LordofTheRings:TheFellowshipofTheRing"
"LordofTheRings:TheReturnofTheKing"
"PiratesoftheCaribbean:TheCurseoftheBlackPearl"]}])
(defactor-spouse
[{:name"CateBlanchett":spouse"AndrewUpton"}
{:name"ElijahWood":spouse"Unknown"}
{:name"LivTyler":spouse"RoystonLangdon"}
{:name"OrlandoBloom":spouse"MirandaKerr"}])
(deftop-5-movies
["LordofTheRings:TheFellowshipofTheRing"
"TheMatrix"
"TheMatrixReloaded"
"PiratesoftheCaribbean:TheCurseoftheBlackPearl"
"Terminator"])
Thenamespacedeclarationissimpleandonlyrequiresthepprintfunction,whichwillhelpusprintourresultinaneasy-to-readformat.Withallthedatainplace,wecancreatethefunctionsthatwillsimulateremoteservicesresponsibleforfetchingtherelevantdata:
(defncast-by-movie[name]
(future(do(Thread/sleep5000)
(:castmovie))))
(defnmovies-by-actor[name]
(do(Thread/sleep2000)
(->>actor-movies
(filter#(=name(:name%)))
first)))
(defnspouse-of[name]
(do(Thread/sleep2000)
(->>actor-spouse
(filter#(=name(:name%)))
first)))
(defntop-5[]
(future(do(Thread/sleep5000)
top-5-movies)))
Eachservicefunctionsleepsthecurrentthreadbyagivenamountoftimetosimulateaslownetwork.Thefunctionscast-by-movieandTop5eachreturnsafuture,indicatingwewishtofetchthisdataonadifferentthread.Theremainingfunctionssimplyreturntheactualdata.Theywillalsobeexecutedinadifferentthread,however,aswewillseeshortly.
Thenextthingweneedisafunctiontoaggregateallfetcheddata,matchspousestoactors,andhighlightmoviesintheTop5list.We’llcallittheaggregate-actor-datafunction:
(defnaggregate-actor-data[spousesmoviestop-5]
(map(fn[{:keys[namespouse]}{:keys[movies]}]
{:namename
:spousespouse
:movies(map(fn[m]
(if(some#{m}top-5)
(strm"-(top5)")
m))
movies)})
spouses
movies))
Theprecedingfunctionisfairlystraightforward.Itsimplyzipsspousesandmoviestogether,buildingamapofkeys:name,:spouse,and:movies.ItfurthertransformsmoviestoappendtheTop5suffixtotheonesinthetop-5list.
Thelastpieceofthepuzzleisthe-mainfunction,whichallowsustoruntheprogramfromthecommandline:
(defn-main[&args]
(time(let[cast(cast-by-movie"LordofTheRings:TheFellowshipof
TheRing")
movies(pmapmovies-by-actor@cast)
spouses(pmapspouse-of@cast)
top-5(top-5)]
(prn"Fetchingdata…")
(pprint(aggregate-actor-dataspousesmovies@top-5))
(shutdown-agents))))
Thereareanumberofthingsworthhighlightingintheprecedingsnippet.
First,wewrapthewholebodyinacalltotime,asimplebenchmarkingfunctionthatcomeswithClojure.Thisisjustsoweknowhowlongtheprogramtooktofetchalldata—thisinformationwillbecomerelevantlater.
Then,wesetupanumberofletbindings.Thefirst,cast,istheresultofcallingcast-by-movie,whichreturnsafuture.
Thenextbinding,movies,usesafunctionwehaven’tseenbefore:pmap.
Thepmapfunctionworkslikemap,exceptthefunctionismappedovertheitemsinthelistinparallel.Thepmapfunctionusesfuturesunderthecoversandthatisthereasonmovies-by-actordoesn’treturnafuture—itleavesthatforpmaptohandle.
TipThepmapfunctionisactuallymeantforCPU-boundoperations,butisusedheretokeepthecodesimple.InthefaceofblockingIO,pmapwouldn’tperformoptimally.WewilltalkmoreaboutblockingIOlaterinthischapter.
Wegetthelistofactorsbyderefingthecastbinding,which,aswesawintheprevioussection,blocksthecurrentthreadwaitingfortheasynchronousfetchtofinish.Onceallresultsareready,wesimplycalltheaggregate-actor-datafunction.
Lastly,wecalltheshutdown-agentsfunction,whichshutsdowntheThreadPoolbackingfuturesinClojure.Thisisnecessaryforourprogramtoterminateproperly,otherwiseitwouldsimplyhangintheterminal.
Toruntheprogram,typethefollowingintheterminal,undertheproject’srootdirectory:
leinrun-mclj-futures-playground.core
"Fetchingdata…"
({:name"CateBlanchett",
:spouse"AndrewUpton",
:movies
("LordofTheRings:TheFellowshipofTheRing-(top5)"
"LordofTheRings:TheReturnofTheKing"
"TheCuriousCaseofBenjaminButton")}
{:name"ElijahWood",
:spouse"Unknown",
:movies
("EternalSunshineoftheSpotlessMind"
"GreenStreetHooligans"
"TheHobbit:AnUnexpectedJourney")}
{:name"LivTyler",
:spouse"RoystonLangdon",
:movies
("LordofTheRings:TheFellowshipofTheRing-(top5)"
"LordofTheRings:TheReturnofTheKing"
"Armageddon")}
{:name"OrlandoBloom",
:spouse"MirandaKerr",
:movies
("LordofTheRings:TheFellowshipofTheRing-(top5)"
"LordofTheRings:TheReturnofTheKing"
"PiratesoftheCaribbean:TheCurseoftheBlackPearl-(top5)")})
"Elapsedtime:10120.267msecs"
Youwillhavenoticedthattheprogramtakesawhiletoprintthefirstmessage.Additionally,becausefuturesblockwhentheyarederefed,theprogramdoesn’tstartfetchingthelistoftopfivemoviesuntilithascompletelyfinishedfetchingthecastofTheLordofTheRings.
Let’shavealookatwhythatisso:
(time(let[cast(cast-by-movie"LordofTheRings:TheFellowshipof
TheRing")
;;thefollowinglineblocks
movies(pmapmovies-by-actor@cast)
spouses(pmapspouse-of@cast)
top-5(top-5)]
Thehighlightedsectionintheprecedingsnippetshowswheretheprogramblockswaitingforcast-by-movietofinish.Asstatedpreviously,Clojurefuturesdon’tgiveusawaytorunsomepieceofcodewhenthefuturefinishes—likeacallback—forcingustoblocktoosoon.
Thispreventstop-5—acompletelyindependentparalleldatafetch—fromrunningbeforeweretrievethemovie’scast.
Ofcourse,thisisacontrivedexample,andwecouldsolvethisparticularannoyancebycallingtop-5beforeanythingelse.Theproblemisthatthesolutionisn’talwayscrystalclearandideallyweshouldnothavetoworryabouttheorderofexecution.
Aswewillseeinthenextsection,thereisabetterway.
Imminent–acomposablefutureslibraryforClojureInthepastfewmonths,IhavebeenworkingonanopensourcelibrarythataimstofixthepreviousissueswithClojurefutures.Theresultofthisworkiscalledimminent(seehttps://github.com/leonardoborges/imminent).
Thefundamentaldifferenceisthatimminentfuturesareasynchronousbydefaultandprovideanumberofcombinatorsthatallowustodeclarativelywriteourprogramswithouthavingtoworryaboutitsorderofexecution.
Thebestwaytodemonstratehowthelibraryworksistorewritethepreviousmoviesexampleinit.Wewilldothisintwosteps.
First,wewillexamineindividuallythebitsofimminent’sAPIthatwillbepartofourfinalsolution.Then,we’llputitalltogetherinaworkingapplication.Let’sstartbycreatinganewproject:
leinnewimminent-playground
Next,addadependencyonimminenttoyourproject.clj:
:dependencies[[org.clojure/clojure"1.6.0"]
[com.leonardoborges/imminent"0.1.0"]]
Then,createanewfile,src/imminent_playground/repl.clj,andaddimminent’scorenamespace:
(nsimminent-playground.repl
(:require[imminent.core:asIi]))
(defrepl-out*out*)
(defnprn-to-repl[&args]
(binding[*out*repl-out]
(applyprnargs)))
Theprecedingsnippetalsocreatesahelperfunctionthatisusefulwhenwe’redealingwithmultiplethreadsintheREPL—thiswillbeexplainedindetaillater,butfornowjusttakethisasbeingareliablewaytoprinttotheREPLacrossmultiplethreads.
FeelfreetotypethisintheREPLaswegoalong.Otherwise,youcanrequirethenamespacefilefromarunningREPLlikeso:
(require'imminent-playground.repl)
Allthefollowingexamplesshouldbeinthisfile.
CreatingfuturesCreatingafutureinimminentisn’tmuchdifferentfromcreatingafutureinClojure.It’sassimpleasthefollowing:
(defage(i/future31))
;;#<Future@2ea0ca7d:#<Success@3e4dec75:31>>
Whatlooksverydifferent,however,isthereturnvalue.Akeydecisioninimminent’sAPIistorepresentthevalueofacomputationaseitheraSuccessoraFailuretype.Success,asintheprecedingexample,wrapstheresultofthecomputation.Failure,asyoumighthaveguessed,willwrapanyexceptionsthathappenedinthefuture:
(deffailed-computation(i/future(throw(Exception."Error"))))
;;#<Future@63cd0d58:#<Failure@2b273f98:#<Exceptionjava.lang.Exception:
Error>>>
(deffailed-computation-1(i/failed-future:invalid-data))
;;#<Future@a03588f:#<Failure@61ab196b::invalid-data>>
Asyoucansee,you’renotlimitedtoexceptionsonly.Wecanusethefailed-futurefunctiontocreateafuturethatcompletesimmediatelywiththegivenreason,which,inthesecondexample,issimplyakeyword.
Thenextquestionwemightaskis“Howdowegettheresultoutofafuture?”.AswithClojurefutures,wecanderefitasfollows:
@age;;#<Success@3e4dec75:31>
(deref@age);;31
(i/dderefage);;31
Theidiomofusingadouble-derefiscommon,soimminentprovidestheconvenienceshown,dderef,whichisequivalenttocallingdereftwice.
However,differentfromClojurefutures,thisisanon-blockingoperation,soifthefuturehasn’tcompletedyet,thefollowingiswhatyou’llget:
@(i/future(do(Thread/sleep500)
"hello"))
;;:imminent.future/unresolved
Theinitialstateofafutureisunresolved,sounlessyouareabsolutelycertainafuturehascompleted,derefingmightnotbethebestwaytoworkwiththeresultofacomputation.Thisiswherecombinatorsbecomeuseful.
CombinatorsandeventhandlersLet’ssaywewouldliketodoublethevalueintheagefuture.Aswewouldwithlists,wecansimplymapafunctionoverthefuturetodojustthis:
(defdouble-age(i/mapage#(*%2)))
;;#<Future@659684cb:#<Success@7ce85f87:62>>
TipWhilei/futureschedulesitsbodyforexecutiononaseparatethread,it’sworthnotingthatfuturecombinatorssuchasmap,filter,andsoon,donotcreateanewthreadimmediately.Instead,theyscheduleafunctiontobeexecutedasynchronouslyinthethreadpooloncetheoriginalfuturecompletes.
Anotherwaytodosomethingwiththevalueofafutureistousetheon-successeventhandlerthatgetscalledwiththewrappedvalueofthefutureincaseitissuccessful:
(i/on-successage#(prn-to-repl(str"Ageis:"%)))
;;"Ageis:31"
Similarly,anon-failurehandlerexists,whichdoesthesameforFailuretypes.Whileonthesubjectoffailures,imminentfuturesunderstandthecontextinwhichtheyarebeingexecutedand,ifthecurrentfutureyieldsaFailure,itsimplyshort-circuitsthecomputation:
(->failed-computation
(i/map#(*%2)))
;;#<Future@7f74297a:#<Failure@2b273f98:#<Exceptionjava.lang.Exception:
Error>>>
Intheprecedingexample,wedon’tgetanewerror,butrathertheoriginalexceptioncontainedinfailed-computation.Thefunctionpassedtomapneverruns.
ThedecisiontowraptheresultofafutureinatypesuchasSuccessorFailuremightseemarbitrarybutisactuallyquitetheopposite.BothtypesimplementtheprotocolIReturn—andacoupleofotherones—whichcomeswithasetofusefulfunctions,oneofwhichismap:
(i/map(i/success"hello")
#(str%"world"))
;;#<Success@714eea92:"helloworld">
(i/map(i/failure"error")
#(str%"world"))
;;#<Failure@6d685b65:"error">
Wegetasimilarbehaviorhereaswedidpreviously:mappingafunctionoverafailuresimplyshort-circuitsthewholecomputation.Ifyoudo,however,wishtomapoverthefailure,youcanusemap’scounterpartmap-failure,whichbehavessimilarlytomapbutisitsinverse:
(i/map-failure(i/success"hello")
#(str%"world"))
;;#<Success@779af3f4:"hello">
(i/map-failure(i/failure"Error")
#(str"Wefailed:"%))
;;#<Failure@52a02597:"Wefailed:Error">
Thisplayswellwiththelasteventhandlersimminentprovides—on-complete:
(i/on-completeage
(fn[result]
(i/mapresult#(prn-to-repl"success:"%))
(i/map-failureresult#(prn-to-repl"error:"%))))
;;"success:"31
Oncontrarytoon-successandon-failure,on-completecallstheprovidedfunctionwiththeresulttypewrapper,soitisaconvenientwaytohandlebothcasesinasinglefunction.
Comingbacktocombinators,sometimeswewillneedtomapafunctionoverafuture,whichitselfreturnsafuture:
(defnrange-future[n]
(i/const-future(rangen)))
(defage-range(i/mapagerange-future))
;;#<Future@3d24069e:#<Success@82e8e6e:#<Future@2888dbf4:#
<Success@312084f6:(012…)>>>>
Therange-futurefunctionreturnsasuccessfulfuturethatyieldsarangeofn.Theconst-futurefunctionisanalogoustofailed-future,exceptitimmediatelycompletesthefuturewithaSuccesstype.
However,weendupwithanestedfuture,whichisalmostneverwhatyouwant.That’sOK.Thisispreciselythescenarioinwhichyouwoulduseanothercombinator,flatmap.
Youcanthinkofitasmapcatforfutures—itflattensthecomputationforus:
(defage-range(i/flatmapagerange-future))
;;#<Future@601c1dfc:#<Success@55f4bcaf:(012…)>>
Anotherveryusefulcombinatorisusedtobringtogethermultiplecomputationstobeusedinasinglefunction—sequence:
(defname(i/future(do(Thread/sleep500)
"Leo")))
(defgenres(i/future(do(Thread/sleep500)
["HeavyMetal""BlackMetal""DeathMetal""Rock
'nRoll"])))
(->(i/sequence[nameagegenres])
(i/on-success
(fn[[nameagegenres]]
(prn-to-repl(format"%sis%syearsoldandenjoys%s"
name
age
(clojure.string/join","genres))))))
;;"Leois31yearsoldandenjoysHeavyMetal,BlackMetal,DeathMetal,Rock
'nRoll"
Essentially,sequencecreatesanewfuture,whichwillcompleteonlywhenallotherfuturesinthevectorhavecompletedoranyoneofthemhavefailed.
Thisisanicesegueintothelastcombinatorwewilllookat—map-future—whichwewoulduseinplaceofpmap,usedinthemoviesexample:
(defncalculate-double[n]
(i/const-future(*n2)))
(->(i/map-futurecalculate-double[1234])
i/await
i/dderef)
;;[2468]
Intheprecedingexample,calculate-doubleisafunctionthatreturnsafuturewiththevaluendoubled.Themap-futurefunctionthenmapscalculate-doubleoverthelist,effectivelyperformingthecalculationsinparallel.Finally,map-futuresequencesallfuturestogether,returningasinglefuture,whichyieldstheresultofallcomputations.
Becauseweareperforminganumberofparallelcomputationsanddon’treallyknowwhentheywillfinish,wecallawaitonthefuture,whichisawaytoblockthecurrentthreaduntilitsresultisready.Ingeneral,youwouldusethecombinatorsandeventhandlersinstead,butforthisexample,usingawaitisacceptable.
Imminent’sAPIprovidesmanymorecombinators,whichhelpuswriteasynchronousprogramsinadeclarativeway.ThissectiongaveusatasteofwhatispossiblewiththeAPIandisenoughtoallowustowritethemoviesexampleusingimminentfutures.
ThemoviesexamplerevisitedStillwithinourimminent-playgroundproject,openthesrc/imminent_playground/core.cljfileandaddtheappropriatedefinitions:
(nsimminent-playground.core
(:require[clojure.pprint:refer[pprint]]
[imminent.core:asi]))
(defmovie…)
(defactor-movies…)
(defactor-spouse…)
(deftop-5-movies…)
Wewillbeusingthesamedataasinthepreviousprogram,representedintheprecedingsnippetbytheuseofellipses.Simplycopytherelevantdeclarationsover.
Theservicefunctionswillneedsmalltweaksinthisnewversion:
(defncast-by-movie[name]
(i/future(do(Thread/sleep5000)
(:castmovie))))
(defnmovies-by-actor[name]
(i/future(do(Thread/sleep2000)
(->>actor-movies
(filter#(=name(:name%)))
first))))
(defnspouse-of[name]
(i/future(do(Thread/sleep2000)
(->>actor-spouse
(filter#(=name(:name%)))
first))))
(defntop-5[]
(i/future(do(Thread/sleep5000)
top-5-movies)))
(defnaggregate-actor-data[spousesmoviestop-5]
...)
Themaindifferenceisthatallofthemnowreturnanimminentfuture.Theaggregate-actor-datafunctionisalsothesameasbefore.
Thisbringsustothe-mainfunction,whichwasrewrittentouseimminentcombinators:
(defn-main[&args]
(time(let[cast(cast-by-movie"LordofTheRings:TheFellowshipof
TheRing")
movies(i/flatmapcast#(i/map-futuremovies-by-actor%))
spouses(i/flatmapcast#(i/map-futurespouse-of%))
result(i/sequence[spousesmovies(top-5)])]
(prn"Fetchingdata…")
(pprint(applyaggregate-actor-data
(i/dderef(i/awaitresult)))))))
Thefunctionstartsmuchlikeitspreviousversion,andeventhefirstbinding,cast,looksfamiliar.Nextwehavemovies,whichisobtainedbyfetchinganactor’smoviesinparallel.Thisinitselfreturnsafuture,soweflatmapitoverthecastfuturetoobtainourfinalresult:
movies(i/flatmapcast#(i/map-futuremovies-by-actor%))
spousesworksinexactlythesamewayasmovies,whichbringsustoresult.Thisiswherewewouldliketobringallasynchronouscomputationstogether.Therefore,weusethesequencecombinator:
result(i/sequence[spousesmovies(top-5)])
Finally,wedecidetoblockontheresultfuture—byusingawait—sowecanprintthefinalresult:
(pprint(applyaggregate-actor-data
(i/dderef(i/awaitresult)))
Weruntheprograminthesamewayasbefore,sosimplytypethefollowinginthecommandline,undertheproject’srootdirectory:
leinrun-mimminent-playground.core
"Fetchingdata…"
({:name"CateBlanchett",
:spouse"AndrewUpton",
:movies
("LordofTheRings:TheFellowshipofTheRing-(top5)"
"LordofTheRings:TheReturnofTheKing"
"TheCuriousCaseofBenjaminButton")}
...
"Elapsedtime:7088.398msecs"
Theresultoutputwastrimmedasitisexactlythesameasbefore,buttwothingsaredifferentanddeserveattention:
Thefirstoutput,Fetchingdata…,isprintedtothescreenalotfasterthanintheexampleusingClojurefuturesTheoveralltimeittooktofetchallthatisshorter,clockinginatjustover7seconds
Thishighlightstheasynchronousnatureofimminentfuturesandcombinators.Theonlytimewehadtowaitiswhenweexplicitlycalledawaitattheendoftheprogram.
Morespecifically,theperformanceboostcomesfromthefollowingsectioninthecode:
(let[...
result(i/sequence[spousesmovies(top-5)])]
...)
Becausenoneofthepreviousbindingsblockthecurrentthread,weneverhavetowaitto
kickofftop-5inparallel,shavingoffroughly3secondsfromtheoverallexecutiontime.Wedidn’thavetoexplicitlythinkabouttheorderofexecution—thecombinatorssimplydidtherightthing.
Finally,onelastdifferenceisthatwedidn’thavetoexplicitlycallshutdown-agentsasbefore.Thereasonforthisisthatimminentusesadifferenttypeofthreadpool:aForkJoinPool(seehttp://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ForkJoinPool.html).
Thispoolhasanumberofadvantages—eachwithitsowntrade-off—overtheotherthreadpools,andonecharacteristicisthatwedon’tneedtoexplicitlyshutitdown—allthreadsitcreatesdaemonthreads.
WhentheJVMshutsdown,ithangswaitingforallnon-daemonthreadstofinish.Onlythendoesitexit.That’swhyusingClojurefutureswouldcausetheJVMtohang,ifwehadnotcalledshutdown-agents.
AllthreadscreatedbytheForkJoinPoolaresetasdaemonthreadsbydefault:whentheJVMattemptstoshutdown,andiftheonlythreadsrunningaredaemonones,theyareabandonedandtheJVMexitsgracefully.
Combinatorssuchasmapandflatmap,aswellasthefunctionssequenceandmap-future,aren’texclusivetofutures.Theyhavemanymorefundamentalprinciplesbywhichtheyabide,makingthemusefulinarangeofdomains.Understandingtheseprinciplesisn’tnecessaryforfollowingthecontentsofthisbook.Shouldyouwanttoknowmoreabouttheseprinciples,pleaserefertotheAppendix,TheAlgebraofLibraryDesign.
FuturesandblockingIOThechoiceofusingForkJoinPoolforimminentisdeliberate.TheForkJoinPool—addedonJava7—isextremelysmart.Whencreated,yougiveitadesiredlevelofparallelism,whichdefaultstothenumberofavailableprocessors.
ForkJoinPoolthenattemptstohonorthedesiredparallelismbydynamicallyshrinkingandexpandingthepoolasrequired.Whenataskissubmittedtothispool,itdoesn’tnecessarilycreateanewthreadifitdoesn’thaveto.Thisallowsthepooltoserveanextremelylargenumberoftaskswithamuchsmallernumberofactualthreads.
However,itcannotguaranteesuchoptimizationsinthefaceofblockingIO,asitcan’tknowwhetherthethreadisblockingwaitingforanexternalresource.Nevertheless,ForkJoinPoolprovidesamechanismbywhichthreadscannotifythepoolwhentheymightblock.
ImminenttakesadvantageofthismechanismbyimplementingtheManagedBlocker(seehttp://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ForkJoinPool.ManagedBlocker.htmlinterface—andprovidesanotherwaytocreatefutures,asdemonstratedhere:
(->(immi/blocking-future
(Thread/sleep100)
10)
(immi/await))
;;#<Future@4c8ac77a:#<Success@45525276:10>>
(->(immi/blocking-future-call
(fn[]
(Thread/sleep100)
10))
(immi/await))
;;#<Future@37162438:#<Success@5a13697f:10>>
Theblocking-futureandblocking-future-callhavethesamesemanticsastheircounterparts,futureandfuture-call,butshouldbeusedwhenthetasktobeperformedisofablockingnature(thatis,notCPU-bound).ThisallowstheForkJoinPooltobetterutilizeitsresources,makingitapowerfulandflexiblesolution.
SummaryInthischapter,welearnedthatClojurefuturesleavealottobedesired.Morespecifically,Clojurefuturesdon’tprovideawaytoexpressdependenciesbetweenresults.Itdoesn’tmean,however,thatweshoulddismissfuturesaltogether.
Theyarestillausefulabstractionandwiththerightsemanticsforasynchronouscomputationsandarichsetofcombinators—suchastheonesprovidedbyimminent—theycanbeabigallyinbuildingreactiveapplicationsthatareperformantandresponsive.Sometimes,thisisallweneed.
Forthetimeswhereweneedtomodeldatathatvariesovertime,weturntoricherframeworksinspiredbyFunctionalReactiveProgramming(FRP)andCompositionalEventSystems(CES)—suchasRxJava—orCommunicatingSequentialProcesses(CSP)—suchascore.async.Astheyhavealotmoretooffer,muchofthisbookhasbeendedicatedtothoseapproaches.
Inthenextchapter,wewillgobacktodiscussingFRP/CESbywayofacasestudy.
Chapter9.AReactiveAPItoAmazonWebServicesThroughoutthisbook,wehavelearnedanumberoftoolsandtechniquestoaidusinbuildingreactiveapplications—futureswithimminent,ObservableswithRxClojure/RxJava,channelswithcore.async—andeveninbuildingreactiveuserinterfacesusingOmandReact.
Intheprocess,wealsobecameacquaintedwiththeconceptofFunctionalReactiveProgrammingandCompositionalEventSystems,aswellaswhatmakesthemdifferent.
Inthislastchapter,wewillbringafewofthesedifferenttoolsandconceptstogetherbydevelopinganapplicationbasedonareal-worldusecasefromaclientIworkedwithinSydney,Australia.Wewill:
DescribetheproblemofinfrastructureautomationweweretryingtosolveHaveabrieflookatsomeofAmazon’sAWSservicesBuildanAWSdashboardusingtheconceptswehavelearnedsofar
TheproblemThisclient—whichwewillcallBubbleCorpfromnowon—hadabigproblemthatisalltoocommonandwellknowntobigenterprises:onemassivemonolithicapplication.
Besidesmakingthemmoveslow,asindividualcomponentscan’tbeevolvedindependently,thisapplicationmakesdeploymentincrediblyhardduetoitsenvironmentconstraints:allinfrastructureneedstobeavailableinorderfortheapplicationtoworkatall.
Asaresult,developingnewfeaturesandbugfixesinvolveshavingonlyahandfulofdevelopmentenvironmentssharedacrossdozensofdeveloperseach.Thisrequiresawastefulamountofcoordinationbetweenteamsjustsothattheywon’tsteponeachother’stoes,contributingtoslowthewholelife-cyclefurther.
Thelong-termsolutiontothisproblemistobreakdownthisbigapplicationintosmallercomponents,whichcanbedeployedandworkedonindependently,butasgoodasthissounds,it’salaboriousandlengthyprocess.
Asafirststep,BubbleCorpdecidedthebestthingtheycouldimproveintheshorttermistogivedeveloperstheabilitytoworkintheapplicationindependentlyfromeachother,whichimpliesbeingabletocreateanewenvironmentaswell.
Giventheinfrastructureconstraints,runningtheapplicationonasingledevelopermachineisprohibitive.
Instead,theyturnedtoinfrastructureautomation:theywantedatoolthat,withthepressofabutton,wouldspinupacompletelynewenvironment.
Thisnewenvironmentwouldbealreadypreconfiguredwiththeproperapplicationservers,databaseinstances,DNSentries,andeverythingelseneededtoruntheapplication.
Thisway,developerswouldonlyneedtodeploytheircodeandtesttheirchanges,withouthavingtoworryabouttheapplicationsetup.
InfrastructureautomationAmazonWebServices(AWS)isthemostmatureandcomprehensivecloudcomputingplatformavailabletoday,andassuchitwasanaturalchoiceforBubbleCorptohostitsinfrastructurein.
Ifyouhaven’tusedAWSbefore,don’tworry,we’llfocusonlyonafewofitsservices:
ElasticComputeCloud(EC2):Aservicethatprovidesuserswiththeabilitytorentvirtualcomputersinwhichtoruntheirapplications.RelationalDatabaseService(RDS):ThiscanbethoughtofasaspecializedversionofEC2thatprovidesmanageddatabaseservices.CloudFormation:WithCloudFormation,usershavetheabilitytospecifyinfrastructuretemplates,calledstacks,ofseveraldifferentAWSresources—suchasEC2,AWS,andmanyothers—aswellashowtheyinteractwitheachother.Oncewritten,theinfrastructuretemplatecanbesenttoAWStobeexecuted.
ForBubbleCorp,theideawastowritetheseinfrastructuretemplates,whichoncesubmittedwouldresultintoacompletelynew,isolatedenvironmentcontainingalldataandcomponentsrequiredtorunitsapp.Atanygiventime,therewouldbedozensoftheseenvironmentsrunningwithdevelopersworkingonthem.
Asdecentaplanasthissounds,bigcorporationsusuallyhaveanaddedburden:costcenters.Unfortunately,BubbleCorpcan’tsimplyallowdeveloperstologintotheAWSConsole—wherewecanmanageAWSresources—andspinupenvironmentsatwill.Theyneededawayto,amongotherthings,addcostcentermetadatatotheenvironmenttohandletheirinternalbillingprocess.
Thisbringsustotheapplicationwewillbefocusingonfortheremainderofthischapter.
AWSresourcesdashboardMyteamandIweretaskedwithbuildingaweb-baseddashboardforAWS.ThisdashboardwouldallowdeveloperstologinusingtheirBubbleCorp’scredentialsand,onceauthenticated,createnewCloudFormationenvironmentsaswellasvisualizethestatusofeachindividualresourcewithinaCloudFormationstack.
Theapplicationitselfisfairlyinvolved,sowewillfocusonasubsetofit:interfacingwiththenecessaryAWSservicesinordertogatherinformationaboutthestatusofeachindividualresourceinagivenCloudFormationstack.
Oncefinished,thisiswhatoursimplifieddashboardwilllooklike:
ItwilldisplaytheID,type,andcurrentstatusofeachresource.Thismightnotseemlikemuchfornow,butgiventhatallthisinformationiscomingfromdifferent,independentwebservices,itisfartooeasytoendupwithunnecessarilycomplexcode.
WewillbeusingClojureScriptforthisandthereforetheJavaScriptversionoftheAWSSDK,whosedocumentationcanbefoundathttp://aws.amazon.com/sdk-for-node-js/.
Beforewegetstarted,let’shavealookateachoftheAWSServicesAPIswewillbeinteractingwith.
TipInreality,wewillnotbeinteractingwiththerealAWSservicesbutratherastubserverprovidedfordownloadfromthebook’sGitHubrepository.
Thereasonforthisistomakefollowingthischaptereasier,asyouwon’tneedtocreateanaccountaswellasgenerateanAPIaccesskeytointeractwithAWS.
Additionally,creatingresourcesincurscost,andthelastthingIwantisforyoutobechargedhundredsofdollarsattheendofthemonthbecausesomeoneaccidentallyleftresourcesrunningforlongerthantheyshould—trustmeithashappenedbefore.
CloudFormationThefirstservicewewilllookatisCloudFormation.ThismakessenseastheAPIsfoundinherewillgiveusastartingpointforfindinginformationabouttheresourcesinagivenstack.
ThedescribeStacksendpointThisendpointisresponsibleforlistingallstacksassociatedwithaparticularAWSaccount.Foragivenstack,itsresponselookslikethefollowing:
{"Stacks"
[{"StackId"
"arn:aws:cloudformation:ap-southeast-2:337944750480:stack/DevStack-
62031/1",
"StackStatus""CREATE_IN_PROGRESS",
"StackName""DevStack-62031",
"Parameters"[{"ParameterKey""DevDB","ParameterValue"nil}]}]}
Unfortunately,itdoesn’tsayanythingaboutwhichresourcesbelongtothisstack.Itdoes,however,giveusthestackname,whichwecanusetolookupresourcesinthenextservice.
ThedescribeStackResourcesendpointThisendpointreceivesmanyarguments,buttheonewe’reinterestedinisthestackname,which,onceprovided,returnsthefollowing:
{"StackResources"
[{"PhysicalResourceId""EC2123",
"ResourceType""AWS::EC2::Instance"},
{"PhysicalResourceId""EC2456",
"ResourceType""AWS::EC2::Instance"}
{"PhysicalResourceId""EC2789",
"ResourceType""AWS::EC2::Instance"}
{"PhysicalResourceId""RDS123",
"ResourceType""AWS::RDS::DBInstance"}
{"PhysicalResourceId""RDS456",
"ResourceType""AWS::RDS::DBInstance"}]}
Weseemtobegettingsomewherenow.Thisstackhasseveralresources:threeEC2instancesandtwoRDSinstances—nottoobadforonlytwoAPIcalls.
However,aswementionedpreviously,ourdashboardneedstoshowthestatusofeachoftheresources.WiththelistofresourceIDsathand,weneedtolooktootherservicesthatcouldgiveusdetailedinformationabouteachresource.
EC2ThenextservicewewilllookatisspecifictoEC2.Aswewillsee,theresponsesofthedifferentservicesaren’tasconsistentaswewouldlikethemtobe.
ThedescribeInstancesendpointThisendpointsoundspromising.Basedonthedocumentation,itseemswecangiveitalistofinstanceIDsanditwillgiveusbackthefollowingresponse:
{"Reservations"
[{"Instances"
[{"InstanceId""EC2123",
"Tags"
[{"Key""StackType","Value""Dev"}
{"Key""junkTag","Value""shouldnotbeincluded"}
{"Key""aws:cloudformation:logical-id","Value""theDude"}],
"State"{"Name""running"}}
{"InstanceId""EC2456",
"Tags"
[{"Key""StackType","Value""Dev"}
{"Key""junkTag","Value""shouldnotbeincluded"}
{"Key""aws:cloudformation:logical-id","Value""theDude"}],
"State"{"Name""running"}}
{"InstanceId""EC2789",
"Tags"
[{"Key""StackType","Value""Dev"}
{"Key""junkTag","Value""shouldnotbeincluded"}
{"Key""aws:cloudformation:logical-id","Value""theDude"}],
"State"{"Name""running"}}]}]}
Buriedinthisresponse,wecanseetheStatekey,whichgivesusthestatusofthatparticularEC2instance.ThisisallweneedasfarasEC2goes.ThisleavesuswithRDStohandle.
RDSOnemightbetemptedtothinkthatgettingthestatusesofRDSinstanceswouldbejustaseasyaswithEC2.Let’sseeifthatisthecase.
ThedescribeDBInstancesendpointThisendpointisequivalentinpurposetotheanalogousEC2endpointwejustlookedat.Itsinput,however,isslightlydifferent:itacceptsasingleinstanceIDasinputand,asofthetimeofthiswriting,doesn’tsupportfilters.
ThismeansthatifourstackhasmultipleRDSinstances—say,inaprimary/replicasetup—weneedtomakemultipleAPIcallstogatherinformationabouteachoneofthem.Notabigdeal,ofcourse,butalimitationtobeawareof.
OncegivenaspecificdatabaseinstanceID,thisservicerespondswiththefollowingcode:
{"DBInstances"
[{"DBInstanceIdentifier""RDS123","DBInstanceStatus""available"}]}
Thefactthatasingleinstancecomesinsideavectorhintsatthefactthatfilteringwillbesupportedinthefuture.Itjusthasn’thappenedyet.
DesigningthesolutionWenowhavealltheinformationweneedtostartdesigningourapplication.WeneedtocoordinatefourdifferentAPIcallsperCloudFormationstack:
describeStacks:TolistallavailablestacksdescribeStackResources:ToretrievedetailsofallresourcescontainedinastackdescribeInstances:ToretrievedetailsofallEC2instancesinastackdescribeDBInstances:ToretrievedetailsofallDB2instancesinastack
Next,Iwouldlikeyoutostepbackforamomentandthinkabouthowyouwoulddesigncodelikethis.Goahead,I’llwait.
Nowthatyou’reback,let’shavealookatonepossibleapproach.
Ifwerecallthescreenshotofwhatthedashboardwouldlooklike,werealizethat,forthepurposesofourapplication,thedifferencebetweenEC2andRDSresourcescanbecompletelyignoredsolongaseachonehastheattributesID,type,andstatus.
Thismeanswhateveroursolutionmaybe,ithastosomehowprovideauniformwayofabstractingthedifferentresourcetypes.
Additionally,apartfromdescribeStacksanddescribeStackResources,whichneedtobecalledsequentially,describeInstancesanddescribeDBInstancescanbeexecutedconcurrently,afterwhichwewillneedawaytomergetheresults.
Sinceanimageisworthathousandwords,thefollowingimageiswhatwewouldliketheworkflowtolooklike:
Theprecedingimagehighlightsanumberofkeyaspectsofoursolution:
WestartbyretrievingstacksbycallingdescribeStacksNext,foreachstack,wecalldescribeStackResourcestoretrievealistofresourcesforeachoneThen,wesplitthelistbytype,endingwithalistofEC2andonewithRDSresourcesWeproceedbyconcurrentlycallingdescribeInstancesanddescribeDBInstances,yieldingtwolistsofresults,oneperresourcetypeAstheresponseformatsaredifferent,wetransformeachresourceintoauniformrepresentationLastly,wemergeallresultsintoasinglelist,readyforrendering
Thisisquiteabittotakein,butasyouwillsoonrealize,oursolutionisn’ttoofaroffthishigh-leveldescription.
WecanquiteeasilythinkofthisproblemashavinginformationaboutseveraldifferenttypesofinstancesflowingthroughthisgraphofAPIcalls—beingtransformedasneededinbetween—untilwearriveattheinformationwe’reafter,intheformatwewouldliketoworkwith.
Asitturnsout,agreatwaytomodelthisproblemistouseoneoftheReactive
RunningtheAWSstubserverBeforewejumpintowritingourdashboard,weshouldmakesureourAWSstubserverisproperlysetup.ThestubserverisaClojurewebapplicationthatsimulateshowtherealAWSAPIbehavesandisthebackendourdashboardwilltalkto.
Let’sstartbygoingintoourterminal,cloningthebookrepositoryusingGitandthenstartingthestubserver:
$gitclonehttps://github.com/leonardoborges/ClojureReactiveProgramming
$cdClojureReactiveProgramming/code/chapter09/aws-api-stub
$leinringserver-headless3001
2014-11-2317:33:37.766:INFO:oejs.Server:jetty-7.6.8.v20121106
2014-11-2317:33:37.812:INFO:oejs.AbstractConnector:Started
[email protected]:3001
Startedserveronport3001
Thiswillhavestartedtheserveronport3001.Tovalidateitisworkingasexpected,pointyourbrowsertohttp://localhost:3001/cloudFormation/describeStacks.YoushouldseethefollowingJSONresponse:
{
"Stacks":[
{
"Parameters":[
{
"ParameterKey":"DevDB",
"ParameterValue":null
}
],
"StackStatus":"CREATE_IN_PROGRESS",
"StackId":"arn:aws:cloudformation:ap-southeast-
2:337944750480:stack/DevStack-62031/1",
"StackName":"DevStack-62031"
}
]
}
SettingupthedashboardprojectAswepreviouslymentioned,wewillbedevelopingthedashboardusingClojureScriptwiththeUIrenderedusingOm.Additionally,aswehavechosenObservablesasourmainReactiveabstraction,wewillneedRxJS,oneofthemanyimplementationsofMicrosoft’sReactiveExtensions.Wewillbepullingthesedependenciesintoourprojectshortly.
Let’screateanewClojureScriptprojectcalledaws-dashusingtheom-startleiningentemplate:
$leinnewom-startaws-dash
Thisgivesusastartingpoint,butweshouldmakesureourversionsallmatch.Openuptheproject.cljfilefoundintherootdirectoryofthenewprojectandensurethedependenciessectionlookslikethefollowing:
...
:dependencies[[org.clojure/clojure"1.6.0"]
[org.clojure/clojurescript"0.0-2371"]
[org.clojure/core.async"0.1.346.0-17112a-alpha"]
[om"0.5.0"]
[com.facebook/react"0.9.0"]
[cljs-http"0.1.20"]
[com.cognitect/transit-cljs"0.8.192"]]
:plugins[[lein-cljsbuild"1.0.3"]]
...
Thisisthefirsttimeweseethelasttwodependencies.cljs-httpisasimpleHTTPlibrarywewillusetomakeAJAXrequeststoourAWSstubserver.transit-cljsallowsusto,amongotherthings,parseJSONresponsesintoClojureScriptdatastructures.
TipTransititselfisaformatandasetoflibrariesthroughwhichapplicationsdevelopedindifferenttechnologiescanspeaktoeachother.Inthiscase,weareusingtheClojurescriptlibrarytoparseJSON,butifyou’reinterestedinlearningmore,IrecommendreadingtheofficialblogpostannouncementbyRichHickeyathttp://blog.cognitect.com/blog/2014/7/22/transit.
Next,weneedRxJS,which,beingaJavaScriptdependency,isn’tavailablevialeiningen.That’sOK.Wewillsimplydownloaditintotheapplicationoutputdirectory,aws-dash/dev-resources/public/js/:
$cdaws-dash/dev-resources/public/js/
$wgethttps://raw.githubusercontent.com/Reactive-
Extensions/RxJS/master/dist/rx.all.js
--2014-11-2318:00:21--https://raw.githubusercontent.com/Reactive-
Extensions/RxJS/master/dist/rx.all.js
Resolvingraw.githubusercontent.com…103.245.222.133
Connectingtoraw.githubusercontent.com|103.245.222.133|:443…connected.
HTTPrequestsent,awaitingresponse…200OK
Length:355622(347K)[text/plain]
Savingto:'rx.all.js'
100%[========================>]355,622966KB/sin0.4s
2014-11-2318:00:24(966KB/s)-'rx.all.js'saved[355622/355622]
Movingon,weneedtomakeourapplicationawareofournewdependencyonRxJS.Opentheaws-dash/dev-resources/public/index.htmlfileandaddascripttagtopullinRxJS:
<html>
<body>
<divid="app"></div>
<scriptsrc="http://fb.me/react-0.9.0.js"></script>
<scriptsrc="js/rx.all.js"></script>
<scriptsrc="js/aws_dash.js"></script>
</body>
</html>
Withallthedependenciesinplace,let’sstarttheauto-compilationforourClojureScriptsourcefilesasfollows:
$cdaws-dash/
$leincljsbuildauto
CompilingClojureScript.
Compiling"dev-resources/public/js/aws_dash.js"from("src/cljs""dev-
resources/tools/repl")...
Successfullycompiled"dev-resources/public/js/aws_dash.js"in0.981
seconds.
CreatingAWSObservablesWe’renowreadytostartimplementingoursolution.IfyourecallfromtheReactiveExtensionschapter,RxJava/RxJS/RxClojureshipwithseveralusefulObservables.However,whenthebuilt-inObservablesaren’tenough,itgivesusthetoolstobuildourown.
SinceitishighlyunlikelyRxJSalreadyprovidesObservablesforAmazon’sAWSAPI,wewillstartbyimplementingourownprimitiveObservables.
Tokeepthingsneat,wewilldothisinanewfile,underaws-dash/src/cljs/aws_dash/observables.cljs:
(nsaws-dash.observables
(:require-macros[cljs.core.async.macros:refer[go]])
(:require[cljs-http.client:ashttp]
[cljs.core.async:refer[<!]]
[cognitect.transit:ast]))
(defr(t/reader:json))
(defaws-endpoint"http://localhost:3001")
(defnaws-uri[path]
(straws-endpointpath))
Thenamespacedeclarationrequiresthenecessarydependencieswewillneedinthisfile.NotehowthereisnoexplicitdependencyonRxJS.SinceitisaJavaScriptdependencythatwemanuallypulledin,itisgloballyavailableforustouseviaJavaScriptinteroperability.
ThenextlinesetsupatransitreaderforJSON,whichwewillusewhenparsingthestubserverresponses.
Then,wedefinetheendpointwewillbetalkingtoaswellasahelperfunctiontobuildthecorrectURIs.Makesurethevariableaws-endpointmatchesthehostandportofthestubserverstartedintheprevioussection.
AllObservablesweareabouttocreatefollowacommonstructure:theymakearequesttothestubserver,extractsomeinformationfromtheresponse,optionallytransformingit,andthenemiteachiteminthetransformedsequenceintothenewObservablesequence.
Toavoidrepetition,thispatterniscapturedinthefollowingfunction:
(defnobservable-seq[uritransform]
(.createjs/Rx.Observable
(fn[observer]
(go(let[response(<!(http/geturi{:with-credentials?
false}))
data(t/readr(:bodyresponse))
transformed(transformdata)]
(doseq[xtransformed]
(.onNextobserverx))
(.onCompletedobserver)))
(fn[](.logjs/console"Disposed")))))
Let’sbreakthisfunctiondown:
observable-seqreceivestwoarguments:thebackendURItowhichwewillissueaGETrequest,andatransformfunctionwhichisgiventherawparsedJSONresponseandreturnsasequenceoftransformeditems.Then,itcallsthecreatefunctionoftheRxJSobjectRx.Observable.NotehowwemakeuseofJavaScriptinteroperabilityhere:weaccessthecreatefunctionbyprependingitwithadotmuchlikeinJavainteroperability.SinceRx.Observableisaglobalobject,weaccessitbyprependingtheglobalJavaScriptnamespaceClojureScriptmakesavailabletoourprogram,js/Rx.Observable.TheObservable’screatefunctionreceivestwoarguments.OneisafunctionthatgetscalledwithanObservertowhichwecanpushitemstobepublishedintheObservablesequence.ThesecondfunctionisafunctionthatiscalledwheneverthisObservableisdisposedof.Thisisthefunctionwherewecouldperformanycleanupneeded.Inourcase,thisfunctionsimplylogsthefactthatitiscalledtotheconsole.
Thefirstfunctionistheonethatinterestsusthough:
(fn[observer]
(go(let[response(<!(http/geturi
{:with-credentials?
false}))
data(t/readr(:bodyresponse))
transformed(transformdata)]
(doseq[xtransformed]
(.onNextobserverx))
(.onCompletedobserver))))
Assoonasitgetscalled,itperformsarequesttotheprovidedURIusingcljs-http’sgetfunction,whichreturnsacore.asyncchannel.That’swhythewholelogicisinsideagoblock.
Next,weusethetransitJSONreaderweconfiguredpreviouslytoparsethebodyoftheresponse,feedingtheresultintothetransformfunction.Rememberthisfunction,asperourdesign,returnsasequenceofthings.Therefore,allthatislefttodoispusheachitemintotheobserverinturn.
Oncewe’redone,weindicatethatthisObservablesequencewon’temitanynewitembyinvokingthe.onCompletedfunctionoftheobserverobject.
Now,wecanproceedcreatingourObservablesusingthishelperfunction,startingwiththeoneresponsibleforretrievingCloudFormationstacks:
(defndescribe-stacks[]
(observable-seq(aws-uri"/cloudFormation/describeStacks")
(fn[data]
(map(fn[stack]{:stack-id(stack"StackId")
:stack-name(stack"StackName")})
(data"Stacks")))))
Thiscreatesanobservablethatwillemitoneitemperstack,inthefollowingformat:
({:stack-id"arn:aws:cloudformation:ap-southeast-
2:337944750480:stack/DevStack-62031/1",:stack-name"DevStack-62031"})
Nowthatwehavestacks,weneedanObservabletodescribeitsresources:
(defndescribe-stack-resources[stack-name]
(observable-seq(aws-uri"/cloudFormation/describeStackResources")
(fn[data]
(map(fn[resource]
{:resource-id(resource"PhysicalResourceId")
:resource-type(resource"ResourceType")})
(data"StackResources")))))
Ithasasimilarpurposeandemitsresourceitemsinthefollowingformat:
({:resource-id"EC2123",:resource-type"AWS::EC2::Instance"}
{:resource-id"EC2456",:resource-type"AWS::EC2::Instance"}
{:resource-id"EC2789",:resource-type"AWS::EC2::Instance"}
{:resource-id"RDS123",:resource-type"AWS::RDS::DBInstance"}
{:resource-id"RDS456",:resource-type"AWS::RDS::DBInstance"})
Sincewe’refollowingourstrategyalmosttotheletter,weneedtwomoreobservables,oneforeachinstancetype:
(defndescribe-instances[instance-ids]
(observable-seq(aws-uri"/ec2/describeInstances")
(fn[data]
(let[instances(mapcat(fn[reservation]
(reservation"Instances"))
(data"Reservations"))]
(map(fn[instance]
{:instance-id(instance"InstanceId")
:type"EC2"
:status(get-ininstance["State"
"Name"])})
instances)))))
(defndescribe-db-instances[instance-id]
(observable-seq(aws-uri(str"/rds/describeDBInstances/"instance-id))
(fn[data]
(map(fn[instance]
{:instance-id(instance"DBInstanceIdentifier")
:type"RDS"
:status(instance"DBInstanceStatus")})
(data"DBInstances")))))
EachofwhichwillemitresourceitemsinthefollowingformatsforEC2andRDS,respectively:
({:instance-id"EC2123",:type"EC2",:status"running"}...)
({:instance-id"RDS123",:type"RDS",:status"available"}...)
CombiningtheAWSObservablesItseemswehaveallmajorpiecesinplacenow.Allthatislefttodoistocombinethemoreprimitive,basicObservableswejustcreatedintomorecomplexandusefulonesbycombiningthemtoaggregateallthedataweneedinordertorenderourdashboard.
Wewillstartbycreatingafunctionthatcombinesboththedescribe-stacksanddescribe-stack-resourcesObservables:
(defnstack-resources[]
(->(describe-stacks)
(.map#(:stack-name%))
(.flatMapdescribe-stack-resources)))
Startinginthepreviousexample,webegintoseehowdefiningourAPIcallsintermsofObservablesequencespaysoff:it’salmostsimplecombiningthesetwoObservablesinadeclarativemanner.
RemembertheroleofflatMap:asdescribe-stack-resourcesitselfreturnsanObservable,weuseflatMaptoflattenbothObservables,aswehavedonebeforeinvariousdifferentabstractions.
Thestack-resourcesObservablewillemitresourceitemsforallstacks.Accordingtoourplan,wewouldliketoforktheprocessinghereinordertoconcurrentlyretrieveEC2andRDSinstancedata.
Byfollowingthistrainofthought,wearriveattwomorefunctionsthatcombineandtransformthepreviousObservables:
(defnec2-instance-status[resources]
(->resources
(.filter#(=(:resource-type%)"AWS::EC2::Instance"))
(.map#(:resource-id%))
(.reduceconj[])
(.flatMapdescribe-instances)))
(defnrds-instance-status[resources]
(->resources
(.filter#(=(:resource-type%)"AWS::RDS::DBInstance"))
(.map#(:resource-id%))
(.flatMapdescribe-db-instances)))
Boththefunctionsreceiveanargument,resources,whichistheresultofcallingthestack-resourcesObservable.Thatway,weonlyneedtocallitonce.
Onceagain,itisfairlysimpletocombinetheObservablesinawaythatmakessense,followingourhigh-levelideadescribedpreviously.
Startingwithresources,wefilteroutthetypeswe’renotinterestedin,retrieveitsIDs,andrequestitsdetailedinformationbyflatmappingthedescribe-instancesanddescribe-db-instancesObservables.
Note,however,thatduetoalimitationintheRDSAPIdescribedearlier,wehavetocallit
multipletimestoretrieveinformationaboutallRDSinstances.
ThisseeminglyfundamentaldifferenceinhowweusetheAPIbecomesaminortransformationinourEC2observable,whichsimplyaccumulatesallIDsintoavectorsothatwecanretrievethemallatonce.
OursimpleReactiveAPItoAmazonAWSisnowcomplete,leavinguswiththeUItocreate.
PuttingitalltogetherLet’snowturntobuildingouruserinterface.It’sasimpleone,solet’sjustjumpintoit.Openupaws-dash/src/cljs/aws_dash/core.cljsandaddthefollowing:
(nsaws-dash.core
(:require[aws-dash.observables:asobs]
[om.core:asom:include-macrostrue]
[om.dom:asdom:include-macrostrue]))
(enable-console-print!)
(defapp-state(atom{:instances[]}))
(defninstance-view[{:keys[instance-idtypestatus]}owner]
(reify
om/IRender
(render[this]
(dom/trnil
(dom/tdnilinstance-id)
(dom/tdniltype)
(dom/tdnilstatus)))))
(defninstances-view[instancesowner]
(reify
om/IRender
(render[this]
(applydom/table#js{:style#js{:border"1pxsolidblack;"}}
(dom/trnil
(dom/thnil"Id")
(dom/thnil"Type")
(dom/thnil"Status"))
(om/build-allinstance-viewinstances)))))
(om/root
(fn[appowner]
(dom/divnil
(dom/h1nil"StackResourceStatuses")
(om/buildinstances-view(:instancesapp))))
app-state
{:target(.js/document(getElementById"app"))})
Ourapplicationstatecontainsasinglekey,:instances,whichstartsasanemptyvector.AswecanseefromeachOmcomponent,instanceswillberenderedasrowsinaHTMLtable.
Aftersavingthefile,makesurethewebserverisrunningbystartingitfromtheREPL:
leinrepl
CompilingClojureScript.
nREPLserverstartedonport58209onhost127.0.0.1-
nrepl://127.0.0.1:58209
REPL-y0.3.5,nREPL0.2.6
Clojure1.6.0
JavaHotSpot(TM)64-BitServerVM1.8.0_25-b17
Docs:(docfunction-name-here)
(find-doc"part-of-name-here")
Source:(sourcefunction-name-here)
Javadoc:(javadocjava-object-or-class-here)
Exit:Control+Dor(exit)or(quit)
Results:Storedinvars*1,*2,*3,anexceptionin*e
user=>(run)
2015-02-0821:02:34.503:INFO:oejs.Server:jetty-7.6.8.v20121106
2015-02-0821:02:34.545:INFO:oejs.AbstractConnector:Started
[email protected]:3000
#<Serverorg.eclipse.jetty.server.Server@35bc3669>
Youshouldnowbeablepointyourbrowsertohttp://localhost:3000/,but,asyoumighthaveguessed,youwillseenothingbutanemptytable.
Thisisbecausewehaven’tyetusedourReactiveAWSAPI.
Let’sfixitandbringitalltogetheratthebottomofcore.cljs:
(defresources(obs/stack-resources))
(.subscribe(->(.merge(obs/rds-instance-statusresources)
(obs/ec2-instance-statusresources))
(.reduceconj[]))
#(swap!app-stateassoc:instances%))
Yes,thisisallweneed!Wecreateastack-resourcesObservableandpassitasanargumenttobothrds-instance-statusandec2-instance-status,whichwillconcurrentlyretrievestatusinformationaboutallinstances.
Next,wecreateanewObservablebymergingtheprevioustwofollowedbyacallto.reduce,whichwillaccumulateallinformationintoavector,convenientforrendering.
Finally,wesimplysubscribetothisObservableand,whenitemitsitsresults,wesimplyupdateourapplicationstate,leavingOmtodoalltherenderingforus.
SavethefileandmakesureClojureScripthascompiledsuccessfully.Then,gobacktoyourbrowserathttp://localhost:3000/,andyoushouldseeallinstancestatuses,aspicturedatthebeginningofthischapter.
ExercisesWithourpreviousapproach,theonlywaytoseenewinformationabouttheAWSresourcesisbyrefreshingthewholepage.Modifyourimplementationinsuchawaythatitqueriesthestubserviceseverysooften—say,every500milliseconds.
TipTheintervalfunctionfromRxJScanbehelpfulinsolvingthisexercise.ThinkhowyoumightuseittogetherwithourexistingstreambyreviewinghowflatMapworks.
SummaryInthischapter,welookedatarealusecaseforReactiveapplications:buildingadashboardforAWSCloudFormationstacks.
Wehaveseenhowthinkingofalltheinformationneededasresources/itemsflowingthroughagraphfitsnicelywithhowonecreatesObservables.
Inaddition,bycreatingprimitiveObservablesthatdoonethingonlygivesusanicedeclarativewaytocombinethemintomorecomplexObservables,givingusadegreeofreusenotusuallyfoundwithcommontechniques.
Finally,wepackagedittogetherwithasimpleOm-basedinterfacetodemonstratehowusingdifferentabstractionsinthesameapplicationdoesnotaddtocomplexityaslongastheabstractionsarechosencarefullyfortheproblemathand.
ThisbringsustotheendofwhathopefullywasanenjoyableandinformativejourneythroughthedifferentwaysofReactiveProgramming.
Farfrombeingacompletereference,thisbookaimstoprovideyou,thereader,withenoughinformation,aswellasconcretetoolsandexamplesthatyoucanapplytoday.
Itisalsomyhopethatthereferencesandexercisesincludedinthisbookprovethemselvesuseful,shouldyouwishtoexpandyourknowledgeandseekoutmoredetails.
Lastly,IstronglyencourageyoutoturnthepageandreadtheAppendix,TheAlgebraofLibraryDesign,asItrulybelieveitwill,ifnothingelse,makeyouthinkhardabouttheimportanceofcompositioninprogramming.
Isincerelywishthisbookhasbeenasentertainingandinstructionaltoreadasitwastowrite.
Thankyouforreading.Ilookforwardtoseeingthegreatthingsyoubuild.
AppendixA.TheAlgebraofLibraryDesignYoumighthavenoticedthatallreactiveabstractionswehaveencounteredinthisbookhaveafewthingsincommon.Forone,theyworkas“container-like”abstractions:
FuturesencapsulateacomputationthateventuallyyieldsasinglevalueObservablesencapsulatecomputationsthatcanyieldmultiplevaluesovertimeintheshapeofastreamChannelsencapsulatevaluespushedtothemandcanhavethempoppedfromit,workingasaconcurrentqueuethroughwhichconcurrentprocessescommunicate
Then,oncewehavethis“container,”wecanoperateonitinanumberofways,whichareverysimilaracrossthedifferentabstractionsandframeworks:wecanfilterthevaluescontainedinthem,transformthemusingmap,combineabstractionsofthesametypeusingbind/flatMap/selectMany,executemultiplecomputationsinparallel,aggregatetheresultsusingsequence,andmuchmore.
Assuch,eventhoughtheabstractionsandtheirunderlyingworkingsarefundamentallydifferent,itstillfeelstheybelongtosometypeofhigher-levelabstractions.
Inthisappendix,wewillexplorewhatthesehigher-levelabstractionsare,therelationshipbetweenthem,andhowwecantakeadvantageoftheminourprojects.
ThesemanticsofmapWewillgetstartedbytakingalookatoneofthemostusedoperationsintheseabstractions:map.
We’vebeenusingmapforalongtimeinordertotransformsequences.Thus,insteadofcreatinganewfunctionnameforeachnewabstraction,librarydesignerssimplyabstractthemapoperationoveritsowncontainertype.
Imaginethemesswewouldendupinifwehadfunctionssuchastransform-observable,transform-channel,combine-futures,andsoon.
Thankfully,thisisnotthecase.Thesemanticsofmaparewellunderstoodtothepointthatevenifadeveloperhasn’tusedaspecificlibrarybefore,hewillalmostalwaysassumethatmapwillapplyafunctiontothevalue(s)containedwithinwhateverabstractionthelibraryprovides.
Let’slookatthreeexamplesweencounteredinthisbook.Wewillcreateanewleiningenprojectinwhichtoexperimentwiththecontentsofthisappendix:
$leinnewlibrary-design
Next,let’saddafewdependenciestoourproject.cljfile:
...
:dependencies[[org.clojure/clojure"1.6.0"]
[com.leonardoborges/imminent"0.1.0"]
[com.netflix.rxjava/rxjava-clojure"0.20.7"]
[org.clojure/core.async"0.1.346.0-17112a-alpha"]
[uncomplicate/fluokitten"0.3.0"]]
...
Don’tworryaboutthelastdependency—we’llgettoitlateron.
Now,startanREPLsessionsothatwecanfollowalong:
$leinrepl
Then,enterthefollowingintoyourREPL:
(require'[imminent.core:asi]
'[rx.lang.clojure.core:asrx]
'[clojure.core.async:asasync])
(defrepl-out*out*)
(defnprn-to-repl[&args]
(binding[*out*repl-out]
(applyprnargs)))
(->(i/const-future31)
(i/map#(*%2))
(i/on-success#(prn-to-repl(str"Value:"%))))
(as->(rx/return31)obs
(rx/map#(*%2)obs)
(rx/subscribeobs#(prn-to-repl(str"Value:"%))))
(defc(chan))
(defmapped-c(async/map<#(*%2)c))
(async/go(async/>!c31))
(async/go(prn-to-repl(str"Value:"(async/<!mapped-c))))
"Value:62"
"Value:62"
"Value:62"
Thethreeexamples—usingimminent,RxClojure,andcore.async,respectively—lookremarkablysimilar.Theyallfollowasimplerecipe:
1. Putthenumber31insidetheirrespectiveabstraction.2. Doublethatnumberbymappingafunctionovertheabstraction.3. PrintitsresulttotheREPL.
Asexpected,thisoutputsthevalue62threetimestothescreen.
Itwouldseemmapperformsthesameabstractstepsinallthreecases:itappliestheprovidedfunction,putstheresultingvalueinafreshnewcontainer,andreturnsit.Wecouldcontinuegeneralizing,butwewouldjustberediscoveringanabstractionthatalreadyexists:Functors.
FunctorsFunctorsarethefirstabstractionwewilllookatandtheyarerathersimple:theydefineasingleoperationcalledfmap.InClojure,Functorscanberepresentedusingprotocolsandareusedforcontainersthatcanbemappedover.Suchcontainersinclude,butarenotlimitedto,lists,Futures,Observables,andchannels.
TipTheAlgebrainthetitleofthisAppendixreferstoAbstractAlgebra,abranchofMathematicsthatstudiesalgebraicstructures.Analgebraicstructureis,toputitsimply,asetwithoneormoreoperationsdefinedonit.
Asanexample,considerSemigroups,whichisonesuchalgebraicstructure.Itisdefinedtobeasetofelementstogetherwithanoperationthatcombinesanytwoelementsofthisset.Therefore,thesetofpositiveintegerstogetherwiththeadditionoperationformaSemigroup.
AnothertoolusedforstudyingalgebraicstructuresiscalledCategoryTheory,ofwhichFunctorsarepartof.
Wewon’tdelvetoomuchintothetheorybehindallthis,asthereareplentyofbooks[9][10]availableonthesubject.Itwas,however,anecessarydetourtoexplainthetitleusedinthisappendix.
DoesthismeanalloftheseabstractionsimplementaFunctorprotocol?Unfortunately,thisisnotthecase.AsClojureisadynamiclanguageanditdidn’thaveprotocolsbuiltin—theywereaddedinversion1.2ofthelanguage—theseframeworkstendtoimplementtheirownversionofthemapfunction,whichdoesn’tbelongtoanyprotocolinparticular.
Theonlyexceptionisimminent,whichimplementstheprotocolsincludedinfluokitten,aClojurelibraryprovidingconceptsfromCategorytheorysuchasFunctors.
ThisisasimplifiedversionoftheFunctorprotocolfoundinfluokitten:
(defprotocolFunctor
(fmap[fvg]))
Asmentionedpreviously,Functorsdefineasingleoperation.fmapappliesthefunctiongtowhatevervalueisinsidethecontainer,Functor,fv.
However,implementingthisprotocoldoesnotguaranteethatwehaveactuallyimplementedaFunctor.Thisisbecause,inadditiontoimplementingtheprotocol,Functorsarealsorequiredtoobeyacoupleoflaws,whichwewillexaminebriefly.
Theidentitylawisasfollows:
(=(fmapa-functoridentity)
(identitya-functor))
Theprecedingcodeisallweneedtoverifythislaw.Itsimplysaysthatmappingtheidentityfunctionovera-functoristhesameassimplyapplyingtheidentityfunction
totheFunctoritself.
Thecompositionlawisasfollows:
(=(fmapa-functor(compfg))
(fmap(fmapa-functorg)f))
Thecompositionlaw,inturn,saysthatifwecomposetwoarbitraryfunctionsfandg,taketheresultingfunctionandapplythattoa-functor,thatisthesameasmappinggovertheFunctorandthenmappingfovertheresultingFunctor.
Noamountoftextwillbeabletoreplacepracticalexamples,sowewillimplementourownFunctor,whichwewillcallOption.Wewillthenrevisitthelawstoensurewehaverespectedthem.
TheOptionFunctorAsTonyHoareonceputit,nullreferencesarehisonebilliondollarmistake(http://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare).Regardlessofbackground,younodoubtwillhaveencounteredversionsofthedreadfulNullPointerException.Thisusuallyhappenswhenwetrytocallamethodonanobjectreferencethatisnull.
ClojureembracesnullvaluesduetoitsinteroperabilitywithJava,itshostlanguage,butitprovidesimprovedsupportfordealingwiththem.
Thecorelibraryispackedwithfunctionsthatdotherightthingifpassedanilvalue—Clojure’sversionofJava’snull.Forinstance,howmanyelementsarethereinanilsequence?
(countnil);;0
Thankstoconsciousdesigndecisionsregardingnil,wecan,forthemostpart,affordnotworryaboutit.Forallothercases,theOptionFunctormightbeofsomehelp.
Theremainingoftheexamplesinthisappendixshouldbeinafilecalledoption.cljunderlibrary-design/src/library_design/.You’rewelcometotrythisintheREPLaswell.
Let’sstartournextexamplebyaddingthenamespacedeclarationaswellasthedatawewillbeworkingwith:
(nslibrary-design.option
(:require[uncomplicate.fluokitten.protocols:asfkp]
[uncomplicate.fluokitten.core:asfkc]
[uncomplicate.fluokitten.jvm:asfkj]
[imminent.core:asI]))
(defpirates[{:name"JackSparrow":born1700:died1740:ship"Black
Pearl"}
{:name"Blackbeard":born1680:died1750:ship"Queen
Anne'sRevenge"}
{:name"HectorBarbossa":born1680:died1740:shipnil}])
(defnpirate-by-name[name]
(->>pirates
(filter#(=name(:name%)))
first))
(defnage[{:keys[borndied]}]
(-diedborn))
AsaPiratesoftheCaribbeanfan,Ithoughtitwouldbeinterestingtoplaywithpiratesforthisexample.Let’ssaywewouldliketocalculateJackSparrow’sage.Giventhedataandfunctionswejustcovered,thisisasimpletask:
(->(pirate-by-name"JackSparrow")
age);;40
However,whatifwewouldliketoknowDavyJones’age?Wedon’tactuallyhaveanydataforthispirate,soifwerunourprogramagain,thisiswhatwe’llget:
(->(pirate-by-name"DavyJones")
age);;NullPointerExceptionclojure.lang.Numbers.ops
(Numbers.java:961)
Thereitis.ThedreadfulNullPointerException.Thishappensbecauseintheimplementationoftheagefunction,weenduptryingtosubtracttwonilvalues,whichisincorrect.Asyoumighthaveguessed,wewillattempttofixthisbyusingtheOptionFunctor.
Traditionally,Optionisimplementedasanalgebraicdatatype,morespecificallyasumtypewithtwovariants:SomeandNone.Thesevariantsareusedtoidentifywhetheravalueispresentornotwithoutusingnils.YoucanthinkofbothSomeandNoneassubtypesofOption.
InClojure,wewillrepresentthemusingrecords:
(defrecordSome[v])
(defrecordNone[])
(defnoption[v]
(ifv
(Some.v)
(None.)))
Aswecansee,SomecancontainasinglevaluewhereasNonecontainsnothing.It’ssimplyamarkerindicatingtheabsenceofcontent.Wehavealsocreatedahelperfunctioncalledoption,whichcreatestheappropriaterecorddependingonwhetheritsargumentisnilornot.
ThenextstepistoextendtheFunctorprotocoltobothrecords:
(extend-protocolfkp/Functor
Some
(fmap[fg]
(Some.(g(:vf))))
None
(fmap[__]
(None.)))
Here’swherethesemanticmeaningoftheOptionFunctorbecomesapparent:asSomecontainsavalue,itsimplementationoffmapsimplyappliesthefunctiongtothevalueinsidetheFunctorf,whichisoftypeSome.Finally,weputtheresultinsideanewSomerecord.
NowwhatdoesitmeantomapafunctionoveraNone?Youprobablyguessedthatitdoesn’treallymakesense—theNonerecordholdsnovalues.TheonlythingwecandoisreturnanotherNone.Aswewillseeshortly,thisgivestheOptionFunctorashort-circuitingsemantic.
TipInthefmapimplementationofNone,wecouldhavereturnedareferencetothisinsteadofanewrecordinstance.I’venotdonesosimplytomakeitclearthatweneedtoreturnaninstanceofNone.
Nowthatwe’veimplementedtheFunctorprotocol,wecantryitout:
(->>(option(pirate-by-name"JackSparrow"))
(fkc/fmapage));;#library_design.option.Some{:v40}
(->>(option(pirate-by-name"DavyJones"))
(fkc/fmapage));;#library_design.option.None{}
Thefirstexampleshouldn’tholdanysurprises.Weconvertthepiratemapwegetfromcallingpirate-by-nameintoanoption,andthenfmaptheagefunctionoverit.
Thesecondexampleistheinterestingone.Asstatedpreviously,wehavenodataaboutDavyJones.However,mappingageoveritdoesnotthrowanexceptionanylonger,insteadreturningNone.
Thismightseemlikeasmallbenefit,butthebottomlineisthattheOptionFunctormakesitsafetochainoperationstogether:
(->>(option(pirate-by-name"JackSparrow"))
(fkc/fmapage)
(fkc/fmapinc)
(fkc/fmap#(*2%)));;#library_design.option.Some{:v82}
(->>(option(pirate-by-name"DavyJones"))
(fkc/fmapage)
(fkc/fmapinc)
(fkc/fmap#(*2%)));;#library_design.option.None{}
Atthispoint,somereadersmightbethinkingaboutthesome->macro—introducedinClojure1.5—andhowiteffectivelyachievesthesameresultastheOptionFunctor.Thisintuitioniscorrectasdemonstratedasfollows:
(some->(pirate-by-name"DavyJones")
age
inc
(*2));;nil
Thesome->macrothreadstheresultofthefirstexpressionthroughthefirstformifitisnotnil.Then,iftheresultofthatexpressionisn’tnil,itthreadsitthroughthenextformandsoon.Assoonasanyoftheexpressionsevaluatestonil,some->short-circuitsandreturnsnilimmediately.
Thatbeingsaid,Functorisamuchmoregeneralconcept,soaslongasweareworkingwiththisconcept,ourcodedoesn’tneedtochangeasweareoperatingatahigherlevelofabstraction:
(->>(i/future(pirate-by-name"JackSparrow"))
(fkc/fmapage)
(fkc/fmapinc)
(fkc/fmap#(*2%)));;#<Future@30518bfc:#<Success@39bd662c:82>>
Intheprecedingexample,eventhoughweareworkingwithafundamentallydifferenttool—futures—thecodeusingtheresultdidnothavetochange.ThisisonlypossiblebecausebothOptionsandfuturesareFunctorsandimplementthesameprotocolprovidedbyfluokitten.WehavegainedcomposabilityandsimplicityaswecanusethesameAPItoworkwithvariousdifferentabstractions.
Speakingofcomposability,thispropertyisguaranteedbythesecondlawofFunctors.Let’sseeifourOptionFunctorrespectsthisandthefirst—theidentity—laws:
;;Identity
(=(fkc/fmapidentity(option1))
(identity(option1)));;true
;;Composition
(=(fkc/fmap(compidentityinc)(option1))
(fkc/fmapidentity(fkc/fmapinc(option1))));;true
Andwe’redone,ourOptionFunctorisalawfulcitizen.Theremainingtwoabstractionsalsocomepairedwiththeirownlaws.Wewillnotcoverthelawsinthissection,butIencouragethereadertoreadaboutthem(http://www.leonardoborges.com/writings/2012/11/30/monads-in-small-bites-part-i-functors/).
FindingtheaverageofagesInthissection,wewillexploreadifferentusecasefortheOptionFunctor.Wewouldliketo,givenanumberofpirates,calculatetheaverageoftheirages.Thisissimpleenoughtodo:
(defnavg[&xs]
(float(/(apply+xs)(countxs))))
(let[a(some->(pirate-by-name"JackSparrow")age)
b(some->(pirate-by-name"Blackbeard")age)
c(some->(pirate-by-name"HectorBarbossa")age)]
(avgabc));;56.666668
Notehowweareusingsome->heretoprotectusfromnilvalues.Now,whathappensifthereisapirateforwhichwehavenoinformation?
(let[a(some->(pirate-by-name"JackSparrow")age)
b(some->(pirate-by-name"DavyJones")age)
c(some->(pirate-by-name"HectorBarbossa")age)]
(avgabc));;NullPointerExceptionclojure.lang.Numbers.ops
(Numbers.java:961)
Itseemswe’rebackatsquareone!It’sworsenowbecauseusingsome->doesn’thelpifweneedtouseallvaluesatonce,asopposedtothreadingthemthroughachainoffunctioncalls.
Ofcourse,notallislost.Allweneedtodoischeckifallvaluesarepresentbeforecalculatingtheaverage:
(let[a(some->(pirate-by-name"JackSparrow")age)
b(some->(pirate-by-name"DavyJones")age)
c(some->(pirate-by-name"HectorBarbossa")age)]
(when(andabc)
(avgabc)));;nil
Whilethisworksperfectlyfine,ourimplementationsuddenlyhadtobecomeawarethatanyorallofthevaluesa,b,andccouldbenil.Thenextabstractionwewilllookat,ApplicativeFunctors,fixesthis.
ApplicativeFunctorsLikeFunctors,ApplicativeFunctorsareasortofcontaineranddefinestwooperations:
(defprotocolApplicative
(pure[avv])
(fapply[agav]))
ThepurefunctionisagenericwaytoputavalueinsideanApplicativeFunctor.Sofar,wehavebeenusingtheoptionhelperfunctionforthispurpose.Wewillbeusingitalittlelater.
ThefapplyfunctionwillunwrapthefunctioncontainedintheApplicativeagandapplyittothevaluecontainedintheapplicativeav.
Thepurposeofboththefunctionswillbecomeclearwithanexample,butfirst,weneedtopromoteourOptionFunctorintoanApplicativeFunctor:
(extend-protocolfkp/Applicative
Some
(pure[_v]
(Some.v))
(fapply[agav]
(if-let[v(:vav)]
(Some.((:vag)v))
(None.)))
None
(pure[_v]
(Some.v))
(fapply[agav]
(None.)))
Theimplementationofpureisthesimplest.AllitdoesiswrapthevaluevintoaninstanceofSome.EquallysimpleistheimplementationoffapplyforNone.Asthereisnovalue,wesimplyreturnNoneagain.
ThefapplyimplementationofSomeensuresbothargumentshaveavalueforthe:vkeyword—strictlyspeakingtheybothhavetobeinstancesofSome.If:visnon-nil,itappliesthefunctioncontainedinagtov,finallywrappingtheresult.Otherwise,itreturnsNone.
ThisshouldbeenoughtotryourfirstexampleusingtheApplicativeFunctorAPI:
(fkc/fapply(optioninc)(option2))
;;#library_design.option.Some{:v3}
(fkc/fapply(optionnil)(option2))
;;#library_design.option.None{}
WearenowabletoworkwithFunctorsthatcontainfunctions.Additionally,wehavealsopreservedthesemanticsofwhatshouldhappenwhenanyoftheFunctorsdon’thavea
value.
Wecannowrevisittheageaverageexamplefrombefore:
(defage-option(comp(partialfkc/fmapage)optionpirate-by-name))
(let[a(age-option"JackSparrow")
b(age-option"Blackbeard")
c(age-option"HectorBarbossa")]
(fkc/<*>(option(fkj/curryavg3))
abc))
;;#library_design.option.Some{:v56.666668}
TipThevarargfunction<*>isdefinedbyfluokittenandperformsaleft-associativefapplyonitsarguments.Essentially,itisaconveniencefunctionthatmakes(<*>fgh)equivalentto(fapply(fapplyfg)h).
Westartbydefiningahelperfunctiontoavoidrepetition.Theage-optionfunctionretrievestheageofapirateasanoptionforus.
Next,wecurrytheavgfunctionto3argumentsandputitintoanoption.Then,weusethe<*>functiontoapplyittotheoptionsa,b,andc.Wegettothesameresult,buthavetheApplicativeFunctortakecareofnilvaluesforus.
TipFunctioncurrying
Curryingisthetechniqueoftransformingafunctionofmultipleargumentsintoahigher-orderfunctionofasingleargumentthatreturnsmoresingle-argumentfunctionsuntilallargumentshavebeensupplied.
Roughlyspeaking,curryingmakesthefollowingsnippetsequivalent:
(defcurried-1(fkj/curry+2))
(defcurried-2(fn[a]
(fn[b]
(+ab))))
((curried-110)20);;30
((curried-210)20);;30
UsingApplicativeFunctorsthiswayissocommonthatthepatternhasbeencapturedasthefunctionalift,asshowninthefollowing:
(defnalift
"Liftsan-aryfunction`f`intoaapplicativecontext"
[f]
(fn[&as]
{:pre[(seqas)]}
(let[curried(fkj/curryf(countas))]
(applyfkc/<*>
(fkc/fmapcurried(firstas))
(restas)))))
ThealiftfunctionisresponsibleforliftingafunctioninsuchawaythatitcanbeusedwithApplicativeFunctorswithoutmuchceremony.BecauseoftheassumptionsweareabletomakeaboutApplicativeFunctors—forinstance,thatitisalsoaFunctor—wecanwritegenericcodethatcanbere-usedacrossanyApplicatives.
Withaliftinplace,ourageaverageexampleturnsintothefollowing:
(let[a(age-option"JackSparrow")
b(age-option"Blackbeard")
c(age-option"HectorBarbossa")]
((aliftavg)abc))
;;#library_design.option.Some{:v56.666668}
WeliftavgintoanApplicativecompatibleversion,makingthecodelookremarkablylikesimplefunctionapplication.Andsincewearenotdoinganythinginterestingwiththeletbindings,wecansimplifyitfurtherasfollows:
((aliftavg)(age-option"JackSparrow")
(age-option"Blackbeard")
(age-option"HectorBarbossa"))
;;#library_design.option.Some{:v56.666668}
((aliftavg)(age-option"JackSparrow")
(age-option"DavyJones")
(age-option"HectorBarbossa"))
;;#library_design.option.None{}
AswithFunctors,wecantakethecodeasitis,andsimplyreplacetheunderlyingabstraction,preventingrepetitiononceagain:
((aliftavg)(i/future(some->(pirate-by-name"JackSparrow")age))
(i/future(some->(pirate-by-name"Blackbeard")age))
(i/future(some->(pirate-by-name"HectorBarbossa")age)))
;;#<Future@17b1be96:#<Success@16577601:56.666668>>
GatheringstatsaboutagesNowthatwecansafelycalculatetheaverageageofanumberofpirates,itmightbeinterestingtotakethisfurtherandcalculatethemedianandstandarddeviationofthepirates’ages,inadditiontotheiraverageage.
Wealreadyhaveafunctiontocalculatetheaverage,solet’sjustcreatetheonestocalculatethemedianandthestandarddeviationofalistofnumbers:
(defnmedian[&ns]
(let[ns(sortns)
cnt(countns)
mid(bit-shift-rightcnt1)]
(if(odd?cnt)
(nthnsmid)
(/(+(nthnsmid)(nthns(decmid)))2))))
(defnstd-dev[&samples]
(let[n(countsamples)
mean(/(reduce+samples)n)
intermediate(map#(Math/pow(-%1mean)2)samples)]
(Math/sqrt
(/(reduce+intermediate)n))))
Withthesefunctionsinplace,wecanwritethecodethatwillgatherallthestatsforus:
(let[a(some->(pirate-by-name"JackSparrow")age)
b(some->(pirate-by-name"Blackbeard")age)
c(some->(pirate-by-name"HectorBarbossa")age)
avg(avgabc)
median(medianabc)
std-dev(std-devabc)]
{:avgavg
:medianmedian
:std-devstd-dev})
;;{:avg56.666668,
;;:median60,
;;:std-dev12.472191289246473}
Thisimplementationisfairlystraightforward.Wefirstretrieveallageswe’reinterestedinandbindthemtothelocalsa,b,andc.Wethenreusethevalueswhencalculatingtheremainingstats.Wefinallygatherallresultsinamapforeasyaccess.
Bynowthereaderwillprobablyknowwherewe’reheaded:whatifanyofthosevaluesisnil?
(let[a(some->(pirate-by-name"JackSparrow")age)
b(some->(pirate-by-name"DavyJones")age)
c(some->(pirate-by-name"HectorBarbossa")age)
avg(avgabc)
median(medianabc)
std-dev(std-devabc)]
{:avgavg
:medianmedian
:std-devstd-dev})
;;NullPointerExceptionclojure.lang.Numbers.ops(Numbers.java:961)
Thesecondbinding,b,returnsnil,aswedon’thaveanyinformationaboutDavyJones.Assuch,itcausesthecalculationstofail.Likebefore,wecanchangeourimplementationtoprotectusfromsuchfailures:
(let[a(some->(pirate-by-name"JackSparrow")age)
b(some->(pirate-by-name"DavyJones")age)
c(some->(pirate-by-name"HectorBarbossa")age)
avg(when(andabc)(avgabc))
median(when(andabc)(medianabc))
std-dev(when(andabc)(std-devabc))]
(when(andabc)
{:avgavg
:medianmedian
:std-devstd-dev}))
;;nil
Thistimeit’sevenworsethanwhenweonlyhadtocalculatetheaverage;thecodeischeckingfornilvaluesinfourextraspots:beforecallingthethreestatsfunctionsandjustbeforegatheringthestatsintotheresultmap.
Canwedobetter?
MonadsOurlastabstractionwillsolvetheveryproblemweraisedintheprevioussection:howtosafelyperformintermediatecalculationsbypreservingthesemanticsoftheabstractionswe’reworkingwith—inthiscase,options.
ItshouldbenosurprisenowthatfluokittenalsoprovidesaprotocolforMonads,simplifiedandshownasfollows:
(defprotocolMonad
(bind[mvg]))
Ifyouthinkintermsofaclasshierarchy,Monadswouldbeatthebottomofit,inheritingfromApplicativeFunctors,which,inturn,inheritfromFunctors.Thatis,ifyou’reworkingwithaMonad,youcanassumeitisalsoanApplicativeandaFunctor.
Thebindfunctionofmonadstakesafunctiongasitssecondargument.ThisfunctionreceivesasinputthevaluecontainedinmvandreturnsanotherMonadcontainingitsresult.Thisisacrucialpartofthecontract:ghastoreturnaMonad.
Thereasonwhywillbecomecleareraftersomeexamples.Butfirst,let’spromoteourOptionabstractiontoaMonad—atthispoint,OptionisalreadyanApplicativeFunctorandaFunctor:
(extend-protocolfkp/Monad
Some
(bind[mvg]
(g(:vmv)))
None
(bind[__]
(None.)))
Theimplementationisfairlysimple.IntheNoneversion,wecan’treallydoanything,sojustlikewehavebeendoingsofar,wereturnaninstanceofNone.
TheSomeimplementationextractsthevaluefromtheMonadmvandappliesthefunctiongtoit.Notehowthistimewedon’tneedtowraptheresultasthefunctiongalreadyreturnsaMonadinstance.
UsingtheMonadAPI,wecouldsumtheagesofourpiratesasfollows:
(defopt-ctx(None.))
(fkc/bind(age-option"JackSparrow")
(fn[a]
(fkc/bind(age-option"Blackbeard")
(fn[b]
(fkc/bind(age-option"HectorBarbossa")
(fn[c]
(fkc/pureopt-ctx
(+abc))))))))
;;#library_design.option.Some{:v170.0}
Firstly,wearemakinguseofApplicative’spurefunctionintheinner-mostfunction.RememberthatroleofpureistoprovideagenericwaytoputavalueintoanApplicativeFunctor.SinceMonadsarealsoApplicative,wemakeuseofthemhere.
However,sinceClojureisadynamicallytypedlanguage,weneedtohintpurewiththecontext—container—typewewishtouse.ThiscontextissimplyaninstanceofeitherSomeorNone.Theybothhavethesamepureimplementation.
Whilewedogettherightanswer,theprecedingexampleisfarfromwhatwewouldliketowriteduetoitsexcessivenesting.Itisalsohardtoread.
Thankfully,fluokittenprovidesamuchbetterwaytowritemonadiccode,calledthedo-notation:
(fkc/mdo[a(age-option"JackSparrow")
b(age-option"Blackbeard")
c(age-option"HectorBarbossa")]
(fkc/pureopt-ctx(+abc)))
;;#library_design.option.Some{:v170.0}
Suddenly,thesamecodebecomesalotcleanerandeasiertoread,withoutlosinganyofthesemanticsoftheOptionMonad.Thisisbecausemdoisamacrothatexpandstothecodeequivalentofthenestedversion,aswecanverifybyexpandingthemacroasfollows:
(require'[clojure.walk:asw])
(w/macroexpand-all'(fkc/mdo[a(age-option"JackSparrow")
b(age-option"Blackbeard")
c(age-option"HectorBarbossa")]
(option(+abc))))
;;(uncomplicate.fluokitten.core/bind
;;(age-option"JackSparrow")
;;(fn*
;;([a]
;;(uncomplicate.fluokitten.core/bind
;;(age-option"Blackbeard")
;;(fn*
;;([b]
;;(uncomplicate.fluokitten.core/bind
;;(age-option"HectorBarbossa")
;;(fn*([c](fkc/pureopt-ctx(+abc)))))))))))
TipItisimportanttostopforamomenthereandappreciatethepowerofClojure—andLispingeneral.
LanguagessuchasHaskellandScala,whichmakeheavyuseofabstractionssuchasFunctors,Applicative,andMonads,alsohavetheirownversionsofthedo-notation.However,thissupportisbakedintothecompileritself.
Asanexample,whenHaskelladdeddo-notationtothelanguage,anewversionofthecompilerwasreleased,anddeveloperswishingtousethenewfeaturehadtoupgrade.
InClojure,ontheotherhand,thisnewfeaturecanbeshippedasalibraryduetothepowerandflexibilityofmacros.Thisisexactlywhatfluokittenhasdone.
Now,wearereadytogobacktoouroriginalproblem,gatheringstatsaboutthepirates’ages.
First,wewilldefineacoupleofhelperfunctionsthatconverttheresultofourstatsfunctionsintotheOptionMonad:
(defavg-opt(compoptionavg))
(defmedian-opt(compoptionmedian))
(defstd-dev-opt(compoptionstd-dev))
Here,wetakeadvantageoffunctioncompositiontocreatemonadicversionsofexistingfunctions.
Next,wewillrewriteoursolutionusingthemonadicdo-notationwelearnedearlier:
(fkc/mdo[a(age-option"JackSparrow")
b(age-option"Blackbeard")
c(age-option"HectorBarbossa")
avg(avg-optabc)
median(median-optabc)
std-dev(std-dev-optabc)]
(option{:avgavg
:medianmedian
:std-devstd-dev}))
;;#library_design.option.Some{:v{:avg56.666668,
;;:median60,
;;:std-dev12.472191289246473}}
Thistimewewereabletowritethefunctionaswenormallywould,withouthavingtoworryaboutwhetheranyvaluesintheintermediatecomputationsareemptyornot.ThissemanticthatistheveryessenceoftheOptionMonadisstillpreserved,ascanbeseeninthefollowing:
(fkc/mdo[a(age-option"JackSparrow")
b(age-option"Blackbeard")
c(age-option"HectorBarbossa")
avg(avg-optabc)
median(median-optabc)
std-dev(std-dev-optabc)]
(fkc/pureopt-ctx{:avgavg
:medianmedian
:std-devstd-dev}))
;;#library_design.option.None{}
Forthesakeofcompleteness,wewillusefuturestodemonstratehowthedo-notationworksforanyMonad:
(defavg-fut(compi/future-callavg))
(defmedian-fut(compi/future-callmedian))
(defstd-dev-fut(compi/future-callstd-dev))
(fkc/mdo[a(i/future(some->(pirate-by-name"JackSparrow")age))
b(i/future(some->(pirate-by-name"Blackbeard")age))
c(i/future(some->(pirate-by-name"HectorBarbossa")
age))
avg(avg-futabc)
median(median-futabc)
std-dev(std-dev-futabc)]
(i/const-future{:avgavg
:medianmedian
:std-devstd-dev}))
;;#<Future@3fd0b0d0:#<Success@1e08486b:{:avg56.666668,
;;:median60,
;;:std-dev12.472191289246473}>>
SummaryThisappendixhastakenusonabrieftouroftheworldofcategorytheory.Welearnedthreeofitsabstractions:Functors,ApplicativeFunctors,andMonads.Theyweretheguidingprinciplebehindimminent’sAPI.
Todeepenourknowledgeandunderstanding,weimplementedourownOptionMonad,acommonabstractionusedtosafelyhandletheabsenceofvalues.
Wehavealsoseenthatusingtheseabstractionsallowustomakesomeassumptionsaboutourcode,asseeninfunctionssuchasalift.Therearemanyotherfunctionswewouldnormallyrewriteoverandoveragainfordifferentpurposes,butthatcanbereusedifwerecognizeourcodefitsintooneoftheabstractionslearned.
Finally,Ihopethisencouragesreaderstoexplorecategorytheorymore,asitwillundoubtedlychangethewayyouthink.AndifIcanbesobold,Ihopethiswillalsochangethewayyoudesignlibrariesinthefuture.
AppendixB.Bibliography[1]RenePardoandRemyLandau,TheWorld’sFirstElectronicSpreadsheet:http://www.renepardo.com/articles/spreadsheet.pdf
[2]ConalElliottandPaulHudak,FunctionalReactiveAnimation:http://conal.net/papers/icfp97/icfp97.pdf
[3]EvanCzaplicki,Elm:ConcurrentFRPforFunctionalGUIs:http://elm-lang.org/papers/concurrent-frp.pdf
[4]ErikMeijer,Subject/ObserverisDualtoIterator:http://csl.stanford.edu/~christos/pldi2010.fit/meijer.duality.pdf
[5]HenrikNilsson,AntonyCourtneyandJohnPeterson,FunctionalReactiveProgramming,Continued:http://haskell.cs.yale.edu/wp-content/uploads/2011/02/workshop-02.pdf
[6]JohnHughes,GeneralisingMonadstoArrows:http://www.cse.chalmers.se/~rjmh/Papers/arrows.pdf
[7]ZhanyongWan,WalidTahaandPaulHudak,Real-TimeFRP:http://haskell.cs.yale.edu/wp-content/uploads/2011/02/rt-frp.pdf
[8]WalidTaha,ZhanyongWan,andPaulHudak,Event-DrivenFRP:http://www.cs.yale.edu/homes/zwan/papers/mcu/efrp.pdf
[9]BenjaminC.Pierce,BasicCategoryTheoryforComputerScientists:http://www.amazon.com/Category-Computer-Scientists-Foundations-Computing-ebook/dp/B00MG7E5WE/ref=sr_1_7?ie=UTF8&qid=1423484917&sr=8-7&keywords=category+theory
[10]SteveAwodey,CategoryTheory(OxfordLogicGuides):http://www.amazon.com/Category-Theory-Oxford-Logic-Guides/dp/0199237182/ref=sr_1_2?ie=UTF8&qid=1423484917&sr=8-2&keywords=category+theory
[11]DuncanCoutts,RomanLeshchinskiy,andDonStewart,StreamFusion:http://code.haskell.org/~dons/papers/icfp088-coutts.pdf
[12]PhilipWadler,Transformingprogramstoeliminatetrees:http://homepages.inf.ed.ac.uk/wadler/papers/deforest/deforest.ps
IndexA
AbstractAlgebraabout/Functors
agileboardcreating,withOm/CreatinganagileboardwithOmstate/Theboardstatecomponents/Componentsoverviewlifecycle/Lifecycleandcomponentlocalstatecomponentlocalstate/Lifecycleandcomponentlocalstatemultiplecolumn-viewcomponents,creating/Remainingcomponentsutilityfunctions,adding/Utilityfunctions
ApplicativeFunctorsabout/ApplicativeFunctorspurefunction/ApplicativeFunctorsfapplyfunction/ApplicativeFunctorsvarargfunction/ApplicativeFunctorsstats,calculatingofages/Gatheringstatsaboutages
ArrowizedFRPabout/ArrowizedFRP
Asteroidsabout/Settinguptheproject
asynchronousdataflowabout/Asynchronousdataflow
asynchronousprogrammingabout/AsynchronousCompositionalEventSystem(CES)aboutprogrammingandconcurrency
AutomatonURL/ArrowizedFRP
AWSusing/InfrastructureautomationEC2/InfrastructureautomationRDS/InfrastructureautomationCloudFormation/Infrastructureautomation
AWSresourcesdashboardbuilding/AWSresourcesdashboardbuilding,withCloudFormation/CloudFormationbuilding,withEC2/EC2building,withRDS/RDSdesigning/Designingthesolutionstubserver,settingup/RunningtheAWSstubserversettingup/Settingupthedashboardproject
Observables,creating/CreatingAWSObservablesObservables,combining/CombiningtheAWSObservablesuserinterface,building/Puttingitalltogether
Bbackpressure
about/Backpressuresamplecombinator/Samplestrategies/Backpressurestrategiesreferencelink/Backpressurestrategies
Bacon.jsURL/FunctionalReactiveProgrammingabout/Asynchronousdataflow
blockingIOabout/FuturesandblockingIO
bufferingabout/Backpressurefixedbuffer/Fixedbufferdroppingbuffer/Droppingbufferslidingbuffer/Slidingbuffer
Ccatchcombinator
about/CatchCES
versusFRP/Asynchronousdataflowversuscore.async/CESversuscore.async
ChestnutURL/AtasteofReactiveProgramming
cljs-startURL/Arespondentapplication,Settinguptheproject
cljxURL/ClojureorClojureScript?about/ClojureorClojureScript?
ClojureURL,fordocumentation/Applicationcomponentsfutures/Clojurefutures
Clojurescriptabout/ClojureScriptandOm
ClojureScriptgameproject,settingup/Settinguptheprojectgameentities,creating/Gameentitiesimplementing/Puttingitalltogetheruserinput,modelingaseventstreams/Modelinguserinputaseventstreamsactivekeysstream,workingwith/Workingwiththeactivekeysstream
CloudFormationabout/Infrastructureautomationused,forbuildingAWSresourcesdashboard/CloudFormationdescribeStacksendpoint/ThedescribeStacksendpointdescribeStackResourcesendpoint/ThedescribeStackResourcesendpoint
combinatorsusing/Combinatorsandeventhandlers
complexWebUIsproblems/TheproblemwithcomplexwebUIsfeatures/TheproblemwithcomplexwebUIs
CompositionalEventSystem(CES)about/AsynchronousCompositionalEventSystem(CES)aboutprogrammingandconcurrency
CompositionalEventSystemsabout/Onemoreflatmapfortheroad
concurrencyabout/AsynchronousCompositionalEventSystem(CES)aboutprogrammingandconcurrency
Contactsapplication
building/BuildingasimpleContactsapplicationwithOmstate/TheContactsapplicationstateproject,settingup/SettinguptheContactsprojectcomponents/Applicationcomponentscursors,using/Omcursorscontacts-appcomponent,creating/Fillingintheblankscontacts-viewcomponent,creating/Fillingintheblankscontact-summary-viewcomponent,creating/Fillingintheblanksdetails-panel-viewcomponent,creating/Fillingintheblankscontact-details-viewcomponent,creating/Fillingintheblankscontact-details-form-viewcomponent,creating/Fillingintheblankscontactinformation,updating/Fillingintheblanks
core.asyncabout/core.asyncCSP/Communicatingsequentialprocessesstockmarketapplication,rewriting/Rewritingthestockmarketapplicationwithcore.asyncerrorhandling/Errorhandlingbackpressure/Backpressuretransducers/Transducersandcore.asyncfeatures/SummaryversusCES/CESversuscore.async
core.asyncchannelsusing/Intercomponentcommunication
CSPabout/CommunicatingsequentialprocessesURL/Communicatingsequentialprocesses
cursorsabout/Omcursors
Ddata
fetching,inparallel/Fetchingdatainparalleldataflowprogramming
about/DataflowprogrammingDataTransfer
referencelink/Utilityfunctionsdc-lib
URL/DataflowprogrammingdescribeDBInstancesendpoint
about/ThedescribeDBInstancesendpointdescribeInstancesendpoint
about/ThedescribeInstancesendpointdescribeStackResourcesendpoint
about/ThedescribeStackResourcesendpointdescribeStacksendpoint
about/ThedescribeStacksendpointdroppingbuffer
about/Droppingbuffer
EEC2
about/Infrastructureautomationused,forbuildingAWSresourcesdashboard/EC2describeInstancesendpoint/ThedescribeInstancesendpoint
Elmabout/First-orderFRPURL/First-orderFRP
errorhandlingabout/ErrorhandlingonErrorcombinator/OnErrorcatchcombinator/Catchretrycombinator/Retryreferencelink/Retryincore.async/Errorhandling
eventhandlersusing/Combinatorsandeventhandlers
eventsabout/Signalsandevents
Ffactorymethods
referencelink/CustomObservablesfirst-orderFRP
about/First-orderFRPfixedbuffer
about/FixedbufferFlapjax
about/ComplexGUIsandanimationsflatmap
about/Flatmapandfriendswithmultiplevalues/Onemoreflatmapfortheroad
ForkJoinPoolURL/Themoviesexamplerevisitedusing/FuturesandblockingIO
FRPabout/FunctionalReactiveProgrammingimplementationchallenges/ImplementationchallengesversusCES/Asynchronousdataflow
FRP,usecasesasynchronousprogramming/Asynchronousprogrammingandnetworkingnetworking/AsynchronousprogrammingandnetworkingcomplexGUIs/ComplexGUIsandanimationsanimations/ComplexGUIsandanimations
FunctionalProgrammingabout/Lessonsfromfunctionalprogramming
functioncurryingabout/ApplicativeFunctors
Functorsabout/FunctorsOptionFunctor/TheOptionFunctorApplicativeFunctors/ApplicativeFunctors
futuresabout/Clojurefuturescreating,inimminent/CreatingfuturesblockingIO/FuturesandblockingIO
HHaskell
about/Monadshigher-orderFRP
about/Higher-orderFRPhistory,ReactiveProgramming
about/Abitofhistorydataflowprogramming/Dataflowprogrammingobject-orientedReactiveProgramming/Object-orientedReactiveProgrammingLANguageforProgrammingArraysatRandom(LANPAR)/ThemostwidelyusedreactiveprogramObserverdesignpattern/TheObserverdesignpatternFRP/FunctionalReactiveProgramminghigher-orderFRP/Higher-orderFRP
Iimminent
about/Imminent–acomposablefutureslibraryforClojureURL/Imminent–acomposablefutureslibraryforClojurefutures,creating/Creatingfuturescombinators,using/Combinatorsandeventhandlerseventhandlers,using/Combinatorsandeventhandlersexample/Themoviesexamplerevisited
implementationchallenges,FRPfirst-orderFRP/First-orderFRPasynchronousdataflow/AsynchronousdataflowArrowizedFRP/ArrowizedFRP
incidentalcomplexityabout/Identifyingproblemswithourcurrentapproachremoving,withRxClojure/RemovingincidentalcomplexitywithRxClojure
infrastructureautomationproblem/TheproblemwithAWS/Infrastructureautomation
intercomponentcommunicationabout/Intercomponentcommunicationagileboard,creating/CreatinganagileboardwithOm
Iteratorinterfaceabout/Observer–anIterator’sdual
LLANguageforProgrammingArraysatRandom(LANPAR)
about/Themostwidelyusedreactiveprogramlein-cljsbuild
URL/ClojureorClojureScript?about/ClojureorClojureScript?
Mmacros
referencelink/GameentitiesManagedBlocker
referencelink/FuturesandblockingIOmap
about/ThesemanticsofmapFunctors/Functors
MimmoCosenzaURL/SettinguptheContactsproject
minimalCESframeworkabout/AminimalCESframeworkproject,settingup/ClojureorClojureScript?publicAPI,designing/DesigningthepublicAPItokens,implementing/Implementingtokenseventstreams,implementing/Implementingeventstreamsbehaviors,implementing/Implementingbehaviorsrespondentapplication,building/Arespondentapplication
Monadabout/Onemoreflatmapfortheroad
Monadsabout/Monadsbindfunction/Monadsusing/Monadsexample/Monads
monetURL/Settinguptheproject
Oobject-orientedReactiveProgramming
about/Object-orientedReactiveProgrammingObservables
creating/CreatingAWSObservablescombining/CombiningtheAWSObservables
observablescreating/CreatingObservablescustomobservables,creating/CustomObservablesmanipulating/ManipulatingObservables
ObservableSequencesabout/Asynchronousdataflow
Observerdesignpatternabout/TheObserverdesignpattern,TheObserverpatternrevisitedIteratorinterface/Observer–anIterator’sdual
Omabout/ClojureScriptandOmContactsapplication,building/BuildingasimpleContactsapplicationwithOmagileboard,creating/CreatinganagileboardwithOm
om-starttemplateURL/SettinguptheContactsproject
OmProjectManagement/CreatinganagileboardwithOmonErrorcombinator
about/OnErrorOptionFunctor
about/TheOptionFunctorusecase/Findingtheaverageofages
PPurelyFunctionalDataStructures
about/LessonsfromfunctionalprogrammingURL/Lessonsfromfunctionalprogramming
RRDS
about/Infrastructureautomationused,forbuildingAWSresourcesdashboard/RDSdescribeDBInstancesendpoint/ThedescribeDBInstancesendpoint
React.jsabout/EnterReact.jsFunctionalProgramming/Lessonsfromfunctionalprogramming
ReactiveCocoaURL/FunctionalReactiveProgrammingabout/Asynchronousdataflow
ReactiveExtensions(Rx)about/Asynchronousdataflow,ReagiandotherCESframeworksdrawbacks/ReagiandotherCESframeworks
Reagicomparing,withotherCESframeworks/ReagiandotherCESframeworksabout/ReagiandotherCESframeworks
respondentapplicationbuilding/Arespondentapplication
retrycombinatorabout/Retry
Rxobservables,creating/CreatingObservablesobservables,manipulating/ManipulatingObservablesflatmap/Flatmapandfriends
RXerrorhandling/Errorhandling
RxClojureURL/CreatingObservables
RxJavaURL/FunctionalReactiveProgramming,CreatingObservables
RxJSURL/AtasteofReactiveProgramming
Ssamplecombinator
about/SampleScala
about/MonadsScheduledThreadPoolExecutorpool/BuildingastockmarketmonitoringapplicationSemigroups
about/Functorssignals
about/Signalsandeventssinewaveanimation
creating/AtasteofReactiveProgrammingtime,creating/Creatingtimecoloring/Morecolorsupdating/Makingitreactiveenhancing/Exercise1.1
slidingbufferabout/Slidingbuffer
stockmarketapplicationrewriting,withcore.async/Rewritingthestockmarketapplicationwithcore.asyncapplicationcode,implementing/Implementingtheapplicationcode
stockmarketmonitoringapplicationbuilding/Buildingastockmarketmonitoringapplicationrollingaverages,displaying/Rollingaveragesproblems,identifying/Identifyingproblemswithourcurrentapproachincidentalcomplexity,removingwithRxClojure/RemovingincidentalcomplexitywithRxClojureobservablerollingaverages/Observablerollingaverages
stubserversettingup/RunningtheAWSstubserver
TThreadPool
about/Fetchingdatainparalleltools.namespace
URL/Implementingeventstreamsusing/Implementingeventstreams
transducersabout/Transducersreferencelink/Transducerswithcore.async/Transducersandcore.async
transitabout/SettingupthedashboardprojectURL/Settingupthedashboardproject
TrelloURL/Intercomponentcommunication