从入门到入土--函数数式编程

从入门到入土--函数数式编程

什么是函数式编程

它是一种编程范式,没有标准的定义。

函数式编程,或称函数程序设计泛函编程(英语:Functional programming),是一种编程范型,它将电脑运算视为函数运算,并且避免使用程序状态以及可变对象。

在函数式编程中,函数是头等对象即头等函数,这意味着一个函数,既可以作为其它函数的输入参数值,也可以从函数中返回值,被修改或者被分配给一个变量。λ演算是这种范型最重要的基础,λ演算的函数可以接受函数作为输入参数和输出返回值。

总结出以下信息:

  1. 范型特点: 函数运算,无状态,避免可变对象。
  2. 头等函数: 函数可传递,返回,赋值。
  3. λ演算基础: 函数输入输出,高阶函数。

λ演算

λ演算是一种数学形式,就像编程语言中的函数。想象它是一个超级简单的语言,只能定义函数和应用函数。λ演算的主要思想是函数可以作为输入和输出,就像数学里的函数一样。这让我们能够用更抽象、灵活的方式思考和表达计算。

比如:

假设我们有一个λ演算的表达式:λx.x+2

这个表达式表示一个函数,输入是 x,输出是 x+2。在这个λ演算中,我们可以将这个函数应用到一个值上,比如 3。应用的方式是将 3替换函数中的 x,得到 3+2,结果就是 5。

所以,这个λ演算的例子可以理解为一个简单的加法函数,它可以把输入的值加上 2。这展示了λ演算如何用简洁的方式表示函数和函数应用。

再进阶一下:

这个表达式表示一个函数,它接受两个参数 x 和 y,然后返回它们的乘积。在 λ 演算中,这个函数也可以被称为一个高阶函数,因为它返回另一个函数。

如果我们将这个函数应用到两个参数 3 和 4 上,过程如下:

(λx.λy.x×y) 3 4

首先,将 3 替换到 x 上,得到:

λy.3×y

然后,将 4 替换到 y 上,得到最终的结果:

3×4=12

这个例子展示了 λ 演算中的多参数函数和函数的嵌套应用。

基于上面的示例,我们来写一个箭头函数

const  num = x => x + 2

console.log(num(2))

是不是跟我们上面的示例λx.x+2很像?

一个箭头函数就是一个单纯的运算式,箭头函数我们也可以称为lambda函数,它在书写形式上就像是一个λ演算式

箭头函数

箭头函数是一种在编程语言中定义匿名函数的简洁语法。这种语法通常用于函数式编程和现代的JavaScript等编程语言中。

箭头函数没有独立的 thisargumentssuper 绑定,并且不可被用作方法。

箭头函数不能用作构造函数。使用 new 调用它们会引发 TypeError。它们也无法访问 new.target 关键字。

箭头函数不能在其主体中使用 yield,也不能作为生成器函数创建。

我们基于上面的num再改进一下,做其它的运算

const num_1 = (x,y) => x + y;
const num_2 = x => x + y; // 外部的y
const num_3 = x => y => x + y; // 闭包串联参数,柯里化

上面是基于针对数字的情况,如果我们针对函数做运算呢

const fn_1 = x => y => x(y);
const fn_2 = f => x => fx(x);
const add 1=(f =>f(5))(x=>x+2);//IIFE7=2+5
const add 2=(x=>y=>x+y)(2)(5);//IIFE 7=2+5

fn_x类型,表明我们可以利用函数内的函数,当函数被当作数据传递的时候,就可以对函数进行应用(apply),生成更高阶的操作。 并且x => y => x(y)****可以有两种理解,一种是x => y函数传入X => x(y),另一种是x传入y => x(y)

λ演算是一种抽象的数学表达方式,我们不关心真实的运算情况,我们只关心这种运算形式

将我们的程序分解为一些更可复用,更可靠且更易于理解的部分,然后在将他们组合起来,形成一个更易推理的程序整体。

用一个示例来说:

// 初级程序员
let arr = [1, 2, 3, 4]
let newArr = []
for (var i = 0; i < arr.length; i++) {
  newArr.push(arr[i] + 1)
}
console.log(newArr) //[2, 3, 4, 5]
// 函数式编程
let arr = [1, 2, 3, 4]
let newArr = (arr, fn) => {
  let res = []
  for (var i = 0; i < arr.length; i++) {
    res.push(fn(arr[i]))
  }
  return res
}
let add = item => item + 1 //每项加1
let multi = item => item * 5 //每项乘5
let sum = newArr(arr, add)
let product = newArr(arr, multi)
console.log(sum, product) // [2, 3, 4, 5] [5, 10, 15, 20]

函数式编程基础

函数的元、柯里化和Point-Free
函数的元:完全调用和不完全调用

不论使用何种方式去构造一个函数,这个函数都有两个固定的信息可以获取:

  • name 表示当前标识符指向的函数的名字。
  • length 表示当前标识符指向的函数定义时的参数列表长度。

在数学上,我们定义f(x) = x是一个一元函数,而f(x, y) = x + y是一个二元函数。在JavaScript中我们可以使用函数定义时的length来定义它的元。

// 一元函数
const one  = a => a;
// 二元函数
const two  = (a, b) => a + b;
// 三元函数
const three  = (a, b, c) => a + b + c;
柯里化函数

柯里化是一种函数的转换,它是指将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)(c)

柯里化不会调用函数。它只是对函数进行转换。

首先,函数的几种写法是等价的(最终计算结果一致)。

const add = (a, b) => a + b;
const add = a => b => a + b;

高级柯里化实现

function curry(func) {

  return function curried(...args) {
    if (args.length >= func.length) {
      return func.apply(this, args);
    } else {
      return function(...args2) {
        return curried.apply(this, args.concat(args2));
      }
    }
  };

}

function sum(a, b, c) {
  return a + b + c;
}

let curriedSum = curry(sum);

alert( curriedSum(1, 2, 3) ); // 6,仍然可以被正常调用
alert( curriedSum(1)(2,3) ); // 6,对第一个参数的柯里化
alert( curriedSum(1)(2)(3) ); // 6,全柯里化

当我们运行它时,这里有两个 if 执行分支:

  1. 如果传入的 args 长度与原始函数所定义的(func.length)相同或者更长,那么只需要使用 func.apply 将调用传递给它即可。
  2. 否则,获取一个部分应用函数:我们目前还没调用 func。取而代之的是,返回另一个包装器 pass,它将重新应用 curried,将之前传入的参数与新的参数一起传入。

然后,如果我们再次调用它,我们将得到一个新的部分应用函数(如果没有足够的参数),或者最终的结果。

  • 只允许确定参数长度的函数

柯里化要求函数具有固定数量的参数。

使用 rest 参数的函数,例如 f(...args),不能以这种方式进行柯里化。

  • 比柯里化多一点

根据定义,柯里化应该将 sum(a, b, c) 转换为 sum(a)(b)(c)

但是,如前所述,JavaScript 中大多数的柯里化实现都是高级版的:它们使得函数可以被多参数变体调用。

函数式编程特性

  • First Class 函数:函数可以被应用,也可以被当作数据。

函数本身也是数据的一种,可以是参数,也可以是返回值

const one = x => x + 1;
  • Pure 纯函数,无副作用:任意时刻以相同参数调用函数任意次数得到的结果都一样。

纯函数是指如果函数的调用参数相同,则永远返回相同的结果。它不依赖于程序执行期间函数外部任何状态或数据的变化,必须只依赖于其输入的参数(相同的输入,必须得到相同的输出)。

// 纯函数
const calculatePrice=(price,discount)=> price * discount
let price = calculatePrice(200,0,8)
console.log(price)

// 不纯函数
const calculatePrice=(price,discount)=>{
      const dt= new Date().toISOString()
        console.log(`${dt}:${something}`)
        return something
}
foo('hello')
  • Referential Transparency 引用透明:可以被表达式替代。

通过表达式替代(也就是λ演算中讲的归约),可以得到最终数据形态。

也就是说,调用一个函数的位置,我们可以使用函数的调用结果来替代此函数调用,产生的结果不变。

一个引用透明的函数调用链永远是可以被合并式代换的。

const add = (a, b) => a + b;
const add = a => b => a + b;
  • Expression 基于表达式:表达式可以被计算,促进数据流动,状态声明就像是一个暂停,好像数据到这里就会停滞了一下。
  • Immutable 不可变性:参数不可被修改、变量不可被修改—宁可牺牲性能,也要产生新的数据(Rust内存模型例外)。

一个函数不应该去改变原有的引用值,避免对运算的其他部分造成影响。

const man = {age: 1};
const nextYear = man => ({age: man.age + 1})
const future = times(19, nextYear)(man)
future !== man //true

一个充满变化的世界是混沌的,在函数式编程世界,我们需要强调参数和值的不可变性,甚至在很多时候我们可以为了不改变原来的引用值,牺牲性能以产生新的对象来进行运算。牺牲一部分性能来保证我们程序的每个部分都是可预测的,任意一个对象从创建到消失,它的值应该是固定的。

一个元如果是引用值,请使用一个副本(克隆、复制、替代等方式)来得到状态变更。

  • High Order Function 大量使用高阶函数:变量存储、闭包应用、函数高度可组合。

高阶函数是对其他函数进行操作的函数,可以将它们作为参数或返回它们

简单来说,高阶函数是一个函数,它接收函数作为参数或将函数作为输出返回

// 用redece做累加
let arr = [1, 2, 3, 4, 5]
let sum = arr.reduce((pre, cur) => {
  return pre + cur
}, 10)

// 用redece做去重
let arr = [1, 2, 3, 4, 5, 3, 3, 4]
let newArr = arr.reduce((pre, cur) => {
  pre.indexOf(cur) === -1 && pre.push(cur)
  return pre
}, [])
console.log(newArr) //[1, 2, 3, 4, 5]
// 参数为函数的高阶函数
function foo(f) {
  // 判断是否为函数
  if (typeof f === 'function') {
    f()
  }
}
foo(function() {})

高阶函数让我们可以更好地组合业务。常见的高阶函数有:

  1. map
  2. compose
  3. fold
  4. pipe
  5. curry
  6. ….
  • Curry 柯里化:对函数进行降维,方便进行组合。

在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一些列使用一个参数的函数技术(把接受多个参数的函数转换成几个单一参数的函数)

// 没有柯里化的函数
 function girl(name,age,single) {
   return `${name}${age}${single}`
 }
  girl('张三',180,'单身')
  // 柯里化的函数
  function girl(name) {
    return function (age){
       return function (single){
         return `${name}${age}${single}`
      }
    }
  }
  girl('张三')(180)('单身')
  • Composition 函数组合:将多个单函数进行组合,像流水线一样工作。

函数式编程范式包括许多基础特性,巧妙组合可形成强大工具。它让我们以数学推导的方式处理函数,将复杂问题简化,并通过累加/累积得到结果。函数式编程类似数学学习,让编程更抽象。