Using js to write various Fibonacci sequence gracefully

Posted by moomsdad on Sat, 18 May 2019 03:50:04 +0200

fibonacci

When I read the official BuckleScript document, I found a Fibonacci code that brightened my eyes. The idea of implementation was something I had never thought of before. I still remember the power of recursion that Fibonacci sequence made me understand when I first learned programming, and now I have a new understanding of recursion. Here I will summarize all my valuable Fibonacci sequence implementations. If there is new knowledge in the future, it will be updated at the end of the article.

Let me take a fresh look at recursive code (the code is Ocaml, which I'll convert to JS later)

let fib n =
  let rec aux n a b =
    if n = 0 then a
    else
      aux (n - 1) b (a+b)
  in aux n 1 1

This code will be introduced in later versions of js

Normal recursive version

Let's start with the normal recursive version, which I often encounter when I was a beginner in programming. It let me know the power of recursion.

function fibonacci (n) {
   if(n==0) return 0
   else if(n==1) return 1
   else return fibonacci(n-1) + fibonacci(n-2)
}

The code is beautiful and logical. But one problem with this version is that there are a lot of duplicate computations. For example, when n is 5, fibonacci(4) + fibonacci(3) is calculated. When n is 4, fibonacci(3) + fibonacci(2) is calculated. Then fibonacci(3) is repeated. It takes half a day to run fibonacci(50) to produce results.

for Loop Version

Recursion has performance problems and is done with loops.

function fibonacci(n){
  var last = 1
  var last2 = 0
  var current = last2
  for(var i=1;i<=n;i++){
    last2 = last
    last = current
    current = last + last2
  }
  return current
}

This version does not have the problem of repeated calculation, and the speed is obviously much faster. This does not mean that loops are better than recursions. The problem with loops is that there are too many state variables. In order to achieve fibonacci, four state variables (last,last2,current,i) are used here, and state variables need to be extra careful in the process of writing, modifying and deleting. It will make me insecure. More state variables make it less beautiful to read.

Removing Recursive Versions of Repetitive Computing

This is the example at the beginning of the article to convert to the version of js

function fib(n){
  function fib_(n,a,b){
    if(n==0)  return a
    else return fib_(n-1,b,a+b)
   }
   return fib_(n,0,1)
}

Using the first two digits as parameters skillfully avoids repetitive calculation and improves the performance obviously. N decreases, the first two digits increment (Fibonacci sequence increment), this code is a subtraction, an increase, at first glance, a bit of brainpower. According to my habit, it's usually a total increase, let n start from zero to n.

Using Memory Function to Optimize Normal Recursive Version

The normal version of fibonacci is a pure function( What is a pure function? ) Pure functions can be optimized with memory functions, putting all the computations that need to be repeated into the cache.

function memozi(fn){
  var r = {}
  return function(n){
    if(r[n] == null){
      r[n] = fn(n)
      return r[n]
    }else{
        return r[n]
    }
  }
}

var fibfn = memozi(function(n){
    if(n==0){
        return 0
    }else if(n==1){
        return 1
    }else{
        return fibfn(n-1) + fibfn(n-2)
    }
})

It not only achieves the goal of performance improvement, but also does not destroy the elegance of the code itself.

Interesting inert sequences

fibonacci itself is a sequence of numbers, but infinitely large. Just use an infinite array to store fibonacci
However, there is no infinite array in js, so you need to construct one by yourself.

// Empty sequence
var _empty = {"@placeholder@":"@@"}
var _end = _empty
// The order pair constructs the value of the inert sequence and evaluates it only when it is needed. Here it is represented by function.
function pair(a,fn){
  return {
    left:a,
    right:fn
  }
}
function isFunction(p){
  return Object.prototype.toString.call(p) == "[object Function]"
}
function left(p){
  return p.left
}
function right(p){
  if(isEmpty(p.right)){
    return p.right
  }else if(isFunction(p.right)){
    return p.right(p)
  }else{
    throw "The second parameter of the sequence must be a function."
  }
}
function isEmpty(seq){
  return seq == _empty
}
function isArrEmpty(arr){
  return arr.length == 0
}

function toArray(seq){
  if(isEmpty(seq)){
    return []
  }else{
    return [left(seq)].concat(toArray(right(seq)))
  }
}
function toSeq(arr){
  if(isArrEmpty(arr)){
    return _end
  }else{
    return pair(arr[0],p=>toSeq(arr.slice(1)))
  }
}
function map(fn,seq){
  if(isEmpty(seq)){
    return _end
  }else{
    return pair(fn(left(seq)),p=>map(fn,right(seq)))
  }
}
function take(n,seq){
  if(isEmpty(seq)){
    return _end
  }else if(n==0){
    return _end
  }else{
    return pair(left(seq),p=>take(n-1,right(seq)))
  }
}

function zip(fn,seq1,seq2){
  if(isEmpty(seq1)){
    return _end
  }else if(isEmpty(seq2)){
    return _end
  }else{
    var l1 = left(seq1)
    var l2 = left(seq2)
    return pair(fn(l1,l2),p=>zip(fn,right(seq1),right(seq2)))
  }
}

var fibonacci = pair(0,p=>pair(1,p1=>zip((a,b)=>a+b,p,p1)))

You can run toArray (take (20, fibonacci) under console to see the output. It takes a while to run toArray (take (30, fibonacci) when the result appears. Don't get stuck with toArray(fibonacci), because Fibonacci is an infinite number of toArray values until memory is exhausted. Each function code line of the above code is a few lines of chat, looking directly at code analysis is better than redundant text interpretation.

This inert fibonacci also has performance problems. As with the first recursive version, there are a lot of repeated computations. The most direct solution is to add caches, that is to say, the evaluated values do not need to be re-evaluated.

Optimized version of inert sequence

// Empty sequence
var _empty = {"@placeholder@":"@@"}
var _end = _empty
// The order pair constructs the value of the inert sequence and evaluates it only when it is needed. Here it is represented by function.
function pair(a,fn){
  return {
    left:a,
    right:fn,
    rightCache:null
  }
}
function isFunction(p){
  return Object.prototype.toString.call(p) == "[object Function]"
}
function left(p){
  return p.left
}
function right(p){
  if(isEmpty(p.right)){
    return p.right
  }else if(isFunction(p.right)){
    if(p.rightCache != null){
      return p.rightCache
    }else{
      p.rightCache = p.right(p)
      return p.rightCache
    }
  }else{
    throw "The second parameter of the sequence must be a function."
  }
}
function isEmpty(seq){
  return seq == _empty
}
function isArrEmpty(arr){
  return arr.length == 0
}

function toArray(seq){
  if(isEmpty(seq)){
    return []
  }else{
    return [left(seq)].concat(toArray(right(seq)))
  }
}
function toSeq(arr){
  if(isArrEmpty(arr)){
    return _end
  }else{
    return pair(arr[0],p=>toSeq(arr.slice(1)))
  }
}
function map(fn,seq){
  if(isEmpty(seq)){
    return _end
  }else{
    return pair(fn(left(seq)),p=>map(fn,right(seq)))
  }
}
function take(n,seq){
  if(isEmpty(seq)){
    return _end
  }else if(n==0){
    return _end
  }else{
    return pair(left(seq),p=>take(n-1,right(seq)))
  }
}

function zip(fn,seq1,seq2){
  if(isEmpty(seq1)){
    return _end
  }else if(isEmpty(seq2)){
    return _end
  }else{
    var l1 = left(seq1)
    var l2 = left(seq2)
    return pair(fn(l1,l2),p=>zip(fn,right(seq1),right(seq2)))
  }
}

var fibonacci = pair(0,p=>pair(1,p1=>zip((a,b)=>a+b,p,p1)))

Calling toArray(take(30,fibSeq)) improves the speed of the previous version substantially. Click here to run the above code online

Pure Arrow Function Version

Strict conditions, the use of anonymous functions to achieve fibonacci function, the normal recursive version mentioned above as follows:

let fib = n => n > 1 ? fib(n-1) + fib(n-2) : n

Removing the name of fib is an anonymous version, but removing fib is not recursive, that is, calling itself. In fact, it can call itself indirectly. The code is as follows: (Note: I don't know how to describe it well)

(f=>n=>n>1?f(f)(n-1)+f(f)(n-2):n)(f=>n=>n>1?f(f)(n-1)+f(f)(n-2):n)(10)

Return to 55. Use f to ingeniously represent the clipping function itself

Run the above code online

Y Combinator + Arrow Function Version

It is Y Combinator that abstracts the function of arrow function calling itself. Li Si Li Supplement

let Y = f => (g=>f(a=>g(g)(a)))(g=>f(a=>g(g)(a)))

Now you can write the version of the clipping function like this

Y(f=>n=>n>1?f(n-1)+f(n-2):n)(10)

F represents the arrow function itself and uses f to recurse. Run the above code online

Topics: Programming less