Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do you store the state of an array in functional JavaScript?

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.

like image 997
Reick Avatar asked Apr 05 '19 15:04

Reick


1 Answers

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.

like image 81
Aadit M Shah Avatar answered Nov 14 '22 23:11

Aadit M Shah