Skip to content

Instantly share code, notes, and snippets.

@snth
Last active December 21, 2024 08:57
Show Gist options
  • Save snth/03a3622f5eaab58fb120520cf3d0e91e to your computer and use it in GitHub Desktop.
Save snth/03a3622f5eaab58fb120520cf3d0e91e to your computer and use it in GitHub Desktop.
RECURSIVE♾ - Excel LAMBDA function for recursion in Excel

Recursion in Excel

In my recent post about TRIMRANGE (https://www.linkedin.com/posts/activity-7267473059383582720-mhJG) I mentioned that I would follow up with more to say about recursion.

Recursion is tricky in any context but probably even more so in Excel as it is not natively supported. However #Excel does support first-class functions and with that we can use a trick first discovered by Alan Turing which also goes by the scare name of "Theta Combinator", the lesser known but equally if not more useful cousin of the "Y Combinator" (if those terms don't scare you then have a look at https://en.wikipedia.org/wiki/Fixed-point_combinator). Incidentally the Y Combinator goes back to Haskell Curry after which the concept of "Currying" is named - a fascinating subject in its own right but a topic for another day.

In my TRIMRANGE implementation I defined a "_loop" LAMBDA which mimics a while loop in other languages. Now Excel Formulas are a "functional programming language" which limits what you can do with iteration because there are no "side-effects", i.e. you can't change anything except the value returned by the function or LAMBDA in our case. Nonetheless there are some useful things you can do with this so let's explore this below.

As alluded to above, recursion in Excel can be a bit tricky to get right and in order to simplify things I've bit I've created the RECURSIVE♾ LAMBDA function in order to hopefully simplify it a bit. RECURSIVE♾ takes two arguments:

  1. a function to make recursive
  2. the argument_count of the function

In order to make recursion work in Excel the function also has to take another argument, let's call it self (although you can call it whatever you like really). Note that the argument_count is the number of arguments without including self as you'll see below.

The canonical example of a recursive function is fib which returns the n-th Fibonacci number. Let's see how to implement FIB♾ in Excel:

FIB♾ = RECURSIVE♾(LAMBDA(self, n, 
    IF(n <= 2, 1, self(self, n - 1) + self(self, n - 2))
), 1);
  • First note how the first argument of the LAMBDA function is self. If you know Python then this might remind you of the self argument of methods in Python or this in methods in Java. I chose this naming convention since this might already be familiar to you. If not then just remember that self must always be the first argument and can be used to refer to the function you are writing inside the function. Crucially it also must be passed as the first parameter in any recursive calls, for example in self(self, n - 1). self(self, ...) is a bit of a mouthful but it's the best we can do in this setup.
  • The second argument to RECURSIVE♾ is 1 since our recursive FIB♾ function will only take 1 argument n. Note that the function self has 2 arguments, self and n, but self is a "nuisance" parameter and an implementation detail so we don't include it in our count.
  • You might be wondering about the ♾ in RECURSIVE♾ and FIB♾. That doesn't have any special meaning but I thought it would be a nice touch to have a convention to name all recursive LAMBDAs with a ♾ (the infinity symbol) at the end to mark that you might potentially get yourself in an infinite loop. That's not strictly true since you'll probably hit a Stack Overflow error instead of waiting indefinitely while contemplating the Halting Problem (another Turing contribution) but I thought it was a nice touch nonetheless.

With this in hand you can now use this anywhere in the Excel grid, for example:

=FIB♾(7)

which should hopefully give you 13.

Great, so we've seen that we can do recursion in Excel but can we do anything actually useful with it?

Just this week one of our Portfolio Managers asked me to assist with a portfolio optimisation problem. While I used the Excel Solver Add-in for that, objective function maximisation and root finding are a nice application so let's explore that.

Over the next two weeks, the festive "spirits" of many people can be approximated by the following XMAS function:

XMAS = LAMBDA(d, 1 + MAX(-1, -(d - 45651.5) * (d - 45651.5) * 
    (d - 45658) * (d - 45658) / 200));

The parameter d is an Excel date value and the output is scaled to the range [0,1]. Let's try to determine if the function has any maxima over the next two weeks. Hopefully you'll recall from calculus that in order to find the turning points of a function you need to find the derivative of the function and set that to zero. We can do this numerically in Excel with the following DERIVATIVE LAMBDA:

DERIVATIVE = LAMBDA(f, [step_size], [order],
    LET(
        _eps, IFOMITTED(step_size, 0.000001),
        _order, IFOMITTED(order, 2),
        CHOOSE(
            _order,
            LAMBDA(x, (f(x + _eps) - f(x)) / _eps),
            LAMBDA(x, (f(x + _eps) - f(x - _eps)) / _eps / 2)
        )
    )
);

This function takes a function f as parameter and returns another LAMBDA which approximates the derivative numerically. In order to find where this derivative is zero we can use a "root-finding" algorithm such as the Newton-Raphson method. This is a recursive procedure which I've implemented as follows:

FINDROOT♾ = RECURSIVE♾(
    LAMBDA(self, function, [guess], [target], [derivative_function], [tolerance],
        // Uses Newton-Raphson method and numerical derivative to find function roots
        LET(
            _x, IFOMITTED(guess, 0),
            _target, IFOMITTED(target, 0),
            _f, IF(_target = 0, function, LAMBDA(x, function(x) - _target)),
            _tolerance, IFOMITTED(tolerance, 0.000001),
            _fprime, IFOMITTED(derivative_function, DERIVATIVE(function)),
            _xn, _x - _f(_x) / _fprime(_x),
            IF(ABS(_xn - _x) < _tolerance, _xn, 
               self(self, _f, _xn, 0, _fprime, _tolerance))
        )
    ),
    5
);

You'll notice that we again used RECURSIVE♾ and this time the argument_count was 5. Some of you will have noticed that only 1 argument is required and the last 4 arguments are optional. Please note that argument_count must include all required and optional parameters, with the exception of self.

If you try:

=FINDROOT♾(DERIVATIVE(XMAS))

you'll get an error. That is because the XMAS function is 0 outside the range [DATE(2024,12,23), DATE(2025,01,03)] and the Newton-Raphson needs a non-zero gradient to work. We can remedy the situation by supplying guess starting value near a suspected root/maximum. If you try:

=FINDROOT♾(DERIVATIVE(XMAS), DATE(2024,12,24))

you should get 2024-12-25 12:00 (be sure to include HH:MM in your output formatting). We can check that this is indeed a maximum with:

=AND(DERIVATIVE(XMAS)(DATE(2024,12,25)+0.5)=0, 
     DERIVATIVE(DERIVATIVE(XMAS))(DATE(2024,12,25)+0.5)<0)

which should return TRUE so this is indeed a maximum. So according to our XMAS function, festive "spirits" might peak around 2024-12-25 12:00 PM. 🎄

There's another maximum hidden in the XMAS function. Can you find the date and time? Share your approach in the comments!

I will be taking a break over the next few weeks and will be back next year with a follow up on CURRYing. Whether you're celebrating and/or taking a break or not, have a Merry Festive Season and all the best for the New Year! 🍾🥂

#Excel #Recursion #FunctionalProgramming

/*
DERIVATIVE
Creates a function that approximates the derivative of another function.
Supports both forward difference and central difference methods.
Inputs:
- f: Function to differentiate
- step_size (optional, default=0.000001): Size of step for approximation
- order (optional, default=2):
1 = Forward difference method
2 = Central difference method (more accurate)
Return:
A function that calculates f'(x) using numerical differentiation
Example:
=LET(f, LAMBDA(x, x^2), df, DERIVATIVE(f), df(3)) ' Calculates derivative of x^2 at x=3
*/
DERIVATIVE = LAMBDA(f, [step_size], [order],
LET(
_eps, IFOMITTED(step_size, 0.000001),
_order, IFOMITTED(order, 2),
CHOOSE(
_order,
LAMBDA(x, (f(x + _eps) - f(x)) / _eps),
LAMBDA(x, (f(x + _eps) - f(x - _eps)) / _eps / 2),
)
)
);
/*
FIB♾ (Recursive Fibonacci)
Calculates the nth Fibonacci number using recursion.
Uses RECURSIVE♾ to simplify recursive calculation in Excel.
Input:
- n: Which Fibonacci number to calculate (positive integer)
Return:
The nth Fibonacci number where F(n) = F(n-1) + F(n-2) and F(1)=F(2)=1
Note:
Performance degrades exponentially for large n due to recursive nature
*/
FIB♾ = RECURSIVE♾(LAMBDA(self, n, IF(n <= 2, 1, self(self, n - 2) + self(self, n - 1))), 1);
/*
FINDROOT♾ (Newton-Raphson Root Finder)
Finds a root of a function using the Newton-Raphson method.
Can find x where f(x)=target or f(x)=0.
Inputs:
- function: Function to find root of
- guess (optional, default=0): Initial guess for root
- target (optional, default=0): Target value (finds where function=target)
- derivative_function (optional): Function's derivative. If omitted, uses numerical approximation
- tolerance (optional, default=0.000001): How close to zero is good enough
Return:
Value of x where function(x)=target (or 0 if no target specified)
Example:
=FINDROOT♾(LAMBDA(x,x^2-4)) ' Finds square root of 4
*/
FINDROOT♾ = RECURSIVE♾(
LAMBDA(self, function, [guess], [target], [derivative_function], [tolerance],
// Uses Newton-Raphson method and numerical derivative to find function roots
LET(
_x, IFOMITTED(guess, 0),
_target, IFOMITTED(target, 0),
_f, IF(_target = 0, function, LAMBDA(x, function(x) - _target)),
_tolerance, IFOMITTED(tolerance, 0.000001),
_fprime, IFOMITTED(derivative_function, DERIVATIVE(function)), // since the target has no effect we ignore it
_xn, _x - _f(_x) / _fprime(_x),
IF(ABS(_xn - _x) < _tolerance, _xn, self(self, _f, _xn, 0, _fprime, _tolerance))
)
),
5
);
/*
IFOMITTED
Returns a default value if a parameter is omitted, with optional else clause.
Alternative to IF(ISOMITTED()) with cleaner syntax.
Inputs:
- value: The value to check
- value_if_omitted: Value to return if 'value' is omitted
- else (optional): Value to return if 'value' is not omitted. If omitted, returns 'value'
Examples:
=IFOMITTED([A1], 0) ' Returns 0 if A1 omitted, otherwise A1
=IFOMITTED([A1], 0, "X") ' Returns 0 if A1 omitted, "X" otherwise
*/
IFOMITTED = LAMBDA(value, value_if_omitted, [else],
IF(ISOMITTED(value), value_if_omitted, IF(ISOMITTED(else), value, else))
);
/*
RECURSIVE♾ (Recursion Helper)
Enables recursive functions in Excel by handling the self-reference pattern.
Uses infinity symbol to indicate potential for infinite recursion.
Inputs:
- function: Lambda that takes itself as first parameter for recursion
- argument_count: Number of arguments function takes (excluding self parameter)
Return:
A properly wrapped recursive function
Example:
=LET(fact, RECURSIVE♾(LAMBDA(self,n,IF(n<=1,1,n*self(self,n-1))), 1), fact(5))
*/
RECURSIVE♾ = LAMBDA(function, argument_count,
CHOOSE(
argument_count,
LAMBDA([_1], function(function, _1)),
LAMBDA([_1], [_2], function(function, _1, _2)),
LAMBDA([_1], [_2], [_3], function(function, _1, _2, _3)),
LAMBDA([_1], [_2], [_3], [_4], function(function, _1, _2, _3, _4)),
LAMBDA([_1], [_2], [_3], [_4], [_5], function(function, _1, _2, _3, _4, _5)),
LAMBDA([_1], [_2], [_3], [_4], [_5], [_6], function(function, _1, _2, _3, _4, _5, _6)),
LAMBDA([_1], [_2], [_3], [_4], [_5], [_6], [_7],
function(function, _1, _2, _3, _4, _5, _6, _7)
),
LAMBDA([_1], [_2], [_3], [_4], [_5], [_6], [_7], [_8],
function(function, _1, _2, _3, _4, _5, _6, _7, _8)
),
LAMBDA([_1], [_2], [_3], [_4], [_5], [_6], [_7], [_8], [_9],
function(function, _1, _2, _3, _4, _5, _6, _7, _8, _9)
),
LAMBDA([_1], [_2], [_3], [_4], [_5], [_6], [_7], [_8], [_9], [_10],
function(function, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10)
),
)
);
/*
XMAS
Input:
- d: Excel date value to evaluate
Return:
A value between 0 and 1
*/
XMAS = LAMBDA(d, 1 + MAX(-1, -(d - 45651.5) * (d - 45651.5) * (d - 45658) * (d - 45658) / 200));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment