DEV Community

Cover image for Making Choices
Andrew Meredith
Andrew Meredith

Posted on • Updated on • Originally published at learn-clojurescript.com

Making Choices

This article is Lesson 10 of Andrew's book, Learn ClojureScript


Up to this point, we have mostly been writing code that is has some starting point and progresses until it is complete.1 It has a well-defined task and must follow a well-defined series of instructions to accomplish this task. What we have been missing is the concept of a choice - being able to take a different path depending on some condition. Imagine a road that stretched for hundreds and hundreds of miles without ever intersecting with another road. If we were to drive along that road, it would be easy because we would never have to decide whether to take another route; however, the drive would also be fairly uneventful. It is the same with the code that we write: we could write scripts that simply run from top to bottom and have a clear linear execution, but then we would be missing out on the most interesting and rewarding programs.

In this chapter:

  • Apply if and when to make simple choices
  • Become familiar with the concept of truthiness and how it is defined
  • Choose between multiple options with cond

Example: Adventure Game

Text-Based Adventure Game

Text-Based Adventure Game

In this chapter, we will build a simple text-based adventure game. Adventure games are perfect for learning the concept of conditionals, since they are built around letting the user make a series of choices of how to navigate a virtual environment. So that we can focus on learning one building block at a time, we will use the author's bterm terminal emulator library. This will allow us to focus on the core concepts of control structures without getting mired in DOM manipulation and event handling or learning a large framework.

Making Simple Choices With if And when

When we are making choices, we usually need to determine if something is true or false: is a particular checkbox selected? Is the account balance below some threshold? Does the player have some particular potion in their inventory? These kinds of questions can only have one of 2 answers - "yes" or "no". Remember that ClojureScript works by evaluating expressions, so these questions usually take the form of an expression that evaluates to something that is either true or false. We could imagine the above questions translated to ClojureScript expressions:

Expressions That Ask Questions

;; Is the checkbox checked?
(aget my-checkbox "checked")

;; Is the account balance below a threshold?
(< (:balance account) low-balance-threshold)

;; Does the player have the potion of wisdom in their inventory?
(some #(= (:name %) "Potion of Wisdom")
      (get-inventory player))
Enter fullscreen mode Exit fullscreen mode

When we evaluate one of these expressions (assuming that the vars we reference are actually defined), the value will be something that we can consider to be either true or false.

Selecting With if

We often want to make one choice when the answer is true and another choice when the answer is false - in this situation, we can use ClojureScript's if special form. An if expression takes the following form:

(if test-expr then-expr else-expr)
Enter fullscreen mode Exit fullscreen mode

If takes 3 expressions: a test, a value to use if the test is true, and a value to use if the test is false. The entire if expression will evaluate to the value of either the second or third expression depending on the value of the first expression. To use the account balance example from above, we could write the following:

(def account-status
  (if (< (:balance account) low-balance-threshold)     <1>
    :low-balance                                       <2>
    :ok))                                              <3>
Enter fullscreen mode Exit fullscreen mode
  1. Test whether the balance is below some point
  2. If the test is true, evaluate to :low-balance
  3. If the test is false, evaluate to :ok

Note: Special Forms

When we write an s-expression, ClojureScript will evaluate it as long as the first symbol in the expression resolves to the name of a function, a macro, or a special form. While the call to if looks just like a function call, if is actually a special form rather than a function. We have now been writing functions for a while, and we will learn to write macros in a later chapter, but special forms are baked into the language because they are so fundamental that they cannot be implemented as a library function (or at least not efficiently). Thankfully, from our perspective as developers, we do not need to be concerned whether the specific thing that we are calling is a function, macro, or special form, since there is no difference in the way that they are called.

While the idea of an if statement should be familiar to any developer, there is a key difference between most languages' if statement and ClojureScript's if: in ClojureScript, if is an expression, so it always evaluates to a specific value. In an imperative language like JavaScript, the if statement usually makes a choice between which code branch to execute. The actual if statement does not yield a value. For instance, the following is not valid JavaScript:

JavaScript Ifs Are Not Expressions

// This will throw a SyntaxError
var answer = if (someCondition) {
   'Yes';
} else {
   'No';
}
Enter fullscreen mode Exit fullscreen mode

In JavaScript - as in most imperative languages - we use if statements to perform side effects such as conditionally setting a variable, prompting the user for input, or manipulating the DOM. In ClojureScript, we usually use the if expression to decide between two values. The entire expression will take the value of either the then expression or the else expression. Selecting between more than 2 values can also be accomplished by replacing either the then or else expression with another (nested) if expression.

Conditional Evaluation

Conditional Evaluation

Take care, however, as deeply nested if expressions can be difficult to read and can usually be replaced with a cond, which we will learn about later in this chapter.

Quick Review

  • Explain the difference between if in JavaScript and ClojureScript
  • How would you write an if expression that - given 2 numeric values, a and b - would evaluate to "greater" if a > b, "less" if a < b, or "same" if they are equal?

Conditional Evaluation With when

Closely related to if is the when expression. We can think of it as an if without an else expression:

(when test-expr some-value)
Enter fullscreen mode Exit fullscreen mode

When the test expression is true, the entire expression evaluates to the value given, and when the test is false, the expression evaluates to nil. In fact, when is just shorthand for an if where the else expression is nil:

(if test-expr some-value nil)
Enter fullscreen mode Exit fullscreen mode

The two common use cases for when are to transform a value only when it is non-nil and to perform some side effect when a certain condition holds true. For the first case, we often see code like the following:

(defn conversion-rate [sessions]              ;; <1>
  (let [users (user-count sessions)
        purchases (purchase-count sessions)]
    (when (> users 0)                         ;; <2>
      (/ purchases users))))
Enter fullscreen mode Exit fullscreen mode
  1. Define a function that gets the ratio of purchases to users
  2. Use when to prevent division by zero

For the second case, we will often want to perform some DOM manipulation or other side effect only in a specific case. For instance, we may want to pop up an error message when we receive a server error from a back-end API:

(when (< 499 (:status response))
  (show-error-notification (:body response)))
Enter fullscreen mode Exit fullscreen mode

Applying if and when

Considering the example of the adventure game, we can use use an if expression to determine what to do after prompting the user for a yes/no question. Let's take a quick step back to discuss the overall architecture of the game. We will represent the entire game as a map where the keys are the name of each state and the values are maps that represent a specific screen. The general shape of our game data structure is below:

We will represent our game as a collection of states with rules that determine how to move between states when the user makes some decision:

{:start { ... }
 :state-1 { ... }
 :state-2 { ... }
 :state-3 { ... }
 :win { ... }}
Enter fullscreen mode Exit fullscreen mode

Our game will start with the user in a spaceship at Starbase Lambda, and their goal is to uncover the location of the Tetryon Singularity. They will issue simple commands as well as answer "yes" or "no" questions.

Each state in the game (which we filled in with { ... } above) will contain a :type, :title, :dialog, and :transitions. The type determines what sort of state the game is in - e.g. :start, :win, or :lose - title and dialog determine what we display onscreen, and the transitions determine which state the user should transition to depending on the choice that they make. For example:

Example game state

{:type :start
 :title "Starbase Lambda"
 :dialog (str "Welcome, bold adventurer! You are about to embark on a dangerous "
              "quest to find the Tetryon Singularity.\nAre you up to the task?")
 :transitions {"yes" :embarked,
               "no" :lost-game}}
Enter fullscreen mode Exit fullscreen mode

When the user is in this state, we will print the title and dialog to the screen and prompt them for input. If they type "yes", we'll advance to the :embarked state; otherwise, we'll move on to the :lost-game state.

Prompting for Input

The first thing that we will want to do is display the title and dialog from whatever scene the user is currently in and prompt them for input. We'll handle the input later, so for now let's just think about how we can display the scene. The bterm library that we are using provides several useful functions for controlling output:

  • print - prints a screen to the terminal
  • println - prints a screen to the terminal with a trailing newline character
  • clear - clears any existing output from the terminal

With this in mind, let's think about how to display the scene. We always want to print the title and dialog, but we should also indicate if they have won or lost the game. In this case, we can display either, "You've Won!" or "Game Over". To accomplish this, we can first test the type of the current scene and only display the end game message if the type is either :win or :lose:

(when (or (= :win type)                 ;; <1>
          (= :lose type))
  ;; Display message                    ;; <2>
  )
Enter fullscreen mode Exit fullscreen mode
  1. Conditionally evaluate code only when the user is in an end-game state

Furthermore, we want to use a different message depending on whether the user has won or lost. We can accomplish this with if:

(io/println term
            (if (= :win type) "You've Won!" "Game Over"))  <1>
Enter fullscreen mode Exit fullscreen mode
  1. The if expression will evaluate to either "You've Won!" or "Game Over" depending on the value of type

Putting these pieces together with the printing of the title and dialog gives us something like this:

Prompting for input

(defn prompt [game current]                                      ;; <1>
  (let [scene (get game current)                                 ;; <2>
        type (:type scene)]
    (io/clear term)
    (when (or (= :win type)                                      ;; <3>
              (= :lose type))
      (io/print term
                (if (= :win type) "You've Won! " "Game Over "))) ;; <4>
    (io/println term (:title scene))                             ;; <5>
    (io/println term (:dialog scene))
    (io/read term #(on-answer game current %))))                 ;; <6>
Enter fullscreen mode Exit fullscreen mode
  1. This function takes the entire game data structure and the current scene
  2. Create 2 local bindings with let that we will use in the rest of the function
  3. Conditionally print an end-game message
  4. Determine which message to print
  5. Print the title and dialog no matter what the scene type is
  6. Handle whatever the user types using the on-answer function that we are about to write

Handling Input

Now that we have taken care of the display side of things, we will want to handle user input. In the previous snippet, we passed control to the on-answer function when the user entered an answer. This function, like prompt, is passed the entire game data structure as well as the key identifying the current scene; however, it is also passed the string that the user entered at the prompt. Using this information, we need to determine which scene to display next then prompt the user for input once more. Here is the skeleton of what this code should look like:

(defn on-answer [game current answer]
  (let [scene (get game current)
        next ;; TODO: determine the next state
        ]
    (prompt game next)))
Enter fullscreen mode Exit fullscreen mode

To start, we only need to handle responses of "yes" or "no". Since we are only deciding between 2 options, a single if expression will suffice:

(if (= "yes" answer)
  (get-in scene [:transitions "yes"])
  (get-in scene [:transitions "no"]))
Enter fullscreen mode Exit fullscreen mode

You Try It

There is another type of game state that we need to handle = :skip, which has the following shape:

{:type :skip
 :title "..."
 :dialog "..."
 :on-continue :next-state}
Enter fullscreen mode Exit fullscreen mode

Add another conditional to the on-answer function that will proceed to the next state regardless of what the user enters. A possible solution is given below:

(defn on-answer [game current answer]
  (let [scene (get game current)
        next (if (= :skip (:type scene))
               (:on-continue scene)
               (if (= "yes" answer)
                 (get-in scene [:transitions "yes"])
                 (get-in scene [:transitions "no"])))]
    (prompt game next)))
Enter fullscreen mode Exit fullscreen mode

Truthiness and Falsiness

Before continuing, let's take a brief step back to talk about the concept of truthiness in ClojureScript. The test expression that we pass to if or when can be an actual boolean value - true or false - but it does not have to be. As in JavaScript, we can pass any value as the test. Even if it is not a boolean, the language will either consider it to be "truthy" and pass the test or "falsy" and fail it.

Unlike JavaScript, which has a number of special cases that it considers to be falsey, ClojureScript follows a very simple rule: false and nil are falsy, and everything else is truthy.

ClojureSctipt's Truthiness Rule

false and nil are falsy, and all other values are truthy.

Quick Review

  • What is the value of (if TEST "Truthy" "Falsy") for each of the following values for "TEST":
    • true
    • false
    • "false"
    • ""
    • 0
    • nil
    • js/NaN
    • []

More Complex Choices With cond

With if and when, we have all that we technically need to handle any sort of decision-making that we need to do in code. However, we are often faced with cases in which if would be awkward to use. Consider adding more commands to our game so that the user could type "restart" to go back to the beginning or "help" to display the commands that are available. As we add more options, we would have to keep nesting more and moreif expressions - like using a pocket knife to carve a wooden sculpture, it could work, but the result would not be pleasant.

Enter cond and its cousins, condp and case. cond takes some expression and any number of test/result pairs, and the entire expression will evaluate to the "then" expression that comes after the first test that is truthy:

The Structure of cond

(cond
  test-1 then-1
  test-2 then-2
  ;; ...
  test-n then-n)
Enter fullscreen mode Exit fullscreen mode

It is idiomatic to use :else as the test expression for a "fall-through" value if no other test is truthy. Remember that only false and nil are falsy, so the keyword :else will always be truthy and will satisfy cond if no prior test does. Thinking about the additional commands that we would like to add to our game, this would be much simpler using cond.

Replacing Nested If With Cond

Replacing Nested If With Cond

Repeated Tests With condp

If this was as good as we can do, it would be a significant improvement, but we can still simplify things further with a more focused variation of cond called condp. Like cond, condp allows us to choose from among a number of options, but if there is a lot of common code in each of the test expressions, condp can usually help us factor it out. In our case, we test the value of "answer" for equality with some string in every test expression.

This is a great case for condp, which take a binary predicate - that is, a function that take two arguments and returns a boolean value, e.g. = - an expression to use as the righthand side in every test, and any number of lefthand expression/result pairs. It can also take an optional default value to use if none of the prior tests were truthy.

The Structure of condp

(condp pred expr
  test-expr-1 then-1
  test-expr-2 then-2
  ;; ...
  test-expr-n then-n
  default-expr)
Enter fullscreen mode Exit fullscreen mode

For every test expression/result pair, it applies the predicate to the test expression and the other expression and evaluates to the first result value whose test expression passed the predicate. On paper, this can be confusing, but seeing an example can help clarify things, so here is our text-based menu re-written using condp:

(condp
  =                                      <1>
  answer                                 <2>
  "restart" (prompt game :start)         <3>
  "help" (show-help game current)
  "save" (save-state current)
  "easter-egg" (rickroll)
  (next-state game current answer))      <4>
Enter fullscreen mode Exit fullscreen mode
  1. Use the = predicate function to test each option
  2. Pass answer as the righthand side in every test
  3. Each clause will be tested as (= "restart" answer)
  4. Provide a default expression if every prior test fails

Compared to our original implementation with nested if statements, this version using condp is quite succinct and readable. For this reason, condp is widely used to test multiple values when the full flexibility of case is not required.

Quick Review

  • Using cond write some code that will evaluate to :pos when a given number is positive, :neg when it is negative, and :zero when it is exactly zero
  • Write code that will do the same thing using condp

Summary

In this chapter, we learned what are usually referred to as the branching control structures. We learned that, in contrast to JavaScript an other imperative languages, these structures are used as expressions that choose between values rather than imperative statements that direct the flow of execution. We also looked at ClojureScript's concept of truthiness and how is is simpler than that of most other languages. We can now:

  • Choose between two values using if
  • Conditionally evaluate code using when
  • Simplify multiple-choice options using cond and condp

Even though they function a little differently in ClojureScript than in other languages, these branching mechanisms are one of the most fundamental building blocks that we need to write applications. Next, we will look at the other class of control structures - loops.


  1. We have come across some code samples that have used conditionals because they are almost unavoidable for any interesting program. 

Top comments (0)