So far, you’ve focused on becoming familiar with the tools that Clojure provides: immutable data structures, functions, abstractions, and so on. In this chapter, you’ll learn how tothink about your programming tasks in a way that makes the best use of those tools. You’ll begin integrating your experience into a new functional programming mindset.
The core concepts you’ll learn include: what pure functions are and why they’re useful; how to work with immutable data structures and why they’re superior to their mutable cousins; how disentangling data and functions gives you more power and flexibility; and why it’s powerful to program to a small set of data abstractions. Once you shove all this knowledge into your brain matter, you’ll have an entirely new approach to problem solving!
After going over these topics, you’ll put everything you’ve learned to useby writing a terminal-based game inspired by an ancient, mystic mind-trainingdevice found in Cracker Barrel restaurants across America: Peg Thing!
Except forprintln andrand, all the functions you’ve used up till now have been pure functions. What makes them pure functions, and why does it matter? A function is pure if it meets two qualifications:
These qualities make it easier for you to reason about your programs because the functions are completely isolated, unable to impact other parts of your system. When you use them, you don’t have to ask yourself, “What could I break by calling this function?” They’re also consistent: you’ll never need to figure out why passing a function the same arguments results in different return values, because that will never happen.
Pure functions are as stable and problem free as arithmetic (when was the last time you fretted over adding two numbers?). They’re stupendous little bricks of functionality that you can confidently use as the foundation of your program. Let’s look at referential transparency and lack of side effects in more detail to see exactly what they are and how they’re helpful.
To return the same result when called with the same argument, purefunctions rely only on 1) their own arguments and 2) immutable values to determine their return value. Mathematical functions, for example, are referentially transparent:
(+12); => 3If a function relies on an immutable value, it’s referentially transparent. The string", Daniel-san" is immutable, so the following function is also referentially transparent:
(defnwisdom[words](strwords", Daniel-san"))(wisdom"Always bathe on Fridays"); => "Always bathe on Fridays, Daniel-san"By contrast, the following functions do not yield the same result with the same arguments; therefore, they are not referentially transparent. Any function that relies on a random number generator cannot be referentially transparent:
(defnyear-end-evaluation[](if(>(rand)0.5)"You get a raise!""Better luck next year!"))If your function reads from a file, it’s not referentially transparentbecause the file’s contents can change. The following function,analyze-file,is not referentially transparent, but the functionanalysis is:
(defnanalyze-file[filename](analysis(slurpfilename)))(defnanalysis[text](str"Character count: "(counttext)))When using a referentially transparent function, you never have toconsider what possible external conditions could affect the return value of the function. This is especially important if your function is used multiple places or if it’s nested deeply in a chain of function calls. In both cases, you can rest easy knowing that changes to external conditions won’t cause your code to break.
Another way to think about this is that reality is largely referentially transparent. If you think of gravity as a function, then gravitational force is the return value of calling that function on two objects. Therefore, the next time you’re in a programming interview, you can demonstrate your functional programming knowledge by knocking everything off your interviewer’s desk. (This also demonstrates that you know how to apply a function over a collection.)
To perform a side effect is to change the association between a name and its value within a given scope. Here is an example in #"clojure">varhaplessObject={emotion:"Carefree!"};varevilMutator=function(object){object.emotion="So emo :'(";}evilMutator(haplessObject);haplessObject.emotion;//=>"So emo :'("
Of course, your program has to have some side effects. It writes to a disk, which changes the association between a filename and a collection of disk sectors; it changes the RGB values of your monitor’s pixels; and so on. Otherwise, there’d be no point in running it.
Side effects are potentially harmful, however, because they introduceuncertainty about what the names in your code are referring to. This leads tosituations where it’s very difficult to trace why and how a name came to be associated with a value, which makes it hard to debug the program. When you call a function that doesn’t have side effects, you only have to consider the relationship between the input and the output. You don’t have to worry about other changes that could be rippling through your system.
Functions with side effects, on the other hand, place more of a burden on your mind grapes: now you have to worry about how the world is affected when you call the function. Not only that, but every function thatdepends on a side-effecting function gets infected by this worry; it, too,becomes another component that requires extra care and thought as you build your program.
If you have any significant experience with a language like Ruby or JavaScript, you’ve probably run into this problem. As an object gets passed around, its attributes somehow change, and you can’t figure out why. Then you have to buy a new computer because you’ve chucked yours out the window. If you’ve read anything about object-oriented design, you know that a lot of writing has been devoted to strategies for managing state and reducing side effects for just this reason.
For all these reasons, it’s a good idea to look for ways to limit the use of side effects in your code. Lucky for you, Clojure makes your job easier by going to great lengths to limit side effects—all of its core data structures are immutable. You cannot change them in place, no matter how hard you try! However, if you’re unfamiliar with immutable data structures, you might feel like your favorite tool has been taken from you. How can youdo anything without side effects? Well, that’s what the next section is all about! How about that segue, eh? Eh?
Immutable data structures ensure that your code won’t have side effects. As you now know with all your heart, this is a good thing. But how do you get anything done without side effects?
Raise your hand if you’ve ever written something like this in #"clojure">varwrestlers=getAlligatorWrestlers();vartotalBites=0;varl=wrestlers.length;for(vari=0; i < l; i++){totalBites+=wrestlers[i].timesBitten;}
Or this:
varallPatients=getArkhamPatients();varanalyzedPatients=[];varl=allPatients.length;for(vari=0; i < l; i++){if(allPatients[i].analyzed){analyzedPatients.push(allPatients[i]);}}Notice that both examples induce side effects on the looping variablei, as well as a variable outside the loop (totalBites in the first example andanalyzedPatients in the second). Using side effects this way—mutatinginternalvariables—is pretty much harmless. You’re creating new values, as opposed to changing an objectyou’ve received from elsewhere in your program.

But Clojure’s core data structures don’t even allow these harmlessmutations. So what can you do instead? First, ignore the fact that you could easily usemap andreduce to accomplish the preceding work. In these situations—iterating over some collection to build a result—the functional alternative to mutation is recursion.
Let’s look at the first example, building a sum. Clojure has no assignment operator. You can’t associate a new value with a name without creating a new scope:
(defgreat-baby-name"Rosanthony")great-baby-name; => "Rosanthony"(let[great-baby-name"Bloodthunder"]great-baby-name); => "Bloodthunder"great-baby-name; => "Rosanthony"In this example, you first bind the namegreat-baby-name to"Rosanthony" within the global scope. Next, you introduce a new scope withlet. Within that scope, you bindgreat-baby-name to"Bloodthunder". Once Clojure finishes evaluating thelet expression, you’re back in the globalscope, and great-baby-name evaluates to"Rosanthony" once again.
Clojure lets you work around this apparent limitation with recursion. The following example shows the general approach to recursive problem solving:
(defnsum➊([vals](sumvals0))([valsaccumulating-total]➋(if(empty?vals)accumulating-total(sum(restvals)(+(firstvals)accumulating-total)))))This function takes two arguments, a collection to process (vals) and an accumulator (accumulating-total), and it uses arity overloading (covered in Chapter 3) to provide a default value of0 foraccumulating-total at ➊.
Like all recursive solutions, this function checks the argument it’s processing against a base condition. In this case, we check whethervals is empty at➋. If it is, we know that we’ve processed all the elements in the collection, so we returnaccumulating-total.
Ifvals isn’t empty, it means we’re still working our way through thesequence, so we recursively callsum passing it two arguments: thetail of vals with(rest vals) and the sum of the first element ofvals plus the accumulating total with(+ (first vals) accumulating-total). In this way, we build upaccumulating-total and at the same time reducevals until it reaches the base case of an empty collection.
Here’s what the recursive function call might look like if we separate out each time it recurs:
(sum[3951]); single-arity body calls two-arity body(sum[3951]0)(sum[51]39)(sum[1]44)(sum[]45); base case is reached, so return accumulating-total; => 45Each recursive call tosum creates a new scope wherevals andaccumulating-total are bound to different values, all without needing to alter the values originally passed to the function or perform any internalmutation. As you can see, you can get along fine without mutation.
Note that you should generally userecur when doing recursion for performance reasons. The reason is that Clojure doesn’t providetail calloptimization, a topic I will never bring up again! (Check out this URL for more information:http://en.wikipedia.org/wiki/Tail_call.) So here’s how you’d do this withrecur:
(defnsum([vals](sumvals0))([valsaccumulating-total](if(empty?vals)accumulating-total(recur(restvals)(+(firstvals)accumulating-total)))))Usingrecur isn’t that important if you’re recursively operating on asmall collection, but if your collection contains thousands or millionsvalues, you will definitely need to whip outrecurso you don’t blow up your program with a stack overflow.
One last thing! You might be saying, “Wait a minute—what if I end up creating thousands of intermediate values? Doesn’t this cause the program to thrash because of garbage collection or whatever?”
Very good question, eagle-eyed reader! The answer is no! The reasonis that, behind the scenes, Clojure’s immutable data structures are implemented usingstructural sharing, which is totally beyond the scope of thisbook. It’s kind of like Git! Read this great article if you want to know more:http://hypirion.com/musings/understanding-persistent-vector-pt-1.
Another way you might be used to using mutation is to build up the final state of an object. In the following Ruby example, theGlamourShotCaption object uses mutation to clean input by removing trailing spaces and capitalizing"lol":
classGlamourShotCaptionattr_reader:textdefinitialize(text)@text=textclean!endprivatedefclean!text.trim!text.gsub!(/lol/,"LOL")endendbest=GlamourShotCaption.new("My boa constrictor is so sassy lol! ")best.text; => "My boa constrictor is so sassy LOL!"In this code, the classGlamourShotCaption encapsulates the knowledge of how to clean a glamour shot caption. On creating aGlamourShotCaption object, you assign text to an instance variable and progressively mutate it.
Listing 5-1 shows how you might do this in Clojure:
(require'[clojure.string:ass])(defnclean[text](s/replace(s/trimtext)#"lol""LOL"))(clean"My boa constrictor is so sassy lol! "); => "My boa constrictor is so sassy LOL!"
In the first line, we userequire to access the string function library (I’ll discuss this function and related concepts in Chapter 6). Otherwise, the code is easy peasy. No mutation required. Instead of progressively mutating an object, theclean function works by passing an immutable value,text, to a pure function,s/trim, which returns an immutable value ("My boa constrictor is so sassy lol!"; the spaces at the end of the string have been trimmed). That value is then passed to the pure functions/replace, which returns another immutable value ("My boa constrictor is so sassy LOL!").
Combining functions like this—so that the return value of one function is passed as an argument to another—is calledfunction composition. In fact,this isn’t so different from the previous example, which used recursion,because recursion continually passes the result of a function to another function; it just happens to be the same function. In general, functional programming encourages you to build more complex functions by combining simpler functions.
This comparison also starts to reveal some limitations ofobject-oriented programming (OOP). In OOP, one of the main purposes of classes is toprotect against unwanted modification of private data—something that isn’t necessary with immutable data structures. You also have to tightly couple methods with classes, thus limiting the reusability of the methods. In the Ruby example, you have to do extra work to reuse theclean! method. In Clojure,clean will work on any string at all. By both a) decoupling functions and data, and b) programming to a small set of abstractions, you end up with more reusable, composable code. You gain power and lose nothing.
Going beyond immediately practical concerns, the differences betweenthe way you write object-oriented and functional code point to a deeper difference between the two mindsets. Programming is about manipulating data for your own nefarious purposes (as much as you can say it’s aboutanything). In OOP, you think about data as something you can embody in an object, and you poke and prod it until it looks right. During this process, your original data is lost forever unless you’re very careful about preserving it. By contrast, in functional programming you think of data as unchanging, and you derive new data from existing data. During this process, the original data remains safe and sound. In the preceding Clojure example,the original caption doesn’t get modified. It’s safe in the same way thatnumbers are safe when you add them together; you don’t somehow transform 4 into 7 when you add 3 to it.
Once you are confident using immutable data structures to get stuff done, you’ll feel even more confident because you won’t have to worry about what dirty code might be getting its greasy paws on your precious, mutable variables. It’ll be great!
You can derive new functions from existing functions in the same way that you derive new data from existing data. You’ve already seen one function,partial, that creates new functions. This section introduces you to two more functions,comp andmemoize, which rely on referential transparency, immutability, or both.
It’s always safe to compose pure functions like we just did in the previous section, because you only need to worry about their input/output relationship. Composing functions is so common that Clojure provides a function,comp, for creating a new function from the composition of any number of functions. Here’s a simple example:
((comp inc*)23); => 7Here, you create an anonymous function by composing theinc and* functions. Then, you immediately apply this function to the arguments2 and3. The function multiplies the numbers 2 and 3 and then incrementsthe result. Using math notation, you’d say that, in general, usingcomp on the functionsf1,f2, ...fn, creates a new functiong such thatg(x1,x2, ...xn) equalsf1(f2(fn(x1,x2, ...xn))). One detail to note here is that the first functionapplied—* in the code shown here—can take any number of arguments, whereas the remaining functions must be able to take only one argument.
Here’s an example that shows how you could usecomp to retrieve character attributes in role-playing games:
(defcharacter{:name"Smooches McCutes":attributes{:intelligence10:strength4:dexterity5}})(defc-int(comp:intelligence:attributes))(defc-str(comp:strength:attributes))(defc-dex(comp:dexterity:attributes))(c-intcharacter); => 10(c-strcharacter); => 4(c-dexcharacter); => 5In this example, you created three functions that help you look up acharacter’s attributes. Instead of usingcomp, you could just have writtensomething like this for each attribute:
(fn[c](:strength(:attributesc)))Butcomp is more elegant because it uses less code to convey more meaning. When you seecomp, you immediately know that the resulting function’s purpose is to combine existing functions in a well-known way.
What do you do if one of the functions you want to compose needsto take more than one argument? You wrap it in an anonymous function. Have a look at this next snippet, which calculates the number of spell slots your character has based on her intelligence attribute:
(defnspell-slots[char](int(inc(/(c-intchar)2))))(spell-slotscharacter); => 6First, you divide intelligence by two, then you add one, and then youuse theint function to round down. Here’s how you could do the samething withcomp:
(defspell-slots-comp(comp int inc#(/%2)c-int))To divide by two, all you needed to do was wrap the division in ananonymous function.
Clojure’scomp function can compose any number of functions. To get a hint of how it does this, here’s an implementation that composes just two functions:
(defntwo-comp[fg](fn[&args](f(applygargs))))I encourage you to evaluate this code and usetwo-comp to compose two functions! Also, try reimplementing Clojure’scomp function so you can compose any number of functions.
Another cool thing you can do with pure functions is memoize them so that Clojure remembers the result of a particular function call. You can do this because, as you learned earlier, pure functions are referentiallytransparent. For example,+ is referentially transparent. You can replace
(+3(+58))with
(+313)or
16and the program will have the same behavior.
Memoization lets you take advantage of referential transparency by storing the arguments passed to a function and the return value of the function. That way, subsequent calls to the function with the same arguments can return the result immediately. This is especially useful for functions that take a lot of time to run. For example, in this unmemoized function, the result is returned after one second:
(defnsleepy-identity"Returns the given value after 1 second"[x](Thread/sleep1000)x)(sleepy-identity"Mr. Fantastico"); => "Mr. Fantastico" after 1 second(sleepy-identity"Mr. Fantastico"); => "Mr. Fantastico" after 1 secondHowever, if you create a new, memoized version ofsleepy-identity withmemoize, only the first call waits one second; every subsequent function call returns immediately:
(defmemo-sleepy-identity(memoizesleepy-identity))(memo-sleepy-identity"Mr. Fantastico"); => "Mr. Fantastico" after 1 second(memo-sleepy-identity"Mr. Fantastico"); => "Mr. Fantastico" immediatelyPretty cool! From here on out,memo-sleepy-identity will not incur theinitial one-second cost when called with"Mr. Fantastico". This implementation could be useful for functions that are computationally intensive or that make network requests.
It’s that time! Time to build a terminal implementation of Peg Thing using everything you’ve learned so far: immutable data structures, lazy sequences, pure functions, recursion—everything! Doing this will help you understandhow to combine these concepts and techniques to solve larger problems. Most important, you’ll learn how to model the changes that result fromeach move a player makes without having to mutate objects like you would in OOP.
To build the game, you’ll first learn the game’s mechanics and how to start and play the program. Then, I’ll explain the code’s organization. Finally, I’ll walk through each function.
You can find the complete code repository for Peg Thing athttps://www.nostarch.com/clojure/.
As mentioned earlier, Peg Thing is based on the secret mind-sharpening tool passed down from ye olden days and is now distributed by Cracker Barrel.
If you’re not familiar with the game, here are the mechanics. You startout with a triangular board consisting of holes filled with pegs, and onehole has a missing a peg, as shown in Figure 5-1.

The object of the game is to remove as many pegs as possible. You do this byjumping over pegs. In Figure 5-2, peg A jumps over peg B into the empty hole, and you remove peg B from the board.

To start Peg Thing, download the code, and then runlein run in yourterminal while in thepegthing directory. A prompt appears that looks like this:
GetreadytoplayPegThing!Howmanyrows?[5]Now you can enter the number of rows the board will have, using5 asthe default. If you want five rows, just pressenter (otherwise, type a numberand pressenter). You’ll then see this:
Here'syourboard:a0b0c0d0e0f0g0h0i0j0k0l0m0n0o0Removewhichpeg?[e]Each letter identifies a position on the board. The number0 (which should be blue, but if it’s not, it’s no big deal) indicates that a position is filled. Before gameplay begins, one peg must be empty, so the prompt asks you to enter the position of the first to peg to remove. The default is the center peg,e, but you can choose a different one. After you remove the peg, you’ll see this:
Here'syourboard:a0b0c0d0e-f0g0h0i0j0k0l0m0n0o0Movefromwheretowhere?Entertwoletters:Notice that thee position now has a dash,- (which should be red, but if it’s not, it’s no big deal). The dash indicates that the position is empty. To move, you enter the position of the peg you want topick up followed by the position of the empty position that you want to place it in. If you enterle, for example, you’ll get this:
Here'syourboard:a0b0c0d0e0f0g0h-i0j0k0l-m0n0o0Movefromwheretowhere?Entertwoletters:You’ve moved the peg froml toe, jumping overh and removing its peg according to the rule shown in Figure 5-2. The game continues to prompt you for moves until no moves are available, whereupon it prompts you to play again.
The program has to handle four major tasks, and the source code is organizedaccordingly, with the functions for each of these tasks grouped together:
Two more points about the organization: First, the code has a basicarchitecture, or conceptual organization, of two layers. The top layer consistsof the functions for handling user interaction. These functions produce all of the program’s side effects, printing out the board and presentingprompts for player interaction. The functions in this layer use the functions in the bottom layer to create a new board, make moves, and create a textualrepresentation, but the functions in the bottom layer don’t use those inthe top layer at all. Even for a program this small, a little architecture helps make the code more manageable.
Second, I’ve tried as much as possible to decompose tasks into smallfunctions so that each does one tiny, understandable task. Some of these functions are used by only one other function. I find this helpful because it lets me name each tiny subtask, allowing me to better express the intention of the code.
But before all the architecture, there’s this:
(nspegthing.core(require[clojure.set:asset])(:gen-class))(declaresuccessful-moveprompt-movegame-overquery-rows)I’ll explain the functions here in more detail in Chapter 6. If you’recurious about what’s going on, the short explanation is that(require[clojure.set :as set]) allows you to easily use functions in theclojure.set namespace,(:gen-class) allows you to run the program from the commandline, and(declare successful-move prompt-move game-over query-rows) allows functions to refer to those names before they’re defined. If that doesn’t quite make sense yet, trust that I will explain it soon.
The data structure you use to represent the board should make it easy for you to print the board, check whether a player has made a valid move, actually perform a move, and check whether the game is over. You could structure the board in many ways to allow these tasks. In this case, you’ll represent the board using a map with numerical keys corresponding to each board position and values containing information about that position’s connections. The map will also contain a:rows key, storing the total number of rows. Figure 5-3 shows a board with each position numbered.

Here’s the data structure built to represent it:
{1{:peggedtrue,:connections{63,42}},2{:peggedtrue,:connections{95,74}},3{:peggedtrue,:connections{106,85}},4{:peggedtrue,:connections{138,117,65,12}},5{:peggedtrue,:connections{149,128}},6{:peggedtrue,:connections{1510,139,45,13}},7{:peggedtrue,:connections{98,24}},8{:peggedtrue,:connections{109,35}},9{:peggedtrue,:connections{78,25}},10{:peggedtrue,:connections{89,36}},11{:peggedtrue,:connections{1312,47}},12{:peggedtrue,:connections{1413,58}},13{:peggedtrue,:connections{1514,1112,69,48}},14{:peggedtrue,:connections{1213,59}},15{:peggedtrue,:connections{1314,610}},:rows5}You might be wondering why, when you play the game, each position is represented by a letter but here the positions are represented by numbers. Using numbers for theinternal representation allows you to take advantage of some mathematical properties of the board layout when validating and making moves. Letters, on the other hand, are better for display because they take up only one character space. Some conversion functions are covered in“Rendering and Printing the Board” on page120.
In the data structure, each position is associated with a map that reads something like this:
{:peggedtrue,:connections{63,42}}The meaning of:pegged is clear; it represents whether that position has a peg in it.:connections is a bit more cryptic. It’s a map where each key identifies alegal destination, and each value representsthe position that would bejumped over. So pegs in position 1, for example, can jumpto position 6over position 3. This might seem backward, but you’ll learn the rationale for it later when you see how move validation is implemented.
Now that you’ve seen what the final map representing the board should look like, we can start exploring the functions that actually build up this map in the program. You won’t simply start assigning mutable states willy-nilly to represent each position and whether it’s pegged or not. Instead,you’ll use nested recursive function calls to build up the final board position by position. It’s analogous to the way you created the glamour shotcaption earlier, deriving new data from input by passing an argumentthrough a chain of functions to get your final result.
The first few expressions in this section of the code deal with triangular numbers. Triangular numbers are generated by adding the firstn natural numbers. The first triangular number is 1, the second is 3 (1 + 2), the third is 6 (1 + 2 + 3), and so on. These numbers line up nicely with the position numbers at the end of every row on the board, which will turn out to be a very useful property. First, you define the functiontri*, which can create a lazy sequence of triangular numbers:
(defntri*"Generates lazy sequence of triangular numbers"([](tri*01))([sumn](let[new-sum(+sumn)](consnew-sum(lazy-seq(tri*new-sum(incn)))))))To quickly recap how this works, callingtri* with no arguments will recursively call(tri* 0 1). This returns a lazy list whose element isnew-sum, which in this case evaluates to1. The lazy list includes a recipe for generating the next element of the list when it’s requested, as described in Chapter 4.
The next expression callstri*, actually creating the lazy sequence and binding it totri:
(deftri(tri*))You can verify that it actually works:
(take5tri); => (1 3 6 10 15)And the next few functions operate on the sequence of triangularnumbers.triangular? figures out if its argument is in thetri lazy sequence.It works by usingtake-while to create a sequence of triangular numbers whose last element is a triangular number that’s less than or equal to the argument. Then it compares the last element to the argument:
(defntriangular?"Is the number triangular? e.g. 1, 3, 6, 10, 15, etc"[n](=n(last(take-while#(>=n%)tri))))(triangular?5); => false(triangular?6); => trueNext, there’srow-tri, which takes a row number and gives you the triangular number at the end of that row:
(defnrow-tri"The triangular number at the end of row n"[n](last(takentri)))(row-tri1); => 1(row-tri2); => 3(row-tri3); => 6Lastly, there’srow-num, which takes a board position and returns the row that it belongs to:
(defnrow-num"Returns row number the position belongs to: pos 1 in row 1, positions 2 and 3 in row 2, etc"[pos](inc(count(take-while#(>pos%)tri))))(row-num1); => 1(row-num5); => 3After that comesconnect, which is used to actually form a mutual connection between two positions:
(defnconnect"Form a mutual connection between two positions"[boardmax-posposneighbordestination](if(<=destinationmax-pos)(reduce(fn[new-board[p1p2]](assoc-innew-board[p1:connectionsp2]neighbor))board[[posdestination][destinationpos]])board))(connect{}15124); => {1 {:connections {4 2}}4{:connections{12}}}The first thingconnect does is check whether the destination is actually a position on the board by confirming that it’s less than the board’s max position. For example, if you have five rows, the max position is 15. However, when the game board is created, theconnect function will becalled with arguments like(connect {} 15 7 16 22). Theif statement at the beginning ofconnect makes sure your program doesn’t allow such outrageous connections, and simply returns the unmodified board when you ask it to do something silly.
Next,connect uses recursion throughreduce to progressively build upthe final state of the board. In this example, you’re reducing over the nested vectors[[1 4] [4 1]]. This is what allows you to return an updatedboard with bothpos anddestination (1 and 4) pointing to each other intheir connections.
The anonymous function passed toreduce uses a function,assoc-in, which you haven’t seen before. Whereas the functionget-in lets you look up values in nested maps,assoc-in lets you return a new map with the given value at the specified nesting. Here are a couple of examples:
(assoc-in{}[:cookie:monster:vocals]"Finntroll"); => {:cookie {:monster {:vocals "Finntroll"}}}(get-in{:cookie{:monster{:vocals"Finntroll"}}}[:cookie:monster]); => {:vocals "Finntroll"}(assoc-in{}[1:connections4]2); => {1 {:connections {4 2}}}In these examples, you can see that new, nested maps are created to accommodate all the keys provided.
Now we have a way to connect two positions, but how should the program choose two positions to connect in the first place? That’s handled byconnect-right,connect-down-left, andconnect-down-right:
(defnconnect-right[boardmax-pospos](let[neighbor(incpos)destination(incneighbor)](if-not(or(triangular?neighbor)(triangular?pos))(connectboardmax-posposneighbordestination)board)))(defnconnect-down-left[boardmax-pospos](let[row(row-numpos)neighbor(+rowpos)destination(+1rowneighbor)](connectboardmax-posposneighbordestination)))(defnconnect-down-right[boardmax-pospos](let[row(row-numpos)neighbor(+1rowpos)destination(+2rowneighbor)](connectboardmax-posposneighbordestination)))These functions each take the board’s max position and a board position and use a little triangle math to figure out which numbers to feed toconnect. For example,connect-down-left will attempt to connect position 1 to position 4. In case you’re wondering why the functionsconnect-left,connect-up-left, andconnect-up-right aren’t defined, the reason is that the existing functions actually cover these cases.connect returns a board with the mutual connection established; when 4connects right to 6, 6connects left to 4. Here are a couple of examples:
(connect-down-left{}151); => {1 {:connections {4 2}4{:connections{12}}}}(connect-down-right{}153); => {3 {:connections {10 6}}10{:connections{36}}}In the first example,connect-down-left takes an empty board with a max position of 15 and returns a new board populated with the mutual connection between the 1 position and the position below and to the left of it.connect-down-right does something similar, returning a board populated with the mutual connection between 3 and the position below it and to its right.
The next function,add-pos, is interesting because it actually reduces on a vector offunctions, applying each in turn to build up the resulting board. But first it updates the board to indicate that a peg is in the given position:
(defnadd-pos"Pegs the position and performs connections"[boardmax-pospos](let[pegged-board(assoc-inboard[pos:pegged]true)](reduce(fn[new-boardconnection-creation-fn](connection-creation-fnnew-boardmax-pospos))pegged-board[connect-rightconnect-down-leftconnect-down-right])))(add-pos{}151){1{:connections{63,42},:peggedtrue}4{:connections{12}}6{:connections{13}}}It’s like this function is first saying, in thepegged-board binding, “Add a peg to the board at position X.” Then, inreduce, it says, “Take the boardwith its new peg at position X, and try to connect position X to a legal,rightward position. Take the board that results from that operation, and try to connect position X to a legal, down-left position. Finally, take the board that results fromthat operation, and try to connect position X to a legal, down-right position. Return the resulting board.”
Reducing over functions like this is another way of composing functions. To illustrate, here’s another way of defining theclean function in Listing 5-1 (page103):
(defnclean[text](reduce(fn[stringstring-fn](string-fnstring))text[s/trim#(s/replace%#"lol""LOL")]))This redefinition ofclean reduces a vector of functions by applying the first function,s/trim, to an initial string, then applying the next function, the anonymous function#(s/replace % #"lol" "LOL"), to the result.
Reducing over a collection of functions is not a technique you’ll useoften, but it’s occasionally useful, and it demonstrates the versatility of functional programming.
Last among our board creation functions isnew-board:
(defnnew-board"Creates a new board with the given number of rows"[rows](let[initial-board{:rowsrows}max-pos(row-trirows)](reduce(fn[boardpos](add-posboardmax-pospos))initial-board(range1(incmax-pos)))))The code first creates the initial, empty board and gets the max position. Assuming that you’re using five rows, the max position will be 15. Next, the function uses(range 1 (inc max-pos)) to get a list of numbers from 1 to 15, otherwise known as the board’s positions. Finally, it reduces over the list of positions. Each iteration of the reduction calls(add-pos board max-pos pos), which, as you saw earlier, takes an existing board and returns a new one with the position added.
The next section of code validates and performs peg moves. Many of the functions (pegged?,remove-peg,place-peg,move-peg) are simple, self-explanatory one-liners:
(defnpegged?"Does the position have a peg in it?"[boardpos](get-inboard[pos:pegged]))(defnremove-peg"Take the peg at given position out of the board"[boardpos](assoc-inboard[pos:pegged]false))(defnplace-peg"Put a peg in the board at given position"[boardpos](assoc-inboard[pos:pegged]true))(defnmove-peg"Take peg out of p1 and place it in p2"[boardp1p2](place-peg(remove-pegboardp1)p2))Let’s take a moment to appreciate how neat this code is. This is where you would usually perform mutation in an object-oriented program; afterall, how else would you change the board? However, these are allpure functions, and they do the job admirably. I also like that you don’t need the overhead of classes to use these little guys. It feels somehow lighter to program like this.
Next up isvalid-moves:
(defnvalid-moves"Return a map of all valid moves for pos, where the key is the destination and the value is the jumped position"[boardpos](into{}(filter(fn[[destinationjumped]](and(not(pegged?boarddestination))(pegged?boardjumped)))(get-inboard[pos:connections]))))This code goes through each of the given position’s connections and tests whether the destination position is empty and the jumped position has a peg. To see this in action, you can create a board with the 4 position empty:
(defmy-board(assoc-in(new-board5)[4:pegged]false))Figure 5-4 shows what that board would look like.

Given this board, positions 1, 6, 11, and 13 have valid moves, but allothers don’t:
(valid-movesmy-board1); => {4 2}(valid-movesmy-board6); => {4 5}(valid-movesmy-board11); => {4 7}(valid-movesmy-board5); => {}(valid-movesmy-board8); => {}You might be wondering whyvalid-moves returns a map instead of, say, a set or vector. The reason is that returning a map allows you to easily look up a destination position to check whether a specific move is valid, which is whatvalid-move? (the next function) does:
(defnvalid-move?"Return jumped position if the move from p1 to p2 is valid, nil otherwise"[boardp1p2](get(valid-movesboardp1)p2))(valid-move?my-board84); => nil(valid-move?my-board14); => 2Notice thatvalid-move? looks up the destination position from the map and then returns the position of the peg that would be jumped over. Thisis another nice benefit of havingvalid-moves return a map, because thejumped position retrieved from the map is exactly what we want to pass on to the next function,make-move. When you take the time to construct a rich data structure, it’s easier to perform useful operations.
(defnmake-move"Move peg from p1 to p2, removing jumped peg"[boardp1p2](if-let[jumped(valid-move?boardp1p2)](move-peg(remove-pegboardjumped)p1p2)))if-let is a nifty way to say, “If an expression evaluates to a truthy value, then bind that value to a name the same way that I can in alet expression. Otherwise, if I’ve provided anelse clause, perform thatelseclause; if I haven’t provided anelse clause, returnnil.” In this case, the test expression is(valid-move? board p1 p2), and you’re assigning the result to the namejumped if the result is truthy. That’s used in the call tomove-peg, whichreturns a new board. You don’t supply anelse clause, so if the move isn’tvalid, the return value of the whole expression isnil.
Finally, the functioncan-move?is used to determine whether the game is over by finding the first pegged positions with moves available:
(defncan-move?"Do any of the pegged positions have valid moves?"[board](some(compnot-empty(partialvalid-movesboard))(map first(filter#(get(second%):pegged)board))))The question mark at the end of this function name indicates it’s apredicate function, a function that’s meant to be used in Boolean expressions.Predicate is taken from predicate logic, which concerns itself with determining whether a statement is true or false. (You’ve already seen some built-in predicate functions, likeempty? andevery?.)
can-move? works by getting a sequence of all pegged positions with(map first (filter #(get (second %) :pegged) board)). You can break thisdown further into thefilter andmap function calls: becausefilter is a seq function, it convertsboard, a map, into a seq of two-element vectors (also calledtuples), which looks something like this:
([1{:connections{63,42},:peggedtrue}][2{:connections{95,74},:peggedtrue}])The first element of the tuple is a position number, and the second is that position’s information.filter then applies the anonymous function#(get (second %) :pegged) to each of these tuples, filtering out the tuples where the position’s information indicates that the position is not currently housing a peg. Finally, the result is passed tomap, which callsfirst on each tuple to grab just the position number from the tuples.
After you get a seq of pegged positions numbers, you call a predicatefunction on each one to find the first position that returns a truthy value. The predicate function is created with(comp not-empty (partial valid-moves board)). The idea is to first return a map of all valid moves for a positionand then test whether that map is empty.
First, the expression(partial valid-moves board) derives an anonymousfunction fromvalid-moves with the first argument,board, filled in usingpartial (because you’re using the same board each time you callvalid-moves).The new function can take a position and return the map of all its valid moves for the current board.
Second, you usecomp to compose this function withnot-empty. This function is self-descriptive; it returnstrue if the given collection is empty andfalse otherwise.
What’s most interesting about this bit of code is that you’re using achain of functions to derive a new function, similar to how you use chainsof functions to derive new data. In Chapter 3, you learned that Clojure treats functions as data in that functions can receive functions as arguments and return them. Hopefully, this shows why that feature is fun and useful.
The first few expressions in the board representation and printing section just define constants:
(defalpha-start97)(defalpha-end123)(defletters(map(comp strchar)(rangealpha-startalpha-end)))(defpos-chars3)The bindingsalpha-start andalpha-end set up the beginning and end of the numerical values for the lettersa throughz. We use those to build up a seq ofletters.char, when applied to an integer, returns the character corresponding to that integer, andstr turns thechar into a string.pos-chars is used by the functionrow-padding to determine how much spacing to add to the beginning of each row. The next few definitions,ansi-styles,ansi, andcolorize output colored text to the terminal.
The functionsrender-pos,row-positions,row-padding, andrender-row create strings to represent the board:
(defnrender-pos[boardpos](str(nthletters(decpos))(if(get-inboard[pos:pegged])(colorize"0":blue)(colorize"-":red))))(defnrow-positions"Return all positions in the given row"[row-num](range(inc(or(row-tri(decrow-num))0))(inc(row-trirow-num))))(defnrow-padding"String of spaces to add to the beginning of a row to center it"[row-numrows](let[pad-length(/(*(-rowsrow-num)pos-chars)2)](apply str(takepad-length(repeat" ")))))(defnrender-row[boardrow-num](str(row-paddingrow-num(:rowsboard))(clojure.string/join" "(map(partialrender-posboard)(row-positionsrow-num)))))If you work from the bottom up, you can see thatrender-row calls each of the functions above it to return the string representation of the given row. Notice the expression(map (partial render-pos board) (row-positions row-num)). This demonstrates a good use case for partials by applying the same function multiple times with one or more arguments filled in, just like in thecan-move? function shown earlier.
Notice too thatrender-pos uses a letter rather than a number to identify each position. This saves a little space when the board is displayed, because it allows only one character per position to represent a five-row board.
Finally,print-board merely iterates over each row number withdoseq,printing the string representation of that row:
(defnprint-board[board](doseq[row-num(range1(inc(:rowsboard)))](println(render-rowboardrow-num))))You usedoseq when you want to perform side-effecting operations (likeprinting to a terminal) on the elements of a collection. The vector thatimmediately follows the namedoseq describes how to bind all the elements in a collection to a name one at a time so you can operate on them. In this instance, you’re assigning the numbers 1 through 5 (assuming there are five rows) to the namerow-num so you can print each row.
Although printing the board technically falls underinteraction, I wanted to show it here with the rendering functions. When I first started writing this game, theprint-board function also generated the board’s string representation. However, nowprint-board defers all rendering to pure functions, which makes the code easier to understand and decreases the surface area of our impure functions.
The next collection of functions handles player interaction. First, there’sletter->pos, which converts a letter (which is how the positions are displayed and identified by players) to the corresponding position number:
(defnletter->pos"Converts a letter string to the corresponding position number"[letter](inc(-(int(firstletter))alpha-start)))Next, the helper functionget-input allows you to read and clean theplayer’s input. You can also provide a default value, which is used if the player pressesenter without typing anything:
(defnget-input"Waits for user to enter text and hit enter, then cleans the input"([](get-inputnil))([default](let[input(clojure.string/trim(read-line))](if(empty?input)default(clojure.string/lower-caseinput)))))The next function,characters-as-strings, is a tiny helper function used byprompt-move to take in a string and return a collection of letters with all nonalphabetic input discarded:
(characters-as-strings"a b"); => ("a" "b")(characters-as-strings"a cb"); => ("a" "c" "b")Next,prompt-move reads the player’s input and acts on it:
(defnprompt-move[board](println"\nHere's your board:")(print-boardboard)(println"Move from where to where? Enter two letters:")(let[input(mapletter->pos(characters-as-strings(get-input)))](if-let[new-board(make-move➊board(firstinput)(secondinput))](user-entered-valid-movenew-board)(user-entered-invalid-moveboard))))At ➊,make-move returns nilif the player’s move was invalid, and you use that information to inform her of her mistake with theuser-entered-invalid-move function. You pass the unmodified board touser-entered-invalid-move so that it can prompt the player with the board again. Here’s the function definition:
(defnuser-entered-invalid-move"Handles the next step after a user has entered an invalid move"[board](println"\n!!! That was an invalid move :(\n")(prompt-moveboard))However, if the move is valid, thenew-board is passed off touser-entered-valid-move, which hands control back toprompt-move if there are still moves to be made:
(defnuser-entered-valid-move"Handles the next step after a user has entered a valid move"[board](if(can-move?board)(prompt-moveboard)(game-overboard)))In our board creation functions, we saw how recursion was used to build up a value using immutable data structures. The same thing is happening here, only it involves two mutually recursive functions and some user input. No mutable attributes in sight!
What happens when the game is over? This is what happens:
(defngame-over"Announce the game is over and prompt to play again"[board](let[remaining-pegs(count(filter:pegged(valsboard)))](println"Game over! You had"remaining-pegs"pegs left:")(print-boardboard)(println"Play again? y/n [y]")(let[input(get-input"y")](if(="y"input)(prompt-rows)(do(println"Bye!")(System/exit0))))))All that’s going on here is that the game tells you how you did, prints the final board, and prompts you to play again. If you selecty, the game callsprompt-rows, which brings us to the final set of functions, those used to start a new game:
(defnprompt-empty-peg[board](println"Here's your board:")(print-boardboard)(println"Remove which peg? [e]")(prompt-move(remove-pegboard(letter->pos(get-input"e")))))(defnprompt-rows[](println"How many rows? [5]")(let[rows(Integer.(get-input5))board(new-boardrows)](prompt-empty-pegboard)))You useprompt-rows to start a game, getting the player’s input on how many rows to include. Then you pass control on toprompt-empty-peg so the player can tell the game which peg to remove first. From there, the program prompts you for moves until there aren’t any moves left.
Even though all of this program’s side effects are relatively harmless (all you’re doing is prompting and printing), sequestering them in their own functions like this is a best practice for functional programming. Ingeneral, you will reap more benefits from functional programming if you identify the bits of functionality that are referentially transparent and side-effect free, and place those bits in their own functions. These functions arenot capable of causing bizarre bugs in unrelated parts of your program.They’re easier to test and develop in the REPL because they rely only on thearguments you pass them, not on some complicated hidden state object.
Pure functions are referentially transparent and side-effect free, which makes them easy to reason about. To get the most from Clojure, try to keep your impure functions to a minimum. In an immutable world, youuse recursion instead offor/while loops, and function composition instead of successions of mutations. Pure functions allow powerful techniques like function composition functions and memoization. They’re also super fun!
One of the best ways to develop your functional programming skills is to try to implement existing functions. To that end, most of the following exercises suggest a function for you to implement, but don’t stop there;go through the Clojure cheat sheet (http://clojure.org/cheatsheet/) andpick more!
(comp :intelligence :attributes) to create a function thatreturns a character’s intelligence. Create a new function,attr, that you can call like(attr :intelligence) and that does the same thing.comp function.assoc-in function. Hint: use theassoc function and define its parameters as[m [k & ks] v].update-in function.update-in.