Functional programming and DOM manipulation

2020-06-14 04:14发布

问题:

How is most "pure" way to manipulate DOM in script written in "functional" way.

For example if I simply need to change element width should I use typical syntax like:

document.querySelector(".class").style.width = ...

or write dedicated function, like:

function resize(el, w) {
    return el.style.width = w;
}

resize(document.querySelector(".class", 100));

I think, I understand general idea of functional programming but all examples I encountered are focused on manipulating numbers or strings. Manipulating DOM is considered side-effect so I'm wondering how to do it in correct way.

EDIT:

Updated code below, the question is, is iit functionall? If not I'd be gratefull for suggestion how to rewrite it.

"use strict";

const outerW = num => 100 * num + "%";
const innerW = num => 100 / num + "%";
const setW = (el, w) => el.style.width = w;

const mask = document.querySelector(".slider__slides__mask");
const slides = document.querySelectorAll(".slider__slide");

setW(mask, outerW(slides.length));
slides.forEach(function(item) {
    setW(item, innerW(slides.length));
});

回答1:

Every meaningful program must eventually carry out some effects, whether it is in functional or imperative style. The idea of functional programming is to shift theses effects to the edge of a program, so that a large portion remains pure. This makes it much easier to reason about it.

But how can this be put to practice?

Make functions composable

Javascript functions are usually defined as multi-argument functions. Hence we can't defer their evaluation at the calling side:

// pure

const add = (x, y) => x + y;

const log = x => console.log(x);

// impure

log(add(2, 3));

It's the same with the imperative style:

let x = 2, y = 3;

// impure

console.log(x + y);

The consequence is that the pure computation (add) can not be separated from the effect (log). This isn't a big deal for this sketch, but as soon as your program grows bigger, these scattered effects impair its readability and comprehensibility.

To prevent this behavior functions must become composable, i.e. their last argument must be partially applicable:

// pure

const comp = (f, g) => x => f(g(x));

const add = x => y => x + y;

const log = x => console.log(x);

const addAndLog = comp(log, add(2));

// impure

addAndLog(3);

You might want to look into currying for more information.

Wrap effects in thunks

Using thunks we can move the effects even further to the edges. A thunk is just a function that expects no arguments and thus represents a deferred computation:

// pure

const comp = (f, g) => x => f(g(x));

const add = x => y => x + y;

const log = x => () => console.log(x);

const addAndLog = comp(log, add(2));

const eventuallyLog = addAndLog(3); // still pure

// impure (release the effect)

eventuallyLog(); // invoke the thunk

You might want to look into IO monads for more information.

An (almost) real world example

// pure

const on = (type, element) => f => {
  element.addEventListener(type, f, true);
  return () => element.removeEventListener(type, f, true);
}

const compose = (...fs) => x => fs.reduce((acc, f) => f(acc), x);

const countFrom = x => () => (x++, x);

const log = x => console.log(x);

const filter = pred => f => x => pred(x) ? f(x) : x;

const even = x => x % 2 === 0;

const concat = y => x => x + y;

const filterEven = filter(even);

const clickStream = on("click", document);

const computation =
 compose(countFrom(0), filterEven(compose(concat("!"), log)));

// impure (release the effects)

console.log("click on the section above");

clickStream(computation);

As a side effect (pun intended)

compose(countFrom(0), filterEven(compose(concat("!"), log)))

reads like plain English. I was recently told that this is not a desirable property. Well, I disagree.