Post

Programming Matrix multiplication in a functional level programming

·Bharat

So, thanks to one of the connections I met recently named Sasank, I have been getting a healthy dose of lisp and different way of looking at current systems.

Sasank has been implementing few nifty things which he hopes will solve problems caused by Von Neumann Bottleneck. For more details the reader, is requested to check out his repo at llama.lisp. This post will to distill some of my learnings from perusal of the projects he has been working on in exhaustive style.

So what is von Neumann Bottleneck ?

If one checks the Turing lecture paper given by Backus of BNF fame, they will understand why current imperative language paradigms do not scale and generalize well to certain mathematical properties. Backus shows with very clear descriptions and bulleted points on the shortcomings of so called Von Neumann programming languages.

The famed landmark lecture by John Backus can be read here. Can Programming Be Liberated from the von Neumann Style? A Functional Style and Its Algebra of Programs

So a von Neumann computer simplistically is nothing but a computer having

  1. Central Processing Unit (CPU)
  2. A store
  3. And a tube that transfers the data between teh store and the CPU.

Backus shows that how an imperative program that has assignment operations causes lots of wastage owing to lot of data fro and to in the tube in the above design. He states that “the assignment statement is the von Neumann bottleneck of programming languages and keeps us thinking in word-at-a-time terms in much the same way the computer’s bottleneck does”.

Defining Inner product in functional level programming style

So inner product is nothing but this formulation

Sasank has written a neat introduction where he shows via python how this form application works for those of us who are wired to think in terms of modern languages here.

So in pythonic code below:

## Inner Product
IP = comp(
    insert(add),
    alpha(mul),
    trans
)

comp function

To dig this further this let us see how he explains comp.

Comp is defined like this

def comp(*fn_list):
    return lambda X: fn_list[0](X) if len(fn_list) == 1 else comp(*fn_list[:-1])(fn_list[-1](X))

Lambda function

Let us analyse this: lambda X: fn_list[0](X)

The expression lambda X: fn_list[0](X) is a lambda function in Python. A lambda function is a small anonymous function that is defined with the lambda keyword, and it can take any number of arguments, but can only have one expression.

Here’s a breakdown of this lambda function:

This lambda function takes one argument X, and applies the first function in fn_list to X.

How comp works?

So if the length of the fn_list argument is 1, then we return the lambda expressions detailed above.

Else, we branch to comp(*fn_list[:-1])(fn_list[-1](X))

The expression comp(*fn_list[:-1])(fn_list-1) is part of the recursive function comp shown earlier. This expression is used to create a composition of functions from the list fn_list.

Here’s a breakdown:

Trace back to inner product definition

So, now we understand inner product is composition of these functions:

  1. insert(add)
  2. alpha(mul)
  3. transpose

Transpose of a matrix

Transpose function implementation is straightforward.

def trans(X):
    return [
        [X[n][m] for n in range(len(X))]
        for m in range(len(X[0]))
    ]

Mul and Add functions

The below are also straightfoward.

def mul(X):
    return X[0] * X[1]

def add(X):
    return X[0] + X[1]

alpha function

def alpha(fn):
    return lambda X: [fn(x) for x in X]

This is nothing but applying a function for X as argument to the lambda function.

insert function

def insert(f):
    return lambda X: X[0] if len(X) == 1 else f([X[0], insert(f)(X[1:])])

index functions

def idx_0(X):
    return X[0]

def idx_1(X):
    return X[1]

The indexing functions given a list, X as shown above are simple to understand.

distl function

def distl(X):
    assert len(X) == 2
    return [[X[0], z] for z in X[1]]

distl(X), is designed to distribute the first element of a list X with each element in the second element of X, which is expected to be a list itself.

Here’s a breakdown of how the function works:

distr function

def distr(X):
    assert len(X) == 2
    return [[y, X[1]] for y in X[0]]

This must be obvious to decipher after reading the above explianation.

cat function

def cat(*fns):
    return lambda X: [fn(X) for fn in fns]

This Python function, cat(*fns), is designed to create a new function that applies a list of functions to an input. Here’s a breakdown of how it works:

Example:

def square(x):
    return x**2

def cube(x):
    return x**3

f = cat(square, cube)
print(f(2))  # prints [4, 8]

understanding execution of mat mul

So, now we can see how it all ties up to implement mat mul, which is the basic building block of all machine learning and deep learning algorithms and model inferencing.

MM = comp(
    alpha(alpha (IP)),
    alpha(distl),
    distr,
    cat(idx_0, comp(trans, idx_1))
)

cat(idx_0, comp(trans, idx_1)): This function takes a list X as input and returns a new list.

The first element of the new list is the first element of X (obtained by idx_0(X)), and the second element is the transpose of the second element of X (obtained by comp(trans, idx_1)(X)).

distr: This function takes a list X as input, where X is expected to have exactly two elements. It returns a new list where each element is a list containing an element from the first element of X and the second element of X.

alpha(distl): This function applies the distl function to each element in a list X.

alpha(alpha (IP)): This function applies the IP function twice to each element in a list X.