Multi Dimensional Matrix in Functional JS

Riky Perdana
8 min readApr 9, 2024
Photo by rashid khreiss on Unsplash

Matrix operations for aspiring mathematicians are pure cracks. Once you have a taste of this forbidden fruit, you’ll only crave for more. In the last article, I wrote basic matrix operations from its generator, its arithmatic operations, determinants, to its inverse and use cases in solving linear programming problems. To finally nailed those matrix functions was such a relief, that lasted for a couple of hours, even days. And I must admit that I did made some continuous changes on the last article as soon as I had a glimpse of where to improve them both functionally and aesthetically. But as soon as the room for improvement diminish, that’s when the urge to have another challenges kicks back. I knew I need to find another crack.

The last matrix operations article posted surely should be sufficient for undergraduate level of linear algebra problems, for it’s only involve 2 dimensions (vertical and horizontal). But study of matrices are no way limited merely in 2 dimensions, it could involve 3, 4, or even nnumber of dimensions. I know that our perceptions only allow us to intuitively perceive up to 3 dimensions like a box with space in it. But as soon as we involve just 1 other dimension, we’d lost track of where to place it as this new dimension ought to be perpendicular against the other 3. You may check a few books, articles, or YT videos that discuss about higher-dimensions and realize the oddity they has to offer. So, relying on our percepction alone will get us nowhere in playing in the turf of higher dimension matrices. Even the fancy animated 4 dimensions cube below still failed to correctly represent a real 4th dimension box.

Our best attempt to geometrically represent 4th-D in 3D

Luckily, understanding higher dimension matrices in the form of array-based data structure is easier than it’s supposed to. Let’s assume that we wanted to have a one dimensional array in JS, we only need to declare a new array with bunch of elements in it, a.k.a. vector. When we need a 2 dimensional array, we only need make an array of arrays, a.k.a. matrix. And when we need the 3 dimensional one, it’s as simple as having a cube matrix. Making a matrix with higher dimension than that only requires us to replace the deepest elements into yet another array.

matrix1D = [1, 2, 3, 4]

matrix2D = [
[1, 2, 3, 4],
[5, 6, 7, 8],
[8, 7, 6, 5],
[4, 3, 2, 1]
]

matrix3D = [
[[0, 0], [0, 1], [1, 0], [1, 1]],
[[0, 0], [0, 1], [1, 0], [1, 1]],
[[0, 0], [0, 1], [1, 0], [1, 1]],
[[0, 0], [0, 1], [1, 0], [1, 1]],
]

matrix4D = [
[
[[0, 0], [0, 0]],
[[0, 0], [1, 1]],
[[1, 1], [0, 0]],
[[1, 1], [1, 1]]
],
[
[[0, 0], [0, 0]],
[[0, 0], [1, 1]],
[[1, 1], [0, 0]],
[[1, 1], [1, 1]]
],
[
[[0, 0], [0, 0]],
[[0, 0], [1, 1]],
[[1, 1], [0, 0]],
[[1, 1], [1, 1]]
],
[
[[0, 0], [0, 0]],
[[0, 0], [1, 1]],
[[1, 1], [0, 0]],
[[1, 1], [1, 1]]
]
]

What once was thought to be perceptively impossible has turned into a problem of recursively making the deepest element into another array. Wait, did I said something that sound familiar? Of course, recursion! Okay, I know that any problems that has recursive patterns, should have a recursive solution. Let’s involve it in our solusion later, but first it’s important to learn from others what they got in their hands regarding this matter, how to create a multi-dimensional matrix simply by specifying our requirement of its dimensions?

Let the hunts begin

The Numpy solution, hmm.. how convenient
The solution in C…… why?
Meanwhile in JS

StackOverflow, Quora, FreeCodeCamp, are few among many places I visit to find what kind of solutions they offer to generate multi-dimensional matrices. You can have a look for yourself if you’d like to. The quickest solution python can offer requires numpy library, while other languages tend to combine conditional statements and for loops. A recognizable pattern emerges from their solutions, it’s always require explicit statements of chained for loops, when you need to go higher, you ought to extend the for loops until you reach the desired dimensions to be generated. It’s perfectly fine if you only deals with certain n-th dimension and not bothered with other levels. The JS solution above is already pretty decent where it actually involved recursion, so it only proves that generation of n-dimension matrices in JS is entirely possible. Let’s have our own home-brew solution.

Lets get dirty then

Our attempt in making a function that generate a matrix in the latest post earned us these functions:

makeArray = (n, cb) =>
[...Array(n).keys()]
.map(i => cb ? cb(i) : i)

makeMatrix = (len, wid, fill) =>
makeArray(len).map(i => makeArray(
wid, j => fill ? fill(i, j) : 0
)) // version 1.0

makeArray(5) // gets [0, 1, 2, 3, 4]
makeMatrix(2, 3) /* gets [
[0, 0, 0],
[0, 0, 0]
]*/
makeMatrix(2, 3, (i, j) => `${i};${j}`) /* gets [
['0;0', '0;1', '0;2'],
['1;0', '1;1', '1;2']
]*/

The function successfully yield the desired 2 dimension matrix, but the problem is that it exclusively designed for 2 dimensions only. So to yield an n-th dimension matrix we ought to rethink the problem, and surely will come out with different solution. To think of pairing recursive problem with recursive solution was already a good start, after few minutes of trial and errors, I came up with this solution:

makeMatrix = ([head, ...tail]) => !head ? 0
: makeArray(head, i => makeMatrix(tail))
// version 1.5

makeMatrix([2, 3]) /* gets [
[0, 0, 0],
[0, 0, 0]
]*/

That was even more simple than the preceeding function (v1.0). The idea is when the function given a list of dimensions, it shall make an array with certain length which we call the head, and each of its elements shall be recursively called by passing what’s remain in the tail. The function shall keep looping back on itself until the head reaches an empty set, where it shall return 0 as the deepest element. Notice that in this new version of function we pass an array of dimensions instead as a list of arguments. But one problem still remains, what if we don’t want every elements to be set only as zeros? We wanted each elements to know their own respective position in the multi-dimensional matrix, can we have it?

makeMatrix = ([head, ...tail], cb, pos) =>
head ? makeArray(head, i => makeMatrix(
tail, cb, [...(pos || []), i]
)) : cb ? cb(pos) : 0 // version 2.0

makeMatrix([2, 3]) /* gets [
[0, 0, 0],
[0, 0, 0]
]*/

makeMatrix([2, 3], pos => pos.join(';')) /* gets [
['0;0', '0;1', '0;2']
['1;0', '1;1', '1;2']
]*/

makeMatrix([4, 3, 2], pos => pos.join(';')) /* gets [
[
['0;0;0', '0;0;1'],
['0;1;0', '0;1;1'],
['0;2;0', '0;2;1']
],
[
['1;0;0', '1;0;1'],
['1;1;0', '1;1;1'],
['1;2;0', '1;2;1']
],
[
['2;0;0', '2;0;1'],
['2;1;0', '2;1;1'],
['2;2;0', '2;2;1']
],
[
['3;0;0', '3;0;1'],
['3;1;0', '3;1;1'],
['3;2;0', '3;2;1']
]
]*/

Another alteration to the function earned us the function above, which is not only capable of generating a multi-dimensional matrix with n-depth but also have a callback (cb) that returns its position in array form. Notice that the examples shows that each elements are aware of their respective position in the multi-dimensional matrix.

Codes Recap

makeArray = (n, cb) =>
[...Array(n).keys()]
.map(i => cb ? cb(i) : i)

makeMatrix = ([head, ...tail], cb, pos) =>
head ? makeArray(head, i => makeMatrix(
tail, cb, [...(pos || []), i]
)) : cb ? cb(pos) : 0

These 7 lines of codes could give you any multi-dimensional matrix with any shape and dimensions. If you so daring, just try it out on your browser console and have a look on what’s yielded, you can expand or collapse the tree to see the entire structure down to the deepest level. And each elements can be altered by any given expression you specify in the callback. As far as we concerned about the functional paradigm, our functions involved no for loops nor conditional statments either. Enough patting myself on the back, why don’t we find a use case to show its capability then? Hip ho.

Use Cases

In set theory I’ve learned that when you’re given 2 variables or more, you can make a new set that contains the combination of those sets at once. Let’s put up the easiest example here with 2x2 matrix. In the gender variable there are man and woman, while the age variable there are young and old. Assuming that their combinations are represented in a matrix, there’s supposed to be 4 elements in total like young man, young woman, old man, old woman. Why bother manually typing them like that, if we can just call the function for help?

withAs = (obj, cb) => cb(obj)

withAs({
age: ['young', 'old'],
gender: ['man', 'woman']
}, ({age, gender}) => makeMatrix(
[2, 2], pos => `${age[pos[0]]} ${gender[pos[1]]}`
))

/* gets [
['young man', 'young woman'],
['old man', 'old woman']
]*/

withAs({
rom: ['I', 'II', 'III', 'IV'],
alp: ['a', 'b', 'c'],
bin: [0, 1]
}, ({rom, alp, bin}) => makeMatrix(
[4, 3, 2], p => [
rom[p[0]], alp[p[1]], bin[p[2]]
].join('')
))

/* gets [
[
['Ia0', 'Ia1'],
['Ib0', 'Ib1'],
['Ic0', 'Ic1']
],
[
['IIa0', 'IIa1'],
['IIb0', 'IIb1'],
['IIc0', 'IIc1']
],
[
['IIIa0', 'IIIa1'],
['IIIb0', 'IIIb1'],
['IIIc0', 'IIIc1']
],
[
['IVa0', 'IVa1'],
['IVb0', 'IVb1'],
['IVc0', 'IVc1']
]
]*/ // no one's got left behind

Well, this is all I can come up with. I believe that the usability of these functions can reach far beyond my expectation, depends on what problems you face that involves multi-variability, combinations, or multi-dimensional matrix in general. Thank you for the read.

--

--