Dac0d3r
Dac0d3r

Reputation: 1854

How to push an element to an array inside a Map using functional programming in TypeScript/JavaScript?

I just started my journey from a OOP background to learning FP, and the process of migrating from writing normal TypeScript (imperative?) to functional TypeScript code. Unfortunately I already struggle with figuring out how to change this into functional code:

const foos: Map<
  string,
  Bar[]
> = new Map();

export const addBar = (
  key: string,
  bar: Bar
) => {
  const foo = foos.get(key);

  if (foo) {
    foo.push(bar);
  } else {
    foos.set(key, [bar]);
  }
};

I understand how .map .filter .concat can be used on an array, but how to deal with a Map that holds arrays?

Regarding the foos Map it I guess the Map itself needs to be read only, and also the array of Bar inside it, so .set .push is not possible. But if I cannot call .set on the Map because it is read only, does it even make sense to use a Map or should I just use an object?

Without mutability how to push an element to an array inside the Map values (or make a New map with the array if the key does not already exist, like in the code above)?

And is this performant enough, since I will need to add an new element to the array every other seconds, will the immutable way of copying the entire map (including its many arrays) each time a change happens not perform a lot worse than if I had just mutated the array like you'd typically do?

Upvotes: 4

Views: 4690

Answers (2)

Guest
Guest

Reputation: 81

You simply cannot use the native Map because it only provides an imperative interface.

You could reach for an open source library such as the popular ImmutableJS.

Or you could write your own persistent (immutable) data structures. The essential requirement is that the operations provided by your data structure do not modify the inputs. Instead a new data structure is returned with each operation -

const PersistentMap =
  { create: () =>
      ({})
  , set: (t = {}, key, value) =>
      ({ ...t, [key]: value })      // <-- immutable operation
  }

We first look at an empty map, the result of a set operation, and then make sure that empty map is not modified -

const empty =
  PersistentMap.create()

console.log
  ( empty
  , PersistentMap.set(empty, "hello", "world")
  , empty
  )

// {}
// { hello: "world" }
// {}

Now let's look at a new intermediate state, m1. Each time we see set returns a new persistent map and does not modify the input -

const m1 =
  PersistentMap.set(empty, "hello", "earth")

console.log
  ( m1
  , PersistentMap.set(m1, "stay", "inside")
  , m1
  )
// { hello: "earth" }
// { hello: "earth", stay: "inside" }
// { hello: "earth" }

Now to answer your question, we can add a push operation to our PersitentMap - we only have to ensure we do not modify the input. Here's one possible implementation -

const PersistentMap =
  { // ...

  , push: (t = {}, key, value) =>
      PersistentMap.set            // <-- immutable operation
        ( t
        , key
        , Array.isArray(t[key])
            ? [ ...t[key], value ] // <-- immutable operation
            : [ value ]
        )
  }

We see push in action below. Note that m2 nor empty are changed as a result -

const m2 =
  PersistentMap.push(empty, "fruits", "apple")

console.log
  ( m2
  , PersistentMap.push(m2, "fruits", "peach")
  , m2
  , empty
  )

// { fruits: [ "apple" ] }
// { fruits: [ "apple", "peach" ] }
// { fruits: [ "apple" ] }
// {}

Expand the snippet below to verify the results in your own browser

const PersistentMap =
  { create: () =>
      ({})
  , set: (t = {}, key, value) =>
      ({ ...t, [key]: value })
  , push: (t = {}, key, value) =>
      PersistentMap.set
        ( t
        , key
        , Array.isArray(t[key])
            ? [ ...t[key], value ]
            : [ value ]
        )
  }

const empty =
  PersistentMap.create()

console.log
  ( empty
  , PersistentMap.set(empty, "hello", "world")
  , empty
  )
// {}
// { hello: "world" }
// {}

const m1 =
  PersistentMap.set(empty, "hello", "earth")

console.log
  ( m1
  , PersistentMap.set(m1, "stay", "inside")
  , m1
  )
// { hello: "earth" }
// { hello: "earth", stay: "inside" }
// { hello: "earth" }

const m2 =
  PersistentMap.push(empty, "fruits", "apple")

console.log
  ( m2
  , PersistentMap.push(m2, "fruits", "peach")
  , m2
  , empty
  )
// { fruits: [ "apple" ] }
// { fruits: [ "apple", "peach" ] }
// { fruits: [ "apple" ] }
// {}

Upvotes: 4

Mor Shemesh
Mor Shemesh

Reputation: 2887

I think it depends on what you want to achieve. If you want your code to be testable, FP, doesn't always mean just writing functions, you can still use classes but if you have a complex piece of code you want to test separately, you can export that piece to test that, and it would look something like that:

// types.ts
type FooDis = Record<string, object[]>;

// addBarToFoos.ts
export const addBarToFoos = (foos: FooDis) => (key: string, bar: object): FooDis {
  foos = {
    ...foos,
    [key]: [
      ...foos[key],
      bar
    ]
  };

  return foos;
}

// FooClass.ts 
export class FooClass {
  private foos: FooDis = {};

  addBar(key: string, bar: object) {
    this.foos = addBarToFoos(this.foos)(key, bar);
  }
}

This way, the "complex" method is separately testable without external dependencies, and you have an implementation that uses that method.

Upvotes: 0

Related Questions