Immutability Helper
Problem
Creating clones of immutable objects with minor alterations can be a tedious process. Write a class ImmutableHelper that serves as a tool to help with this requirement. The constructor accepts an immutable object obj which will be a JSON object or array.
The class has a single method produce which accepts a function mutator.
The function returns a new object which is similar to the original except it has those mutations applied.
mutator accepts a proxied version of obj. A user of this function can (appear to) mutate this object, but the original object obj should not actually be effected.
For example, a user could write code like this:
const originalObj = {"x": 5}; const helper = new ImmutableHelper(originalObj); const newObj = helper.produce((proxy) => { proxy.x = proxy.x + 1; }); console.log(originalObj); // {"x": 5} console.log(newObj); // {"x": 6}
Properties of the mutator function:
- It will always return
undefined. - It will never access keys that don't exist.
- It will never delete keys (
delete obj.key) - It will never call methods on a proxied object (
push,shift, etc). - It will never set keys to objects (
proxy.x = {})
Note on how the solution will be tested: the solution validator will only analyze differences between what was returned and the original obj. Doing a full comparison would be too computationally expensive. Also, any mutations to the original object will result in a wrong answer.
Examples
Example 1:
Input:
obj = {"val": 10},
mutators = [
proxy => { proxy.val += 1; },
proxy => { proxy.val -= 1; }
]
Output:
[
{"val": 11},
{"val": 9}
]
Explanation:
const helper = new ImmutableHelper({val: 10});
helper.produce(proxy => { proxy.val += 1; }); // { "val": 11 }
helper.produce(proxy => { proxy.val -= 1; }); // { "val": 9 }
Example 2:
Input:
obj = {"arr": [1, 2, 3]}
mutators = [
proxy => {
proxy.arr[0] = 5;
proxy.newVal = proxy.arr[0] + proxy.arr[1];
}
]
Output:
[
{"arr": [5, 2, 3], "newVal": 7 }
]
Explanation: Two edits were made to the original array. The first element in the array was to set 5. Then a new key was added with a value of 7.
Example 3:
Input:
obj = {"obj": {"val": {"x": 10, "y": 20}}}
mutators = [
proxy => {
let data = proxy.obj.val;
let temp = data.x;
data.x = data.y;
data.y = temp;
}
]
Output:
[
{"obj": {"val": {"x": 20, "y": 10}}}
]
Explanation: The values of "x" and "y" were swapped.
Constraints:
2 <= JSON.stringify(obj).length <= 4 * 10^5mutatorsis an array of functionstotal calls to produce() < 10^5
Solution
Method 1 – Proxy-based Shallow Copy
Intuition
We want to allow users to "mutate" an object or array, but without changing the original. By using a Proxy, we can intercept mutations and apply them to a shallow copy, returning the new object while keeping the original unchanged.
Approach
- In the
producemethod, create a shallow copy of the original object (array or object). - Use a Proxy to intercept set operations and apply them to the shallow copy.
- Pass the Proxy to the mutator function.
- Return the shallow copy after mutation.
- The original object remains unchanged.
Code
JavaScript
class ImmutableHelper {
constructor(obj) {
this.obj = obj;
}
produce(mutator) {
const isArr = Array.isArray(this.obj);
const ans = isArr ? [...this.obj] : { ...this.obj };
const proxy = new Proxy(ans, {
set(target, prop, value) {
target[prop] = value;
return true;
},
get(target, prop) {
return target[prop];
}
});
mutator(proxy);
return ans;
}
}
TypeScript
class ImmutableHelper<T extends object> {
obj: T;
constructor(obj: T) {
this.obj = obj;
}
produce(mutator: (proxy: T) => void): T {
const isArr = Array.isArray(this.obj);
const ans = isArr ? [...(this.obj as any)] : { ...this.obj };
const proxy = new Proxy(ans, {
set(target, prop, value) {
target[prop] = value;
return true;
},
get(target, prop) {
return target[prop];
}
});
mutator(proxy);
return ans as T;
}
}
Complexity
- ⏰ Time complexity:
O(n)— Shallow copy of the object/array is made, where n is the number of keys/elements. - 🧺 Space complexity:
O(n)— Space for the shallow copy of the object/array.