Hone logo
Hone
Problems

Implementing a Copy-On-Write Array in JavaScript

This challenge asks you to implement a data structure that behaves like an array but uses the copy-on-write (CoW) strategy for mutations. CoW is a memory management technique where a resource (in this case, an array) is duplicated only when a modification is attempted, rather than at the time of copying. This can significantly improve performance in scenarios with many reads and few writes.

Problem Description

You need to create a JavaScript class, CopyOnWriteArray, that mimics the behavior of a standard JavaScript array. The key difference is its internal implementation: when a mutation method is called (e.g., push, pop, splice, setting an element by index), the array should not modify itself directly. Instead, it should create a new, modified copy of its internal data and discard the old one. Reading operations (e.g., get, length, iteration) should be efficient and not trigger any copying.

Key Requirements:

  1. Immutability of Reads: Any method that reads data from the array (e.g., get(index), length, iteration) must not create a new copy. It should operate on the current internal state without side effects.
  2. Copy-On-Write for Writes: Any method that modifies the array's content or structure (e.g., push, pop, splice, set(index, value)) must first create a shallow copy of the current internal array, perform the modification on the copy, and then update its internal state to point to this new copy.
  3. Array-like Interface: The CopyOnWriteArray should provide methods that are familiar to JavaScript array users. At a minimum, implement:
    • constructor(initialArray): Initializes the CoW array, optionally with an initial array.
    • get(index): Returns the element at the given index.
    • set(index, value): Replaces the element at the given index with a new value. This must trigger a copy.
    • push(value): Adds a value to the end of the array. This must trigger a copy.
    • pop(): Removes the last element and returns it. This must trigger a copy.
    • splice(start, deleteCount, ...items): Modifies the array by removing or replacing existing elements and/or adding new elements in place. This must trigger a copy.
    • length: A getter property that returns the number of elements.
    • toArray(): Returns a shallow copy of the internal array. This is useful for external consumption and debugging but does not need to trigger a copy itself if the goal is to expose the current state.
    • Support for iteration using for...of loops.

Expected Behavior:

  • Multiple instances of CopyOnWriteArray created from the same initial array should maintain independent states after mutations.
  • Modifying one CopyOnWriteArray should not affect other CopyOnWriteArray instances that were created from it or share a common ancestor state.

Edge Cases:

  • Empty initial array.
  • Operations on empty arrays (e.g., pop on an empty array).
  • splice with various combinations of deleteCount and items.
  • Setting an index that is out of bounds (should behave similarly to standard array behavior for set if possible, or clearly documented).

Examples

Example 1:

const cowArray = new CopyOnWriteArray([1, 2, 3]);
console.log(cowArray.length); // Output: 3
console.log(cowArray.get(1)); // Output: 2

const newArray = cowArray.push(4); // Assuming push returns the new instance for chaining demonstration
console.log(cowArray.length); // Output: 3 (original array is unchanged)
console.log(newArray.length); // Output: 4
console.log(newArray.get(3)); // Output: 4
console.log(newArray.toArray()); // Output: [1, 2, 3, 4]

Explanation: The push(4) operation on cowArray creates a new CopyOnWriteArray instance (newArray) with [1, 2, 3, 4]. The original cowArray remains [1, 2, 3].

Example 2:

const original = new CopyOnWriteArray(['a', 'b']);
const modified = original.splice(1, 1, 'c', 'd'); // Assuming splice returns the new instance

console.log(original.toArray()); // Output: ['a', 'b']
console.log(modified.toArray()); // Output: ['a', 'c', 'd']
console.log(original.length);    // Output: 2
console.log(modified.length);    // Output: 3

Explanation: splice on original creates a new CopyOnWriteArray instance (modified). The original array is untouched.

Example 3:

const cowArray = new CopyOnWriteArray(); // Empty initial array
cowArray.push('first');
cowArray.set(0, 'updated');
console.log(cowArray.toArray()); // Output: ['updated']
console.log(cowArray.pop());      // Output: 'updated'
console.log(cowArray.toArray()); // Output: []

Explanation: Demonstrates sequential mutations, each creating a new internal copy, leading to the final empty array after pop.

Constraints

  • The underlying data structure for storing elements should be a standard JavaScript array internally.
  • All mutation methods (set, push, pop, splice) must create a shallow copy of the internal array.
  • Reading methods (get, length, iteration) must not create copies.
  • The toArray() method should return a shallow copy of the internal array.
  • The class should be designed to be reasonably performant for a large number of reads compared to writes.

Notes

  • Consider how to handle JavaScript's built-in array methods. You'll need to wrap or reimplement the modifying ones.
  • For iteration (for...of), you can implement the [Symbol.iterator] method to yield elements from the current internal array.
  • Shallow copy means that if the array contains objects, those objects themselves are not copied. Changes to nested objects will be reflected across copies.
  • Think about how to expose the length property efficiently without requiring a copy.
  • The constructor should handle cases where no initial array is provided, defaulting to an empty array.
Loading editor...
javascript