I'm working in a functional way in my JS project.
That's also means I do not mutate object
or array
entities. Instead I always create a new instance and replace an old one.
e.g.
let obj = {a: 'aa', b: 'bb'}
obj = {...obj, b: 'some_new_value'}
The question is:
How to work in a functional (immutable) way with javascript Maps?
I guess I can use the following code to add values:
let map = new Map()
...
map = new Map(map).set(something)
But what about delete items?
I cannot do new Map(map).delete(something)
, because the result of .delete
is a boolean.
P.S. I'm aware of existence of ImmutableJS, but I don't want to use it due to you never 100% sure if you are working now with a plain JS object, or with immutablejs' object (especially nested structures). And because of bad support of TypeScript, btw.
I cannot do new Map(map).delete(something);, because the result of .delete is a boolean.
You can use an interstitial variable. You can farm it out to a function if you like:
function map_delete(old_map, key_to_delete) {
const new_map = new Map(old_map);
new_map.delete(key_to_delete);
return new_map;
}
Or you can create get the entries in the map, filter them and create a new one from the result:
const new_map = new Map( Array.from(old_map.entries).filter( ([key, value]) => key !== something ) );
If you don't want to use a persistent map data structure, then you cannot get around mutations or have to conduct insanely inefficient shallow copies. Please note that mutations themselves aren't harmful, but only in conjunction with sharing the underlying mutable values.
If we are able to limit the way mutable values can be accessed, we can get safe mutable data types. They come at a cost, though. You cannot just use them as usual. As a matter of fact using them takes some time to get familiar with. It's a trade-off.
Here is an example with the native Map
:
// MUTABLE
const Mutable = clone => refType => // strict variant
record(Mutable, app(([o, initialCall, refType]) => {
o.mutable = {
run: k => {
o.mutable.run = _ => {
throw new TypeError("illegal subsequent inspection");
};
o.mutable.set = _ => {
throw new TypeError("illegal subsequent mutation");
};
return k(refType);
},
set: k => {
if (initialCall) {
initialCall = false;
refType = clone(refType);
}
k(refType);
return o;
}
}
return o;
}) ([{}, true, refType]));
const mutRun = k => o =>
o.mutable.run(k);
const mutSet = k => o =>
o.mutable.set(k);
// MAP
const mapClone = m => new Map(m);
const mapDelx = k => m => // safe-in-place-update variant
mutSet(m_ =>
m_.has(k)
? m_.delete(k)
: m_) (m);
const mapGet = k => m =>
m.get(k);
const mapSetx = k => v => // safe-in-place-update variant
mutSet(m_ => m_.set(k, v));
const mapUpdx = k => f => // safe-in-place-update variant
mutSet(m_ => m_.set(k, f(m_.get(k))));
const MutableMap = Mutable(mapClone);
// auxiliary functions
const record = (type, o) => (
o[Symbol.toStringTag] = type.name || type, o);
const app = f => x => f(x);
const id = x => x;
// MAIN
const m = MutableMap(new Map([[1, "foo"], [2, "bar"], [3, "baz"]]));
mapDelx(2) (m);
mapUpdx(3) (s => s.toUpperCase()) (m);
const m_ = mutRun(Array.from) (m);
console.log(m_); // [[1, "foo"], [3, "BAZ"]]
try {mapSetx(4) ("bat") (m)} // illegal subsequent mutation
catch (e) {console.log(e.message)}
try {mutRun(mapGet(1)) (m)} // illegal subsequent inspection
catch (e) {console.log(e.message)}
If you take a closer look at Mutable
you see it creates a shallow copy as well, but only once, initially. You can than conduct as many mutations as you want, until you inspect the mutable value the first time.
You can find an implementation with several instances in my scriptum library. Here is a post with some more background information on the concept.
I borrowed the concept from Rust where it is called ownership. The type theoretical background are affine types, which are subsumed under linear types, in case you are interested.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With