I’m a person who pursues simplicity and neatness in code, and I’ve certainly heard of elegant functions for years. This is just a simple insight into the function. I just grasp the scattered knowledge points, there is no deeper understanding, first write down this learning process.

According to my simple understanding, the function can be divided into the following parts:

  • The function is a first-class citizen
  • Scopes and closures
  • Higher order functions and Currization
  • Tail recursion
  • Inertia is evaluated
  • The purity
  • invariance

In js, everything is an Object. Everything inherits from the prototype of Object. Object should not be the largest. But in the function formula, the function is the most basic thing, so why?

A function can be stored as a variable

let fortytwo = function() { return 42 }
Copy the code

Can be stored as an element of an array

let fortytwos = [42.function() { return 42 }]
Copy the code

Can be stored as a member variable of an object

let fortytwos = { number: 42.fun: function() { return 42 }}
Copy the code

I can just create it

42 + (function() { return 42}) ()Copy the code

Can be passed to another function

function weirdAdd(n, f) { return n + f() }
weirdAdd(42.function() { return 42 })
Copy the code

Can be returned by another function

return function() { return 42 }
Copy the code

Just because functions can exist anywhere, functions can be first-class citizens.


So it’s back to the old arguments about what scope and closure are, and those are the two most important features of a function.

This is a topic that I covered in detail in my previous article, Lexical/function/block scope and closures.


Higher-order functions, such as Map, Reduce and filter, have already been covered in ES6. Take a function as an argument and return a function as a result.

const _ = require('underscore')

function best(fun, coll) {
    return _.reduce(coll, function(x, y) {
        return fun(x, y) ? x : y
    })
}

let result2 = best(function(x, y) {
    return x > y
},
[1.2.3.4.5])console.log(result2)
Copy the code
littlebird@ubuntu:~/js--/The higher-order function $node4-1-1.js 
5
Copy the code

The comparison function above takes a function and an object, returns a reduce function, puts in the first object argument and a function, and returns the result of a function.

Now let’s talk about Currization

A Currying is a method that converts a function that takes multiple arguments into a function that takes a single argument (the first argument of the original function) and returns a new function that takes the remaining arguments and returns a result.

function multiplyBy(num1) {
  return function(num2) {
    return num1 * num2
  }
}

const multiplyByThree = multiplyBy(3)
console.log(multiplyByThree(4))
Copy the code
littlebird@ubuntu:~/js--/Currie transforms $node test.js12
Copy the code

This is easy. Let’s take a look at the following example. What happens when there are multiple arguments?

function add(. args) {
  return args.reduce((total, currentVal) = > {
    total += currentVal
    return total
  }, 0)}Copy the code

This is a function that takes the sum of any parameters, easy enough, right? So how do you get the parameters to be Corrified?

function curry(fn) {
  let allArgs = []
  return function reFn(. args) {
    if (args.length === 0) {
      return fn.apply(this, allArgs)
    } else {
      allArgs = allArgs.concat(args)
      return reFn
    }
  }
}
Copy the code

If there is one, add it to the array. Then return the current function and keep calling.

const curryAdd = curry(add)
console.log(curryAdd(1) (2) (3) (4.5)())

littlebird@ubuntu:~/js--/Currie transforms $node test.js15
Copy the code

Another type of recursion – tail recursion

A recursive function is said to be tail-recursive if all recursive calls to it occur at the end of the function. A recursive call is tail recursion when it is the last statement to be executed in the entire function body and its return value is not part of the expression. Tail-recursive functions have the property of doing nothing during regression, which is important because most modern compilers take advantage of this feature to automatically generate optimized code.

Let’s look at an example

Linear recursive

function Reasuvie(n) {
  return (n == 1)?1 : n * Reasuvie(n - 1)}console.log(Reasuvie(5))
Copy the code

Tail recursion

function TailReasuvie(n, a = 1) {
  return (n == 1)?1 : TailReasuvie(n - 1, a * n)
}
console.log(TailReasuvie(5))
Copy the code

Let’s look at how these two recursions work in detail

// Linear recursion
Rescuvie(5)
{5 * Rescuvie(4)}
{5 * {4 * Rescuvie(3)}}
{5 * {4 * {3 * Rescuvie(2)}}}
{5 * {4 * {3 * {2 * Rescuvie(1)}}}}
{5 * {4 * {3 * {2 * 1}}}}
{5 * {4 * {3 * 2}}}
{5 * {4 * 6}}
{5 * 24}
120

/ / tail recursion
TailRescuvie(5)
TailRescuvie(5.1)
TailRescuvie(4.5)
TailRescuvie(3.20)
TailRescuvie(2.60)
TailRescuvie(1.120)
120
Copy the code

Tail recursion performs much better than linear recursion, because instead of opening a new stack each call, it is stored in n and A.


Lazy evaluation is also an optimization of program performance

const nums = Lazy().range(0.100 _000_000_000_00).map(n= > n * 10).filter(n= > n % 3= = =0).take(2)
for (let n of nums) {
  console.log('num:\t', n, '\n')}Copy the code

The code above generates a large range of data and then, after the operation, only fetches the first two data, which can be very performance costly without lazy evaluation.

Let’s implement these functions

const range = function* (from , to) {
  for (let i = from; i < to; i++) {
    console.log('range\t', i)
    yield i
  }
}

const map = function* (iterator, transform) {
  for (const data of iterator) {
    console.log('map\t', data)
    yield transform(data)
  }
}

const filter = function* (iterator, condition) {
  for (const data of iterator) {
    console.log('filter\t', data)
    if (condition) {
      yield data
    }
  }
}

const stop = function* (iterator, condition) {
  for (const data of iterator) {
    yield data
    if (condition(data)) {
      break}}}const take = function (iterator, number) {
  let count = 0
  const _condition = function (data) {
    count++
    return count >= number
  }
  return stop(iterator, _condition)
}
Copy the code

A chained call of a class returns this, the instance of the current call, in each method.

function Lazy(iterator) {
  const Fn = {
    range(. args){ iterator = range(... args)return Fn
    },
    map(. args){ iterator = map(iterator, ... args)return Fn
    },
    filter(. args){ iterator = filter(iterator, ... args)return Fn
    },
    take(. args){ iterator = take(iterator, ... args)return Fn
    },
    values() {
      let arr = []
      for (const num of this) {
        arr.push(num)
      }
      return arr
    },
    [Symbol.iterator]() {
      return iterator
    }
  }
  return Fn
}
Copy the code

This implementation is similar to jquery’s chained invocation. How about taking a look at the printed result?

littlebird@ubuntu:~/js--/Lazy evaluation of $node test.js range0
map      0
filter   0
num:     0 

range    1
map      1
filter   10
num:     10 
Copy the code

You can see that the data here is calculated on demand, which is lazy evaluation.


What about purity and invariance?

Functional immutability is not absolute, functional systems strive to reduce visible state changes, and functional programming in practice is to minimize the occurrence of mutations in any known system. Just to ensure minimal data changes. So that brings us to the final question, what is purity? Well, what is a pure function? The return value has nothing to do with the function argument. No matter what happens to the outside, the return value of the function does not change.


The last word

Well, that’s about it for me, but I feel functional is still very difficult to use in engineering, easy to maintain and develop compared to object-oriented and today’s componentization. I went into functional programming with a learning mindset, and, well, that’s it. Readability and performance must come first before elegance.