Lambda Example: Generate Fibonacci series

Brass Contributor

In this post, I would like to explain how I have used Lambda to create a function to generate a Fibonacci series array.

 

This example can also be used to understand how to create an array where the value of an element in the array depends on previous elements in the same array.

 

Generating Fibonacci as a dynamic array by @VizGenerating Fibonacci as a dynamic array by @Viz

 

This function has been created using three function in two layers. The inner layer functions include the following:

InFib: This function generates the Nth Fibonacci number

InFibSer: This function generates the entire Fibonacci series up to the Nth number

 

The two functions mentioned above require arguments that are complicated and less intuitive. Therefore, we use an outer layer function (FibSeries) that generates the necessary argument and calls the two functions to generate the series.

 

=InFIB

'This function returns nth Fibonacci using tail recursion
'It takes three arguments n, a=0 , b=1 as argument and returns the nth Fibonacci value

 

 

=LAMBDA(n,a,b,IF(n=1,a,IF(n=2,b,Infib(n-1,b,a+b))))
/* The value for the arguments a and b should be passed as 0 and 1, respectively. */

 

 

 

Example:

=InFIB(10,0,1) would generate 34 as the value

 

Our objective, here, is not to obtain a single value but to generate an entire array. However, here is where we have a challenge: While it is easier to manipulate an existing array, it is far more to difficult to create a new array (other than sequence array) or extend an existing array.

 

Therefore, in order to create a Fibonacci series, we are going to pass a dummy array of n values. This array will have 0 as the first element and 1 as the value for all the other elements.

 

The following image portrays the recursive addition process that converts that the initial array into a Fibonacci series.

 

Fibonacci algo illustration.png

 

The following code does carries out the process recursively till the maximum value of the array equals the nth Fibonacci value.

 

=INFIBSER

'This inner function takes three arguments and returns a Fibonacci series
'The three arguments are nth Fibonacci value, the initial array and a sequence array (sequential numbers from 1 to n)
'Note, the sequence array can instead be generated inside the function but it may inefficient

 

 

=Lambda(NthFib, Initval, Sqn,
	Let(
	    Maxval,Max(Initval),
	    If(Maxval = NthFib,
		Initval,
		Let(
		    NewVal,If(Maxval=1,Initval+1*(Sqn>=4),
		              Let(
			          StPos,Match(Maxval,Initval,0)+1,
			          adval,Large(unique(Initval),2),
			          Initval+adval*(Sqn>=StPos)
		                  )
			     ),
		    InFibser(NthFib,NewVal,Sqn)
		)
	)
)
 )

 

 

 

In the first iteration, the above function adds one to all the values from the fourth position onwards. For the subsequent iterations it adds the second largest unique value to all values that all 2 places after the position of such second largest value. The above function uses the match function to identify the position pointer.

 

Although the above function can generate the Fibonacci series, as mentioned earlier, it requires arguments that are less intuitive and which requires some set up.

 

Therefore, here we have an outer layer function that requires only one argument, n. It creates the necessary argument to be passed to the inner layer function and calls them.

 

=FIBSERIES

'This outermost function generates an initial array, a sequence array, and nth fibonnaci value
'It then calls the inner functions with these parameters 

 

 

 

=Lambda(n,
	Let(
		Sqn,sequence(n),
		initval,sign(Sqn-1),
		Nthfib,Infib(n,0,1),
		InFibser(Nthfib, Initval, Sqn)
	)
 )

 

 

 

This final function can be used by the user to generate the Fibonacci series by merely passing the number of values required as argument.

 

Hope you found this useful. I would love to know if you think there is a way to improve the algorithm, here. I am especially looking for suggestions that could replace the match / xmatch in the InFibSer function to get the position pointer. Since the positions move sequentially, I believe a mathematical solution may exist.

 

Thank you

82 Replies

@lori_m 

Oops, It took time to understand the formula, still don't sure if I took it correctly or not. Fibonacci and efficiency don't matter, approach is great.

@lori_m this is an interesting idea, I was trying to test it, we need to treat the special case of n=1, because it provides the wrong result {1;1}, but it is an easy fix:

 

 

Fib=LAMBDA(n,IF(n=1, 1,REDUCE(1,SEQUENCE(n-1),
  LAMBDA(b,_,VSTACK(b,SUM(TAKE(b,-2)))))));

 

 

I don't fully understand the underscore notation (_) I google it, but I was not able to find any reference. Would you elaborate it more or share some link? thanks. My intuition, from other languages, it is a dummy variable, or unnamed variable, because it is not really used for other purpose than to define the number of iterations, but their values are not really used.

 

I modified it to use BIGADD. I incorporated @mtarler's  fix:

 

 

BigAdd = LAMBDA(A, B, [c], let(cc, if(isomitted(c),0,c), 
    if((len(a)>14)+(len(b)>14), 
    let(lowadd, right(a,14)+right(b,14)+cc,
        ccc, if(len(lowadd)>14,1,0),
        aaa, if(len(a)>14,left(a,len(a)-14),0),
        bbb, if(len(b)>14,left(b,len(b)-14),0),
        bigadd(aaa,bbb,ccc)
        & text(right(lowadd,14),"00000000000000")),
    (a+b+cc)&"")));

 

 

Here is a possible solution for large Fibonacci numbers, I think we need to separate the cases n=1, n=2 from the iteration process to make it works and then to generate the sequence from n-2:

 

 

FIBO_SERIE_STR_LM2=LAMBDA(n,IF(n<3, SEQUENCE(n)^0&"",
  REDUCE({"1";"1"},SEQUENCE(n-2),
  LAMBDA(b,_,VSTACK(b,BIGADd(@TAKE(b,-1), 
    @CHOOSEROWS(b,-2)))))));

 

 

It works as expected. In terms of performance about the same as your previous formula using recursion. Which is expected since this approach is similar to mine and similar to @mtarler, but your is more concise and invokes less functions, I have an extra HSTACK and INDEX for example, so I would expect that for larger dataset your approach will show some performance improvements.

 

David

 

@Sergei Baklan 

Maybe you understand @lori_m 's formula but I am still struggling!  I even tried pulling the formula apart using local names but, though the formula still works, there has not been a huge leap forward in terms of its transparency. 

= LET(
    identityλ, LAMBDA(i,i),
    k,         SEQUENCE(10),
    fibArrλ,   SCAN(identityλ, k,
                 LAMBDA(fnλ,_, LAMBDA(i,
                   i*fnλ(0)+fnλ(1)
               ))
    ),
    MAP(fibArrλ, LAMBDA(fibλ,fibλ(0))
    )
  )

@davidleal  In response to the points you raise...

 

Underscore _:

This is used to specify an unused variable in Python, I don't know if this convention is more widespread. See Fibonacci code further down this link

https://realpython.com/python-itertools/#recurrence-relations

underscore is used both within accumulate (equivalent to SCAN) and within the for loop 

 

Case n=1:

If one decides to follow a strict convention then yes it makes sense to add n=1 case. On the other hand if Fibonacci is considered more as a playground of ideas (as per Sergei's comment) such details are of less significance and inserting extra conditions may distract from the essence of a chosen approach. In any case since the sequence requires a pair of initial values, either (0,1) or (1,1). any of  0, 1 or #error may be considered possible conventions for n=1.

@Peter Bartholomew 

I think the idea of replacing arrays by functions arose after attempting the FFT challenge you posted for general radix. One can't hold real and imaginary 2D arrays simultaneously, but instead one can add a lambda parameter to choose between the two arrays - a bit like adding a third dimension.

 

In the formula, the function argument of SCAN is given by:

LAMBDA(f,_, LAMBDA(i,i*f(0)+f(1)))

The idea here is to rewrite the pair (x, y), say, as a function: f(i) i=0,1. i.e. 

f(i) <- i*f(0)+f(1), i = 0,1

or more explicitly

f(0),f(1) <- f(1),f(0)+f(1)

 

Similarly if we set as initial value:

f = LAMBDA(i,i), i = 0,1

then

f(0),f(1) <- 0,1

The initial value of i needs to be input which was a problem for an array of lambdas so I ended up passing to MAP instead,. No doubt there's numerous improvements to be made - I'd be interested in other ideas to so this kind of thing.

@lori_m 

Hi Lori, by the time you got to the third formula there was the start of something familiar!  I have no idea how you managed to dream that up as a strategy but I am at least in with a chance of following it!

 

On a different matter, I asked the the AI search engine in Edge what is significant about the sum of the squares of two adjacent Fibonacci numbers and it came back with a formula and the statement: "if you square two consecutive Fibonacci numbers and add them, you get another Fibonacci number that is twice as far in the sequence".  Search engine technology seems to be progressing!

 

@Peter Bartholomew 

I wasn't aware of that sum of squares identity! After some reflection, I think it can be derived from the 2x2 matrix representation which also provides a fast calc of larger Fibonacci numbers,

=INDEX(REDUCE({1,1;1,0},SEQUENCE(10),LAMBDA(a,_,MMULT(a,a))),2,1)

This matches @davidleal's FIBO_SERIE_STR_LM2(1024) last entry to 15sf as well as this alternative:

Fib(n)
=TEXTBEFORE(
    SCAN(
        "0,1",
        SEQUENCE(n),
        LAMBDA(a, _,
            TEXTAFTER(a, ",") & "," &
                BigAdd(TEXTBEFORE(a, ","), TEXTAFTER(a, ","))
        )
    ),
    ","
)

 

@lori_m 

I started by looking at 

= F(m)*F(n) + F(m+1)*F(n+1) 

setting m=1, and applying the basic recursion formula to F(n+1).  As usual, once one has finished you know what to search for and can find the answer.

@lori_m just a minor correction to your first formula using matrix exponentiation, in order to make it works:

 

FIBO_MAT=LAMBDA(n, LET(a, {1,1;1,0}, IF(n=1,1, 
 INDEX(REDUCE(a,SEQUENCE(n-1),LAMBDA(ac,_,MMULT(ac,a))),2,1))))

 

Rather than the exponential matrix of the accumulator (ac), it is from the initial matrix (a), so I created a name for that. I haven't done any performance testing, but anyway I have seen this matrix multiplication can be simplified, calculating directly each element, so there is no need to do a matrix multiplication (resulting in O(log n)). I would say this would improve the performance. For example the following python code, uses this idea, but I don't know if it has an easy conversion to Excel (Fast Exponentiation)

 

def fib(n):
    v1, v2, v3 = 1, 1, 0    # initialise a matrix [[1,1],[1,0]]
    for rec in bin(n)[3:]:  # perform fast exponentiation of the matrix (quickly raise it to the nth power)
        calc = v2*v2
        v1, v2, v3 = v1*v1+calc, (v1+v3)*v2, calc+v3*v3
        if rec=='1':    v1, v2, v3 = v1+v2, v1, v2
    return v2

 

I guess the logic is based on this mathematical explanation (video) and also here another python implementation using fast exponentiation with the mathematical justification.

 

Regards,

 

David

@Peter Bartholomew 

Yes, the wikipedia entry was quite long and I hadn't noticed it before. Regarding the SCAN version above, I recall seeing some examples from you applying string concatenation in a similar way, this one compared favourably against the other REDUCE/VSTACK approach (0.2s vs 0.7s for 1024 elements)

 

@davidleal 

If there were an efficient BigMul function available maybe we could make use of that kind of approach in Excel but otherwise i think we are limited by the precision.

@davidleal  so I did not read up on any of this but do have 2 questions:

a) Why are you using a [2x2] * [2x2] instead of [1x2] * [2x2] ?  Basically aren't you using the matrix multiplication to hold your prior value so [a b] * [ 1,1; 1,0] => [a+b,  a] and you don't need the second row?

b) Why are you then using INDEX( .... ,2, 1) which is essentially the value 1 calculation behind instead of SEQUENCE(n-2) and INDEX(... 1,1) or in my suggestion just INDEX(..., 1)

I don't see any time performance improvements using my crude timer but it just makes sense to me:

FIBO_MAT=LAMBDA(n, LET(a, {1,1;1,0}, 
    IF(n>2,
       INDEX(REDUCE({1,1},SEQUENCE(n-2),
         LAMBDA(ac,_,MMULT(ac,a))),1),
       1)))

@mtarler 

I'm glad you raised this point as I think there was a misunderstanding in the "corrected" version - probably due to being insufficiently clearly expressed on my part. The REDUCE example was actually designed to show that Fib(1024) could be calculated in just 10 steps by successively squaring the 2x2 matrix. The video David linked to extends this method to calculate arbitrary fibonacci numbers faster by using the binary representation of a number to decompose into powers of 2. Iterating the 2x2 matrix multiplication misses the point somewhat.

I realize this solution is not the most efficient but it was fun to create and it's a bit different (Inspired by Pascal's Triangle).

 

 

'DiagonalSum
=LAMBDA(a,v,VSTACK(
    a,
    IF(v < 3, 1, SUM(IFERROR(COMBIN(SEQUENCE(v - 1, , v - 1, -1), SEQUENCE(v - 1, , 0)), 0)))
))

'Fib
=LAMBDA(elements,REDUCE(0, SEQUENCE(elements), DiagonalSum))

 

@Patrick2788 

I think I am becoming conditioned!  I only have to read the words Pascal's triangle and I find myself writing

=REDUCE(1, SEQUENCE(n),
    LAMBDA(triangle, k,
        LET(
            previous, TAKE(triangle, -1),
            left,     HSTACK(previous, 0),
            right,    HSTACK(0, previous),
            IFNA(VSTACK(triangle, left + right), "")
        )
    )
)

@Peter Bartholomew 

Does that create a pyramid-like array?

 

I went with a left-justified version:

'Pascal
=Lambda(n,DROP(
    REDUCE(
        "",
        SEQUENCE(, n),
        LAMBDA(a, v, VSTACK(a, EXPAND(COMBIN(v - 1, SEQUENCE(, v, 0)), , n, "")))
    ),
    1
))

@Patrick2788 

No, like yours it is left stacked.  Far more effort would be required for they layout than was needed for the calculation!  The main difference is that mine is more primitive in that it uses the recurrence relation from row to row rather that the functional form of the result.

Since you mention it though, how about this?

image.png

= REDUCE(1, SEQUENCE(n),
    LAMBDA(triangle,k,
      LET(
        previous, TAKE(triangle, -1),
        left,     HSTACK(previous, {0,0}),
        right,    HSTACK({0,0}, previous),
        triangle, VSTACK(HSTACK(0,triangle), left + right),
        IFERROR(IF(triangle,triangle,""),"")
      )
    )
  )

 

@Peter Bartholomew  OK all this inspired me to bring it back to this topic and create the "classic" fib spiral:

mtarler_0-1682426950615.png

=LET(n,10, LET(a, {1,1;1,0}, IF(n>2,
    DROP(REDUCE({1,1;1,1},SEQUENCE(n-2,,0),LAMBDA(ac,n,
      LET(fibNs,TAKE(ac,1,2),
      fibSp,DROP(ac,1),
      fibNextPair,MMULT(fibNs,a),
      fibNext, INDEX(fibNextPair,1),
      fibNextBlock, SEQUENCE(fibNext,fibNext,fibNext,0),
      fibSpiral, CHOOSE(MOD(n,4)+1,VSTACK(fibSp,fibNextBlock), HSTACK(fibSp,fibNextBlock), VSTACK(fibNextBlock,fibSp),HSTACK(fibNextBlock,fibSp)),
      VSTACK(fibNextPair,fibSpiral)  ))),1),
   "n must > 2")))
It reads very well and is elegant. It's a different approach than I've been studying. Pascal's Triangle is fascinating because of the sub-arrays contained within. I had been working on something where the triangle would be created and the fibonacci numbers would be pulled from the sum of the 'shallow diagonals'. I had to stop myself because I was creating a problem that didn't need to exist when a more direct approach is better!

@mtarler 

I didn't even think of trying that!  How about a bit of conditional formatting?

image.png

Every workbook should have one?

Following up on the earlier BigMul idea, it seems one could apply convolution to the digits, e.g. 123*321 could be translated to

=CONVOLVE({1,2,3},{3,2,1})

giving {3,8,14,8,3} then carry tens to return 39483. Combining with a simplified version of the @davidleal python method above (which appears to come from here ),

def fib(n):
    a,b =  0,1                       # initialise matrix [[a+b,b] ,[b,a]]]
    for rec in bin(n)[3:]:  
        a,b = a*a+b*b,b*(2*a+b)      # square of matrix 
        if rec=='1' :  a,b = b,a+b   # multiply by initial matrix
    return b

returned values for fib(10000) in agreement to several thousand digits. The FFT convolution method in the attachment is from this gist courtesy of @aaltomani