Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to work with javascript Map without mutations

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.

like image 978
Sergei Panfilov Avatar asked Dec 30 '22 17:12

Sergei Panfilov


2 Answers

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 ) );
like image 120
Quentin Avatar answered Jan 12 '23 08:01

Quentin


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.

like image 35
Iven Marquardt Avatar answered Jan 12 '23 09:01

Iven Marquardt