Matrix Operations in Functional JS

Riky Perdana
10 min readApr 3, 2024
Photo by Henry & Co. on Unsplash

“One of the happiest day in this year” was what I wrote on the latest github commit when I finally nailed my take on matrices operation application in javascript. I know that the idea of solving problems in the math books I read was sparked few years ago where I successfully made matrix multiplication function, but I was stumbled when trying to make one that yields a matrix determinant, thats when I stopped for good and didn’t dare to return. But, here I am, took another good look at the long lingering problem and ask myself again, do I have the guts to finish what I started?

I feel like I don’t have the needs to tell you guys what the matrices and it’s bizzare operations are good for — as many preceeding articles already done better job for that — so this post won’t venture that path. I might just post all the codes right away and let you figure the rest by yourself, right? No, this post serves no other purpose than to share you the elaboration of how the codes work to yield a matrix operation result. As usual, functional programming tends to write codes in functions rather than explicitly imperative ordered computation commands. Thus, I’ll split this post for each functions made.

Samples

Lets have a common matrices to be used repetitively throughout this article

sample1 = [
[1, 2],
[3, 4]
] // self made

sample2 = [
[1, 1, 1,-1],
[1, 1,-1, 1],
[1,-1, 1, 1],
[-1,1, 1, 1]
] // example from https://semath.info/src/inverse-cofactor-ex4.html

sample3 = [
[1, 0, 4,-6],
[2, 5, 0, 3],
[-1,2, 3, 5],
[2, 1,-2, 3]
] // example from The Organic Chemistry - Determinant of 4x4 Matrix

Utilities code

These are a set of functions which shall be used throughout the rest

withAs = (obj, cb) => cb(obj)
// to pass object around in a purely expressive function

sum = arr => arr.reduce((a, b) => a + b)
// to sum any given array of numbers

mul = arr => arr.reduce((a, b) => a * b)
// to multiply each given numbers in an array

sub = arr => arr.splice(1).reduce((a, b) => a - b, arr[0])
// to substract each numbers against the first given

deepClone = obj => JSON.parse(JSON.stringify(obj))
// when array spread syntax didn't work, this one will

shifter = (arr, step) => [
...arr.splice(step),
...arr.splice(arr.length - step)
] // if an array is a ring, rotate leftwise in certain steps

makeArray = (n, cb) =>
[...Array(n).keys()].map(
i => cb ? cb(i) : i
) // create an array with certain length

makeMatrix = (len, wid, fill) =>
makeArray(len).map(i => makeArray(
wid, j => fill ? fill(i, j) : 0
)) // create a matrix in any size, with customizable contents

matrixRandom =
(len, wid, min=0, max=100) =>
makeMatrix(len, wid, x => Math.round(
Math.random() * (max - min)
) + min)

matrixSize = matrix => [
matrix.length, matrix[0].length
] // yields the dimension of any given matrix

arr2mat = (len, wid, arr) =>
makeArray(len).map(i => arr.slice(
i * wid, i * wid + wid
))

/*----------------------Examples----------------------*/

makeArray(5) // [0, 1, 2, 3, 4]

// Make a 3x3 identity matrix
makeMatrix(3, 3, (i, j) => +(i === j))
/* gets [
[1, 0, 0],
[0, 1, 0],
[0, 0, 1]
]*/

matrixRandom(2, 3, -100)
/* shall yield [
[41, -23, 0],
[-78, 10, 42]
]*/

arr2mat(2, 3, [1, 2, 3, 4, 5, 6])
/* get [
[1, 2, 3],
[4, 5, 6]
]*/

Matrix Map and Scalar

A matrix is said to be scaled when each of the elements are multiplied with certain number. We make matrixMap to be a little more versatile than to just multiply the elements with certain number, but to alter the contents by any given expression.

matrixMap = (matrix, cb) =>
deepClone(matrix).map((i, ix) => i.map(
(j, jx) => cb({i, ix, j, jx, matrix})
))

matrixMap(sample1, ({j}) => j * 2)
// shall yield [[2, 4], [6, 8]]

withAs('abcdefghijklmn', alph => matrixMap(
sample1, ({j}) => alph.split('')[j]
)) // shall yield [['b', 'c'], ['d', 'e']]

matrixScalar = (n, matrix) =>
matrixMap(matrix, ({j}) => n * j)

matrixScalar(2, sample1) // get [[2, 4], [6, 8]]

Matrix Arithmatic Operations

Basic arithmatic operations allowed in matrix realm are addition, substraction, and multiplication (while the division is still arguable though still achievable with some tricks). These are the functions to perform each of those operations:

matrixAdd = matrices =>
matrices.reduce((acc, inc) => matrixMap(
acc, ({j, ix, jx}) => j + inc[ix][jx]
), makeMatrix(...matrixSize(matrices[0])))

matrixSub = matrices => matrices.splice(1)
.reduce((acc, inc) => matrixMap(
acc, ({j, ix, jx}) => j - inc[ix][jx]
), matrices[0])

matrixMul = (m1, m2) => makeMatrix(
m1.length, m2[0].length, (i, j) => sum(
m1[i].map((k, kx) => k * m2[kx][j])
)
)

matrixAdd([sample1, sample1])
// shall yield [[2, 4], [6, 8]]
matrixAdd([sample1, sample1, sample1])
// shall yield [[3, 6], [9, 12]]
matrixSub([sample1, sample1])
// shall yield [[0, 0], [0, 0]]
matrixMul(sample1, sample1)
// shall yield [[7, 10], [15, 22]]

Update!: While matrixAdd and matrixSub accepts an array instead of a list of arguments, matrixMul function above only accepts two matrices exclusively. It’s a bit unfair if I left it lacking behind its other two siblings, so I decided to make an upgraded version of it which called matrixMuls like this:

matrixMuls = matrices =>
deepClone(matrices).splice(1)
.reduce((acc, inc) => makeMatrix(
acc.length, inc[0].length,
(ix, jx) => sum(acc[ix].map(
(k, kx) => k * inc[kx][jx]
))
), deepClone(matrices[0]))

matrixMuls([
matrixRandom(3, 5),
matrixRandom(5, 3),
matrixRandom(3, 1)
]) // gets [[3722689], [3301757], [3720788]]

Matrix Transpose

Is a function that transpose each elements position based on their row-column, to column-row position. Which works like this:

matrixTrans = matrix => makeMatrix(
...shifter(matrixSize(matrix), 1),
(i, j) => matrix[j][i]
)

matrixTrans(sample1) // [[1, 3], [2, 4]]
matrixTrans(sample3) /* shall yield [
[1, 2, -1, 2]
[0, 5, 2, 1]
[4, 0, 3, -2]
[-6, 3, 5, 3]
] */

Matrix Minor

The later matrix determinant operation requires a function which when given a matrix, shall eliminate any elements in specified row AND column. So that’s why this function requires 1.the matrix, 2. n-th row, 3. n-th column

matrixMinor = (matrix, row, col) =>
matrix.length < 3 ? matrix :
matrix.filter((i, ix) => ix !== row - 1)
.map(i => i.filter((j, jx) => jx !== col - 1))

matrixMinor(sample3, 1, 1) /* shall yield [
[5, 0, 3]
[2, 3, 5]
[1,-2, 3]
]*/
matrixMinor(sample3, 3, 3) /* shall yield [
[1, 0,-6]
[2, 5, 3]
[2, 1, 3]
]*/
matrixMinor(
matrixMinor(sample3, 3, 3),
2, 2
) // shall yield [[1, -6], [2, 3]]

Matrix Determinant

This one was the big rock that stumbled me and finally solved through recursion. The main idea is to switch between 2 conditions, if the given matrix size is 3 or more, make a sum of each elements on the top row’s matrixDet of it’s specified matrixMinor. The function will keep looping on itself getting deeper and deeper, until the given matrix is 2x2 in size. Once it reach 2x2 size, it shall perform the matrixTrans, multiply each rows, and substract both result. The Math.pow part is to alternate the positive-negative sign of each elements according to their positions.

If understanding this function inner-works left you fazed, just imagine the effort of trials-and-errors I went through just to finally make it work, “it hurts me poor little brain”. So, whenever matrixDet function given a matrix, it shall yield its determinant.

matrixDet = matrix => withAs(
deepClone(matrix), clone => matrix.length < 3
? sub(matrixTrans(clone.map(shifter)).map(mul))
: sum(clone[0].map((i, ix) => matrixDet(
matrixMinor(matrix, 1, ix+1)
) * Math.pow(-1, ix+2) * i))
)

matrixDet(sample1) // get -2
matrixDet(sample2) // get -16
matrixDet(sample3) // get 318

Matrix Cofactor

I have no idea myself what the cofactor means and what it’s used for, other than just being another step towards reaching matrix inversion. Basically it’s just altering the content of a matrix by it’s respective matrix minor’s determinant, and again, alternate the positive-negative signs based on their respective positions.

matrixCofactor = matrix => matrixMap(
matrix, ({i, ix, j, jx}) =>
matrix[0].length > 2 ?
Math.pow(-1, ix + jx + 2) * matrixDet(
matrixMinor(matrix, ix+1, jx+1)
) : (ix != jx ? -matrix[jx][ix]
: matrix[+!ix][+!jx]
)
)

matrixCofactor(sample1) // shall yield [[4, -3], [-2, 1]]
matrixCofactor(sample2) /* shall yield [
[-4, -4, -4, 4]
[-4, -4, 4, -4]
[-4, 4, -4, -4]
[4, -4, -4, -4]
]*/
matrixCofactor(sample3) /* shall yield [
[ 74,-26, 52, -6]
[-38, 95,-31,-27]
[12, -30, 60, 42]
[166,-97, 35, 51]
]*/

Matrix Inverse

We sowed the seeds of arithmatic matrix operations, fertilize it with numerous following functions, until it gradually grows to an adult tree we call a cofactor, and matrix inverse is the fruit rightfully meant to be harvested. Still I have no idea what inversing a matrix means and why it behaves like that, all that I know is inversed matrix has vast utilities in more advanced matrix operations. Here is the matrix inversion function:

matrixInverse = matrix => matrixMap(
matrixTrans(matrixCofactor(matrix)),
({j}) => j / matrixDet(matrix)
)

matrixInverse(sample1) // shall yield [[1, -1], [-1, 1]]
matrixInverse(sample2) /* shall yield [
[0.25, 0.25, 0.25, -0.25]
[0.25, 0.25, -0.25, 0.25]
[0.25, -0.25, 0.25, 0.25]
[-0.25, 0.25, 0.25, 0.25]
]*/
matrixInverse(sample3) /* shall yield [
[0.2327, -0.1194, 0.0377, 0.5220]
[-0.0817, 0.2987,-0.0943,-0.3050]
[0.1635, -0.0974, 0.1886, 0.1100]
[-0.0188,-0.0849, 0.1320, 0.1603]
]*/

Matrix Linear Solver

Still remember my latest post about solving linear programming problems with functional javascript? Only able to solve problems with 7 variables top or less, left a bad taste in my mouth. So this function is yet another attempt to solve linear programming problems with ‘n’ variables, no longer limited to certain number caused by maximun number length allowed in JS. If we take matrix inverse as the fruit, this linear programming problem solver is just analogous to another palatable cooked dish of fruits.

This function accepts 2 arguments: 1. the condition matrix, 2. the result array. Lets say that the sample3 matrix contents are coefficients of variable a, b, c, d, which is representable as this snippet:

 1a + 0b + 4c - 6d = -12
2a + 5b + 0c + 3d = 34
-1a + 2b + 3c + 5d = 41
2a + 1b - 2c + 3d = 14

What is my secret combination of numbers lies behind those four variables? You can approach the problem by hand — if you so insist — but let’s use the power of matrix operation to solve this one.

matrixLinSolve = (conds, res) => matrixMul(
matrixInverse(conds), res.map(i => [i])
).map(i => i[0])

matrixLinSolve(
[
[1, 0, 4,-6],
[2, 5, 0, 3],
[-1,2, 3, 5],
[2, 1,-2, 3]
], [-12, 34, 41, 14]
) // correctly get [2, 3, 4, 5]

My secret combination of numbers were exposed just like that, with so little effort. I assume that this approach of solving linear programming problems is supposed to be superior than using the linPro function of the last article with no limitation of number of variables.

Matrix Division

Dividing matrix by another matrix is as forbidden as dividing 1 by zero. Any approach to divide them directly by each other will only lead to undefined result. But if we shift our perspective:

A / A = A * A^(-1) = A * inversed(A) = identity

Then if we perform the matrix operation above of any matrix, it shall yield an identity matrix of given matrix. So, lets see how it’s done:

matrixMap(
matrixMul(sample3, matrixInverse(sample3)),
({j}) => Math.round(j)
) /* shall yield [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1],
]*/

It’s rather unsurprising that we’d find the identity after we perform such matrix operations. The Math.round part is somewhat necessary to clean things up since JS tends to change any fractions to decimals, and this is just a sorry excuse to keep the results tidy.

Fun Fact: We knew that matrix multiplication is non-commutative, the position of matrices in the operation order determine the result. But in this case, no matter you make matrixMul(A, matrixInverse(A)) or matrixMul(matrixInverse(A), A) , they will always yield the same identity matrix, how weird is that?

Wrap things up

Sometimes people with great (or terrible) curiosity tends to think “How should I do it?” rather than just “Should I do it?”. Surely there are myriads of softwares (be they open or propetiary) capable of performing matrix operations which we attempted in this article. There’s no justification to call these crafts superior in one or some ways to those established softwares. But accross many articles I read on sites like StackOverflow or some tutorial sites, their presented codes of the same matter, are overly obfuscated, and — in my eyes — exudes little to no beauty. So I take this work not only as a functional software, but somehow I also see it as a painting. It’s both functionally and aesthetically pleasing. May it be useful to you too. Thank you.

Codes Recap

withAs = (obj, cb) => cb(obj)
sum = arr => arr.reduce((a, b) => a + b)
mul = arr => arr.reduce((a, b) => a * b)
sub = arr => arr.splice(1).reduce((a, b) => a - b, arr[0])
deepClone = obj => JSON.parse(JSON.stringify(obj))

shifter = (arr, step) => [
...arr.splice(step),
...arr.splice(arr.length - step)
]

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
))

matrixRandom =
(len, wid, min=0, max=100) =>
makeMatrix(len, wid, x => Math.round(
Math.random() * (max - min)
) + min)

matrixSize = matrix => [
matrix.length, matrix[0].length
]

arr2mat = (len, wid, arr) =>
makeArray(len).map(i => arr.slice(
i * wid, i * wid + wid
))

matrixMap = (matrix, cb) =>
deepClone(matrix).map((i, ix) => i.map(
(j, jx) => cb({i, ix, j, jx, matrix})
))

matrixScalar = (n, matrix) =>
matrixMap(matrix, ({j}) => n * j)

matrixAdd = matrices =>
matrices.reduce((acc, inc) => matrixMap(
acc, ({j, ix, jx}) => j + inc[ix][jx]
), makeMatrix(...matrixSize(matrices[0])))

matrixSub = matrices => matrices.splice(1)
.reduce((acc, inc) => matrixMap(
acc, ({j, ix, jx}) => j - inc[ix][jx]
), matrices[0])

matrixMul = (m1, m2) => makeMatrix(
m1.length, m2[0].length, (i, j) => sum(
m1[i].map((k, kx) => k * m2[kx][j])
)
)

matrixMuls = matrices =>
deepClone(matrices).splice(1)
.reduce((acc, inc) => makeMatrix(
acc.length, inc[0].length,
(ix, jx) => sum(acc[ix].map(
(k, kx) => k * inc[kx][jx]
))
), deepClone(matrices[0]))

matrixMinor = (matrix, row, col) =>
matrix.length < 3 ? matrix :
matrix.filter((i, ix) => ix !== row - 1)
.map(i => i.filter((j, jx) => jx !== col - 1))

matrixTrans = matrix => makeMatrix(
...shifter(matrixSize(matrix), 1),
(i, j) => matrix[j][i]
)

matrixDet = matrix => withAs(
deepClone(matrix), clone => matrix.length < 3
? sub(matrixTrans(clone.map(shifter)).map(mul))
: sum(clone[0].map((i, ix) => matrixDet(
matrixMinor(matrix, 1, ix+1)
) * Math.pow(-1, ix+2) * i))
)

matrixCofactor = matrix => matrixMap(
matrix, ({i, ix, j, jx}) =>
matrix[0].length > 2 ?
Math.pow(-1, ix + jx + 2) * matrixDet(
matrixMinor(matrix, ix+1, jx+1)
) : (ix != jx ? -matrix[jx][ix]
: matrix[+!ix][+!jx]
)
)

matrixInverse = matrix => matrixMap(
matrixTrans(matrixCofactor(matrix)),
({j}) => j / matrixDet(matrix)
)

matrixLinSolve = (conds, res) => matrixMul(
matrixInverse(conds), res.map(i => [i])
).map(i => i[0])

--

--