4

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?

2 Answers 2

4

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" ] }
// {}

Sign up to request clarification or add additional context in comments.

2 Comments

Very nice answer, especially great for educational purposes. However, even if you apply immutable functionality under the hook (imperative api) you're still not really coding in the functional programming way. Also in real code, instead of making my own immutable map, I'd probably go with either ImmutableJs or immer for doing this. Not sure which is best, but immer looks interesting.
This is a functional api though, not an imperative one. Notice the functions receive arguments, return values, do not mutate their inputs, and do not produce side effects (pure). Statements like arr.push(x), obj.prop = val, or map.set(k,v) are imperative. Given your OOP background, you might choose Immer if you think imperatively and prefer an imperative api. Or choose ImmutableJS or Mori if you want to challenge yourself and solve problems using a functional way of thinking.
0

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.

4 Comments

Thanks, but in functional programming mutation is not possible. That's why I think both the Map and arrays should be read only. Which I guess also means that the addBar function will need to return its value instead of .set on the Map or foos =.
edited my answer - you also need to decide where you want to be able to test "pure" function and when you want to be able to test including a state (like in the class).
I really like your answer, and I might just go with that, but from what I can read, in FP there are no concept of classes and objects but only functions - and ideally I'd like to stick to FP if possible. I guess with your return I have my function, and can just leave out the class :-)
I had to modify it slightly to fix an error: ``` [key]: Array.isArray(foos[key]) ? [...foos[key], bar] : [bar] }; ``` So I guess your answer solves it, but back to my other question, is functional programming / immutability not a lot slower for these kinds of operations... when you have a large map / array (in this case a large map with large arrays inside it) that gets new elements added to it every other second? Copying the entire map and all the arrays inside seems like an awful lot of work, just to add an item. Compare that to mutability ot mymap.get(key).push(elem).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.