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