An Immutable Array that looks, behaves (and IS) a standard JavaScript Array but with immutable properties.
For immutable Objects, see IObject
One of the cornerstones of functional programming is immutable data structures. Using immutable data structures insures to the author and other programmers that once they have a handle to an object, it won't change beneath their feet. This is important for reasoning about code. It aids with testing, debugging and refactoring. It provides a much faster way to determine if state has changed. And it helps you write pure functions.
Arrays are an extremely popular data structure in most JavaScript programs. But they are far from immutable. One can simply assign values to an array at a given index via myArray[10] = 100
. And many of the Array methods are mutating, such as push
, sort
and slice
.
Some libraries exist that provide random access data structures similar to the Array but that are immutable, such as Mori or Immutable - but they are large opinionated libraries that expose a completely different API.
IArray provides an Immutable array only, and is very light.
wc -l IArray.js
146 IArray.js
146 lines in the source file, much of which is comments and the universal module definition.
IArray
extends the Array
object by building a prototype which includes Array.prototype
. This enables IArray
to offer all the API methods contained in the standard Array
, and appear to debuggers and most other type checkers as an Array
const a = IArray()
if(a instanceof Array) // true!
// ...
But IArray
intercepts calls to mutating methods, such as push
and internally clones the array, calls the Array.push
using your same arguments, and finally returning a new IArray
with the result.
So mutating methods all exist, but their behavior is slightly different - returning a new IArray
with the changes applied, rather than mutating the array upon which its called.
In cases where a method mutated the underlying array and also returned a value (such as pop
), the value is available at IArray.ret
.
Since this extends the standard JavaScript Array
, I will only document the methods that have changed from the standard Array API. These include 3 new methods, rm
, rmAt
and set
along with several methods from the Array
API whose behavior is slightly changed to reflect the immutable nature of IArray
.
Behaves just as the standard Array.concat
with the exception that the returned array is an IArray
.
IArray([1, 2, 3]).concat([7, 8]) // => IArray([1, 2, 3, 7, 8])
IArray().concat(IArray([3, 4, 5]) // => IArray([3, 4, 5])
IArray([1, 2, 3]).concat(100) // => IArray([1, 2, 3, 100])
Behaves just as the standard Array.copyWithin
with the exception that the returned array is an IArray
and the original array is not modified
var a = IArray([5, 6, 7, 8])
var b = a.copyWithin(2) // => IArray([5, 6, 5, 6]) (a unmodified)
Behaves just as the standard Array.fill
with the exception that the returned array is an IArray
.
var a1 = IArray([1, 2, 3, 4, 5])
var a2 = a1.fill(9, 2, 4)
// a1 = IArray([1, 2, 3, 4, 5]) - this shouldn't change
// a2 = IArray([1, 2, 9, 9, 5]) - fill 9s starting at item 2, ending at 4
Behaves just as the standard Array.filter
with the exception that the returned array is an IArray
.
function even(n) { return n % 2 === 0 }
var a1 = IArray([1, 2, 3, 4, 5, 6])
var a2 = a1.filter(even)
// a1 = IArray([1, 2, 3, 4, 5, 6]) - this shouldn't change
// a2 = IArray([2, 4, 6]) - even values only
Behaves just as the standard Array.map
with the exception that the returned array is an IArray
.
var a1 = IArray([1, 4, 9, 25])
var a2 = a1.map(Math.sqrt)
// a1 = IArray([1, 4, 9, 25]) - ensure it is unchanged
// a2 = IArray([1, 2, 3, 5]) - mapped values
The same API signature as the standard Array.push
, but the original IArray
is not modified. A new IArray
is returned with the value(s) appended, and the length property is stored in the ret
property (which is mostly redundant since it is also available in the length
property - but is included for consistency)
var a1 = IArray([1, 4, 9])
var a2 = a1.push(100)
// a1 = IArray([1, 4, 9])
// a2 = IArray([1, 4, 9, 100])
The same API signature as the standard Array.pop
, but the original IArray
is not modified. A new IArray
is returned with the last value removed, and the value that was removed is stored in the ret
property.
var a1 = IArray([1, 4, 9])
var a2 = a1.pop()
// a1 = IArray([1, 4, 9]) - unchanged
// a2 = IArray([1, 4]) - same as a1 with last element removed
// a2.ret = 9 - last element stored in ret property
Returns a new IArray
with the value
specified removed. If multiple occurrences of the value exists, the first one is removed. The removed value appears on the ret
property of the newly created IArray
. If the value does not exist in the array, no error is thrown, but the ret
property contains undefined
.
var a1 = IArray([1, 4, 9]) // a1 is [ 1, 4, 9 ]
var a2 = a1.rm(4) // a2 is [ 1, 9 ]
log(a2.ret) // 4
var a3 = a2.rm(6) // this doesn't exist, so a3 is [ 1, 9 ]
log(a3.ret) // undefined
Returns a new IArray
with the value at position index
removed. The removed value appears on the ret
property of the newly created IArray
. If the index
position specified is out of the range of the IArray
, no error is thrown, but the ret
property contains undefined
and the values in the returned IArray
are unchanged.
var a1 = IArray([1, 4, 9]) // a1 is [ 1, 4, 9 ]
var a2 = a1.rmAt(1) // a2 is [ 1, 9 ]
log(a2.ret) // 4
var a3 = a2.rmAt(6) // position out of range, so a3 is [ 1, 9 ]
log(a3.ret) // undefined
Used to assign a value to a specific index within the IArray
. This is used as a substitute to the array[index] = value
notation, which mutates the array. The returned IArray
is a copy of the original array with value assigned at the index specified.
var a1 = IArray([1, 4, 9])
var a2 = a1.set(1, 6)
// a1 = IArray(1, 4, 9]) - unchanged
// a2 = IArray(1, 6, 9]) - item at index 1 is a 6
The same API signature as the standard Array.shift
, but the original IArray
is not modified. A new IArray
is returned with the first element removed. This removed element is available through the ret
property of the returned IArray
.
var a1 = IArray([1, 4, 9])
var a2 = a1.shift()
// a1 = IArray([1, 4, 9]) - no change
// a2 = IArray([4, 9]) - first value removed
// a2.ret = 1 - removed value
Behaves just as the standard Array.slice
with the exception that the returned array is an IArray
.
var a1 = IArray([5, 10, 15, 20])
var a2 = a1.slice(1, 3)
// a1 = IArray([5, 10, 15, 20])) - no changes
// a2 = IArray([10, 15])) - elements 1 to 3 (non-inclusive)
Behaves just like the standard Array.sort
with the exception that the original IArray
is not modified, and the newly sorted IArray
is returned.
var fruit = ["cherries", "apples", "bananas", "pears"]
var a1 = IArray(fruit)
var a2 = a1.sort()
var a3 = a1.sort(function(a, b) { return a.length - b.length }) // sort by word length
// a1 remains unchanged: IArray(["cherries", "apples", "bananas", "pears"])
// a2 is sorted: IArray(["apples", "bananas", "cherries", "pears"])
// a3 is sorted according to comparator: IArray(["pears", "apples", "bananas", "cherries"])
Behaves just like the standard Array.splice
with the exception that the original IArray
is not modified but is returned as a new IArray
. The removed items will be stored as an IArray
on the ret
property.
var a1 = IArray([5, 10, 15, 20])
var a2 = a1.splice(1, 1, 22)
// a1 is unchanged
// a2 is IArray([5, 22, 15, 20]) - item 1 removed and replaced with 22
// a2.ret is IArray([10]) - the removed items in an IArray
The same API signature as the standard Array.unshift
, but the original IArray
is not modified. A new IArray
is returned with elements added to the front of the array.
var a1 = IArray([5, 10, 20])
var a2 = a1.unshift("hello", "world")
// a1 = IArray([5, 10, 20])) - unchanged
// a2 = IArray(["hello", "world", 5, 10, 20]))
See the LICENSE file for license rights and limitations (MIT).