9

I've been learning some functional programming with JavaScript lately, and wanted to put my knowledge to the test by writing a simple ToDo app with just functional programming. However, I'm not sure how does one store the state of the list in a pure functional way, since functions are not allowed to have side effects. Let me explain with an example.

Let's say I have a constructor called "Item", which just has the task to be done, and a uuid to identify that item. I also have an items array, which holds all the current items, and an "add" and "delete" functions, like so:

function Item(name){
    this.name = name;
    this.uuid = uuid(); //uuid is a function that returns a new uuid
}

const items = [];

function addItem(name){
    const newItem = new Item(name);
    items.push(newItem);
}

function deleteItem(uuid){
    const filteredItems = items.filter(item => item.uuid !== uuid);
    items = filteredItems
}

Now this works perfectly, but as you can see functions are not pure: they do have side effects and don't return anything. With this in mind, I try to make it functional like this:

function Item(name){
    this.name = name;
    this.uuid = uuid(); //uuid is a function that returns a new uuid
}

const items = [];

function addItem(array, constructor, name){
    const newItem = new constructor(name);
    return array.concat(newItem);
}

function removeItem(array, uuid){
    return array.filter(item => item.uuid !== uuid);
}

Now the functions are pure (or so I think, correct me if I'm wrong), but in order to store the list of items, I need to create a new array each time I add or remove an item. Not only this seems incredibly inefficient, but I'm also not sure how to properly implement it. Let's say that I want to add a new item to the list each time a button is pressed in the DOM:

const button = document.querySelector("#button") //button selector
button.addEventListener("click", buttonClicked)

function buttonClicked(){
    const name = document.querySelector("#name").value
    const newListOfItems = addItem(items, Item, name);
}

This is once again not purely functional, but there is yet another problem: this will not work properly, because each time the function gets called, it will create a new array using the existing "items" array, which is itself not changing (always an empty array). To fix this, I can only think of two solutions: modifying the original "items" array or store a reference to the current items array, both of which involve the functions having some kind of side effects.

I've tried to look for ways to implement this but haven't been successful. Is there any way to fix this using pure functions?

Thanks in advance.

7
  • 2
    I'd say you do not store anything in the pure functional part of your program. A function has no state, just inputs and an output if it is pure. The only thing you can do is store the state in a "not pure" part of your program, then passing it to a function that will compute the next state of your app. But you always need to have side-effect, otherwise, nothing happens: the memory isn't altered, nothing is displayed... So you cannot be 100% pure functional programing. Commented Apr 5, 2019 at 15:33
  • functions here should be unaware of the original item, to me. In a nutshell, they should not take care whether the array is new or not, they should just alter it. Somewhere else, in your code, you should handle whether the array to alter should be duplicated in order to avoid altering existing references or not. You can do that with pure functions, of course, you just need to think where you want to store the array and when you want to alter an array without affecting other references to it and when you don't. Commented Apr 5, 2019 at 15:37
  • You can't avoid storing the state of the app somehow. But yes, the functional way of life is: not to mutate the state of the app, instead create a new one with a pure function that takes the previous state and change/action as arguments. Commented Apr 5, 2019 at 15:38
  • Your constructor isn't pure since uuid() isn't pure. Randomness is also an effect. Don't focus on purity in Javascript though. Btw., you shouldn't rely on this but simply construct the object through a literal, because this is the first step towards side effects. Commented Apr 5, 2019 at 15:45
  • 3
    The problem is the Array type, which is inherently imperative. The functional paradigm requires its own purely functional data types with certain traits like structural sharing. Look into immutablejs in order to get an idea. Commented Apr 5, 2019 at 15:47

1 Answer 1

2

The model–view–controller pattern is used to solve the state problem you described. Instead of writing a lengthy article about MVC, I'll teach by demonstration. Let's say that we're creating a simple task list. Here are the features that we want:

  1. The user should be able to add new tasks to the list.
  2. The user should be able to delete tasks from the list.

So, let's dive in. We'll begin by creating the model. Our model will be a Moore machine:

// The arguments of createModel are the state of the Moore machine.
//                    |
//                    v
const createModel = tasks => ({
    // addTask and deleteTask are the transition functions of the Moore machine.
    // They return new updated Moore machines and are purely functional.
    addTask(task) {
        if (tasks.includes(task)) return this;
        const newTasks = tasks.concat([task]);
        return createModel(newTasks);
    },
    deleteTask(someTask) {
        const newTasks = tasks.filter(task => task !== someTask);
        return createModel(newTasks);
    },
    // Getter functions are the outputs of the Moore machine.
    // Unlike the above transition functions they can return anything.
    get tasks() {
        return tasks;
    }
});

const initialModel = createModel([]); // initially the task list is empty

Next, we'll create the view which is a function that given the output of the model returns a DOM list:

// createview is a pure function which takes the model as input.
// It should only use the outputs of the model and not the transition functions.
// You can use libraries such as virtual-dom to make this more efficient.
const createView = ({ tasks }) => {
    const input = document.createElement("input");
    input.setAttribute("type", "text");
    input.setAttribute("id", "newTask");

    const button = document.createElement("input");
    button.setAttribute("type", "button");
    button.setAttribute("value", "Add Task");
    button.setAttribute("id", "addTask");

    const list = document.createElement("ul");

    for (const task of tasks) {
        const item = document.createElement("li");

        const span = document.createElement("span");
        span.textContent = task;

        const remove = document.createElement("input");
        remove.setAttribute("type", "button");
        remove.setAttribute("value", "Delete Task");
        remove.setAttribute("class", "remove");
        remove.setAttribute("data-task", task);

        item.appendChild(span);
        item.appendChild(remove);

        list.appendChild(item);
    }

    return [input, button, list];
};

Finally, we create the controller which connects the model and the view:

const controller = model => {
    const app = document.getElementById("app"); // the place we'll display our app

    while (app.firstChild) app.removeChild(app.firstChild); // remove all children

    for (const element of createView(model)) app.appendChild(element);

    const newTask = app.querySelector("#newTask");
    const addTask = app.querySelector("#addTask");
    const buttons = app.querySelectorAll(".remove");

    addTask.addEventListener("click", () => {
        const task = newTask.value;
        if (task === "") return;
        const newModel = model.addTask(task);
        controller(newModel);
    });

    for (const button of buttons) {
        button.addEventListener("click", () => {
            const task = button.getAttribute("data-task");
            const newModel = model.deleteTask(task);
            controller(newModel);
        });
    }
};

controller(initialModel); // start the app

Putting it all together:

// The arguments of createModel are the state of the Moore machine.
//                    |
//                    v
const createModel = tasks => ({
    // addTask and deleteTask are the transition functions of the Moore machine.
    // They return new updated Moore machines and are purely functional.
    addTask(task) {
        if (tasks.includes(task)) return this;
        const newTasks = tasks.concat([task]);
        return createModel(newTasks);
    },
    deleteTask(someTask) {
        const newTasks = tasks.filter(task => task !== someTask);
        return createModel(newTasks);
    },
    // Getter functions are the outputs of the Moore machine.
    // Unlike the above transition functions they can return anything.
    get tasks() {
        return tasks;
    }
});

const initialModel = createModel([]); // initially the task list is empty

// createview is a pure function which takes the model as input.
// It should only use the outputs of the model and not the transition functions.
// You can use libraries such as virtual-dom to make this more efficient.
const createView = ({ tasks }) => {
    const input = document.createElement("input");
    input.setAttribute("type", "text");
    input.setAttribute("id", "newTask");

    const button = document.createElement("input");
    button.setAttribute("type", "button");
    button.setAttribute("value", "Add Task");
    button.setAttribute("id", "addTask");

    const list = document.createElement("ul");

    for (const task of tasks) {
        const item = document.createElement("li");

        const span = document.createElement("span");
        span.textContent = task;

        const remove = document.createElement("input");
        remove.setAttribute("type", "button");
        remove.setAttribute("value", "Delete Task");
        remove.setAttribute("class", "remove");
        remove.setAttribute("data-task", task);

        item.appendChild(span);
        item.appendChild(remove);

        list.appendChild(item);
    }

    return [input, button, list];
};

const controller = model => {
    const app = document.getElementById("app"); // the place we'll display our app

    while (app.firstChild) app.removeChild(app.firstChild); // remove all children

    for (const element of createView(model)) app.appendChild(element);

    const newTask = app.querySelector("#newTask");
    const addTask = app.querySelector("#addTask");
    const buttons = app.querySelectorAll(".remove");

    addTask.addEventListener("click", () => {
        const task = newTask.value;
        if (task === "") return;
        const newModel = model.addTask(task);
        controller(newModel);
    });

    for (const button of buttons) {
        button.addEventListener("click", () => {
            const task = button.getAttribute("data-task");
            const newModel = model.deleteTask(task);
            controller(newModel);
        });
    }
};

controller(initialModel); // start the app
<div id="app"></div>

Of course, this is not very efficient because you are updating the entire DOM every time the model is updated. However, you can use libraries like virtual-dom to fix that.

You may also look at React and Redux. However, I'm not a big fan of it because:

  1. They use classes, which makes everything verbose and clunky. Although, you can make functional components if you really want to.
  2. They combine the view and the controller, which is bad design. I like to put models, views, and controllers in separate directories and then combine them all in a third app directory.
  3. Redux, which is used to create the model, is a separate library from React, which is used to create the view–controller. Not a dealbreaker though.
  4. It's unnecessarily complicated.

However, it is well-tested and supported by Facebook. Hence, it's worth looking at.

Sign up to request clarification or add additional context in comments.

4 Comments

Thanks for your answer! It was really helpful. I have however, some additional questions: 1.- You say view is a pure function, but as I understand, it is not, because it directly creates elements on the document object, which was not passed into view as an argument. Am I wrong in this? 2.- You say the createModel function is a Moore machine, but according to wikipedia, a Moore machine's output only depends on its state. However, this functions output also depends on the argument passed to it, a tasks object. Again, am I wrong in this? I'll take a look at react and redux too, thanks
The createView function doesn't have to create actual DOM elements. For example, you can use the virtual-dom library to create a data structure that can later used to efficiently patch the real DOM tree, in which case the createView function is indeed pure. However, even if you did what I did above it's still considered a pure function because all you are doing is creating new DOM elements. You aren't adding the new elements to the existing DOM tree. Hence, nothing is being updated. Indeed, purity is subjective. A function can be internally impure but externally pure.
By the way, the internal-external purity distinction is not just theoretical. All programs running on a computer (whether you consider them pure or impure) are eventually executed as machine level instructions, which are impure. Therefore, without the internal-external purity distinction every computer program (including the ones you consider to be pure functions) would be impure.
Indeed, a Moore machine's output only depends upon its state. So, where do you get this state from? You get it as an argument. To convince you, consider the following definition of a Moore machine in Haskell: data Moore a b s = Moore { state :: s, transition:: (s, a) -> s, output :: s -> b }. Notice that the state is always given as an argument to the transition and output functions. Hence, we can cancel it out as follows: data Moore a b = Moore { transition :: a -> Moore a b, output :: b }. Notice that the return type transition changed. This is because we canceled out the state.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.