Atoms in Clojure - How to Create and Manipulate Them

Atoms are thread-safe containers for handling mutable state. In this chapter, we'll learn how to create and manipulate atoms. We'll also learn how to hook into an atom's lifecycle and execute side-effects.

Project Source Code

Get the project source code below, and follow along with the lesson material.

Download Project Source Code

To set up the project on your local machine, please follow the directions provided in the README.md file. If you run into any issues with running the project source code, then feel free to reach out to the author in the course's Discord channel.

This lesson preview is part of the Tinycanva: Clojure for React Developers course and can be unlocked immediately with a \newline Pro subscription or a single-time purchase. Already have access to this course? Log in here.

This video is available to students only
Unlock This Course

Get unlimited access to Tinycanva: Clojure for React Developers, plus 70+ \newline books, guides and courses with the \newline Pro subscription.

Thumbnail for the \newline course Tinycanva: Clojure for React Developers
  • [00:00 - 00:09] All closure data structures are immutable. However, the real world might not always be so. Any useful application would have some state that changes over time.

    [00:10 - 00:21] Closure atoms are thread-safe constructs to accommodate a mutable state. In a true multi-threaded environment like the JVM, atoms guarantee atomic updates across threads.

    [00:22 - 00:29] The examples in this chapter decide in the first project.atoms namespace. We suggest you create this file and follow along.

    [00:30 - 00:36] New atoms can be created using the atom function. This function takes the initial value of the atom as an argument.

    [00:37 - 00:52] The initial value can be anything, a number, a map, a function, or even other atoms. The atom function also takes two optional keyword arguments, a validator, which is a predicate function that's run each time before updating the value.

    [00:53 - 01:01] If this returns false, the value is not updated. And a metadata map, which is a set of key value pairs to hold atoms metadata.

    [01:02 - 01:10] If you eval an atom in line, you would see its initial value and the wrapping object. This might differ depending on the language underneath.

    [01:11 - 01:17] For JavaScript, it looks like so. A closure atom is an instance of a closure ref, or reference for short.

    [01:18 - 01:30] We will not go into the details of refs right now, but you can think of a ref as a base class for an atom. To get the current value of an atom or any ref, we need to use the dref method.

    [01:31 - 01:38] There are two ways to dref. The literal way using the adderit prefix and the functional way using the dref function.

    [01:39 - 01:50] If you want to update the value without caring about the initial value, you can use the reset function on an atom. This function takes two arguments, the atom to reset and its new value.

    [01:51 - 02:00] The functions ending with an exclamation sign, conventionally denote an unsafe or impure operation. The exclamation sign is usually read as bang.

    [02:01 - 02:15] If the new value was set on the atom, then the newly set value is returned and the atom is updated in place. If the value is not set, maybe because it failed the validation, an error is raised and the atom is left untouched.

    [02:16 - 02:28] In cases where the update depends on the previous state of the atom, for example adding to a vector, the swap function can be used. Swap signature is swap atom f n arcs.

    [02:29 - 02:38] And arcs signify zero or more additional arguments. f is a pure function that is called with the current value of the atom and the arguments passed along.

    [02:39 - 02:50] The atom's value is then updated to the return value of this function and a newly set value is returned. Consume pop add an element and remove the last element from the vector respectively.

    [02:51 - 02:59] Asok or associate adds a key value pair to a map. Do you recall your first encounter with these functions in the syntax and native data types chapter?

    [03:00 - 03:07] Do you remember the immutable nature of these operations? Asok does not update the original map but returns a new one.

    [03:08 - 03:13] Atoms help us mutate state safely. You can add a hook that watches changes made to an atom state.

    [03:14 - 03:24] This is useful in cases where you want an impure operation to run after the state has changed. For example, you might want to log the changes or re-render the UI when the atom updates.

    [03:25 - 03:38] The addWatch function takes an atom and identifies a keyword and a function to be called when the state changes. When the reset function is executed, a log statement is printed in the runtime, i.e. the running node script.

    [03:39 - 03:49] A watch it can be removed using the removeWatch function by passing in the atom and the id of the watch. So far, all the atoms that we used were defined using the def macro.

    [03:50 - 03:56] The variables produced using def are global in that namespace. The let function lets you define local variables.

    [03:57 - 04:02] A let definition is also called a letbinding. Let requires a vector with an even number of elements.

    [04:03 - 04:11] The variables defined in a letbinding are available only inside the let closure . The last element of the let list is the return value.

    [04:12 - 04:19] In this example, the atom A will be returned on evaluation. The usage of letbinding is not limited to atoms.

    [04:20 - 04:27] Any valid closure data structure can be defined in a letbinding. In this chapter, we learned about atoms and how to handle mutable state.

    [04:28 - 04:38] We also learned that atoms are implementation of closure refs. We saw how to watch changes and execute side effects on atoms and closed with the introduction to lead bindings.