blog

  • Home
  • blog
  • Immutable Data and Functional JavaScript with Mori

Immutable Data and Functional JavaScript with Mori

This article was peer reviewed by Craig Bilner and Adrian Sandu. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

Functional programming and immutable data are a current focus for many JavaScript developers as they try to find ways to make their code simpler and easier to reason about.

[author_more]

Although JavaScript has always supported some functional programming techniques, they’ve only really become popular in the last few years and traditionally there’s been no native support for immutable data either. JavaScript is still learning a lot about both and the best ideas are coming from languages that have tried and tested these techniques already.

In another corner of the programming world, Clojure is a functional programming language that’s dedicated to genuine simplicity, especially where data structures are concerned. Mori is a library that allows us to use Clojure’s persistent data structures directly from JavaScript.

This article will explore the rationale behind the design of these data structures and examine some patterns for using them to improve our applications. We could also think of this as the first stepping stone for JavaScript developers interested in programming with Clojure or ClojureScript.

What Is Persistent Data?

Clojure makes a distinction between persistent values that can’t be changed and transient values that have temporal lifetimes between mutations. Attempts to modify persistent data structures avoid mutating the underlying data by returning a new structure with the changes applied.

It may help to see what this distinction would look in a theoretical programming language.

// transient list
a = [1, 2, 3];
b = a.push(4);
// a = [1, 2, 3, 4]
// b = [1, 2, 3, 4]

// persistent list
c = #[1, 2, 3]
d = c.push(4);
// c = #[1, 2, 3]
// d = #[1, 2, 3, 4]

We can see that the transient list was mutated when we pushed a value onto it. Both a and b point to the same mutable value. In contrast, calling push on the persistent list returned a new value and we can see that c and d point to different to discrete lists.

These persistent data structures can’t be mutated, meaning that once we have a reference to a value, we also have a guarantee that it won’t ever be changed. These guarantees generally help us write safer and simpler code. For instance, a function that takes persistent data structures as arguments can’t mutate them and therefore if the function wants to communicate meaningful change, it must come from the return value. This leads to writing referentially transparent, pure functions, which are easier to test and optimize.

More simply, immutable data forces us to write more functional code.

What Is Mori?

Mori uses the ClojureScript compiler to compile the implementations for the data structures in Clojure’s standard library to JavaScript. The compiler emits optimized code, which means that without additional consideration, it’s not easy to communicate with compiled Clojure from JavaScript. Mori is the layer of additional consideration.

Just like Clojure, Mori’s functions are separated from the data structures that they operate on, which contrasts against JavaScript’s object oriented tendencies. We’ll find that this difference changes the direction we write code.

// standard library
Array(1, 2, 3).map(x => x * 2);
// => [2, 4, 6]

// mori
map(x => x * 2, vector(1, 2, 3))
// => [2, 4, 6]

Mori also uses structural sharing to make efficient changes to data by sharing as much of the original structure as possible. This allows persistent data structures to be nearly as efficient as regular transient ones. The implementations for these concepts are covered in much more detail in this video.

Why Is It Useful?

To begin with, let’s imagine we’re trying to track down a bug in a JavaScript codebase that we inherited. We’re reading over the code trying to figure out why we’ve ended up with the wrong value for fellowship.

const fellowship = [
  {
    title: 'Mori',
    race: 'Hobbit'
  },
  {
    title: 'Poppin',
    race: 'Hobbit'
  }
];

deletePerson(fellowship, 1);
console.log(fellowship);

What is the value of fellowship when it is logged to the console?

Without running the code, or reading the definition for deletePerson() there is no way to know. It could be an empty array. It could have three new properties. We would hope that it is an array with the second element removed, but because we passed in a mutable data structure, there are no guarantees.

Even worse, the function could keep hold of a reference and mutate it asynchronously in the future. All references to fellowship from here onwards are going to be working with an unpredictable value.

Compare this to an alternative with Mori.

import { vector, hashMap } from 'mori';

const fellowship = vector(
  hashMap(
    "name", "Mori",
    "race", "Hobbit"
  ),
  hashMap(
    "name", "Poppin",
    "race", "Hobbit"
  )
)

const newFellowship = deletePerson(fellowship, 1);
console.log(fellowship);

Regardless of the implementation of deletePerson(), we know that the original vector will be logged, simply because there is a guarantee that it can’t be mutated. If we want the function to be useful, then it should return a new vector with the specified item removed.

Understanding flow through functions that work on immutable data is easy, because we know that their only effect will be to derive and return a distinct immutable value.

Flow through functions that work on immutable data

Functions operating on mutable data don’t always return values, they can mutate their inputs and sometimes it’s left to the programmer to pick up the value again at the other side.

Flow through functions that work on mutable data

Continue reading %Immutable Data and Functional JavaScript with Mori%

LEAVE A REPLY