把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象)
函数式编程中的函数指的不是程序中的函数(方法),而是数学中的函数即映射关系,例如:y = sin(x),x 和 y 的关系,相同的输入始终要得到相同的输出(纯函数)。
在函数式编程中,"抽象"通常指的是将代码中的重复模式或通用部分提取出来,并将其封装成可重用的函数或数据结构的过程。这种过程旨在减少代码的重复性,提高代码的可读性、可维护性和可复用性。
在函数式编程中,常见的抽象方法包括高阶函数、纯函数、函数组合、闭包和柯里化等。这些方法都可以帮助程序员更好地实现代码的抽象化,以便在更高的层次上思考问题并解决问题。
当一个函数接收另一个函数作为参数,这种函数就称之为高阶函数。
高阶段函数的特点:
使用高阶函数的意义:
我们常用的函数中 forEach、filter、some、every、map、reduce 都是高阶函数。例如在 map 函数中,不需要关心里面的细节,只需管理好 return 的值即可获得想要的重组后的数据组合。
x// 高阶函数 - 函数作为参数function forEach(array, fn) { for (let i = 0; i < array.length; i++) { fn(array[i]); }}// 测试let arr = [1, 2, 3, 4, 5];forEach(arr, function (item) { console.log(item);});声明在一个函数中的函数,叫做闭包函数。内部函数总是可以访问其所在的外部函数中声明的参数和变量,即使在其外部函数被返回了之后。
闭包的本质:
当函数执行完毕后会从执行栈上移除,但是堆上的作用域成员因为被外部引用而不能释放,因此内部函数依然可以访问外部函数的成员。
闭包的特点:
局部变量会常驻在内存中,可以避免使用全局变量,防止全局变量污染。如果被长期占用,而不被释放,会造成内存泄漏。
x
function makeSalary(base) { return function (performance) { return base + performance; };}let salaryLevel1 = makeSalary(12000);let salaryLevel2 = makeSalary(15000);console.log(salaryLevel1(2000)); // 14000console.log(salaryLevel2(3000)); // 18000纯函数指:相同的输入永远会得到相同的输出,而且没有任何的副作用。
当数据需要依靠外部来源的时候,纯函数的结果就得不到相同输出,这就是副作用。 数组的 slice 和 splice 分别是纯函数和不纯函数:slice 返回数组中的指定部分,不会改变原数组。splice 对数组进行操作返回该数组,会改变原数组。
纯函数的优点:
当一个函数有多个参数的时候先传递一部分参数调用它,返回一个已经记住了某些固定参数的新函数。新函数再接收剩余的参数,返回结果。使用柯里化可以避免硬编码的问题,这是一种对函数参数的缓存,让函数变得更灵活,粒度更小。
xxxxxxxxxx// 普通的纯函数// function checkAge(min, age) {// return age >= min;// }// console.log(checkAge(18, 20));// console.log(checkAge(18, 24));// console.log(checkAge(22, 24));// 函数的柯里化function checkAge(min) { return function (age) { return age >= min; };}let checkAge18 = checkAge(18);let checkAge20 = checkAge(20);console.log(checkAge18(20));console.log(checkAge18(24));数据抽象是一种编程技术,它可以将数据类型的实现细节隐藏起来,只暴露出必要的接口和操作,以便在使用该数据类型的代码中,让开发者更加关注数据类型的行为和语义,而不是其具体的实现细节。
数据抽象的目的是将代码中的复杂性降低到可管理的程度,并且使代码更加模块化和可重用。通过使用数据抽象,程序员可以在不暴露实现细节的情况下,将数据类型的接口和操作暴露给其他代码,从而促进代码的复用性和可维护性。
在面向对象编程中,数据抽象通常是通过类和接口来实现的。类定义了数据类型的属性和方法,而接口则定义了该数据类型所支持的操作。通过将类的实现细节隐藏在类的内部,并只暴露出公共接口,可以实现数据抽象,从而在实现类的代码中,更加关注数据类型的语义和行为。
在函数式编程中,数据抽象通常是通过封装数据结构和提供相关的操作函数来实现的。例如,在函数式编程中,可以定义一个列表数据结构,并提供相关的操作函数,例如添加元素、删除元素、查询元素等,从而实现对列表的数据抽象。通过封装数据结构和操作函数,可以实现对数据的抽象,从而促进代码的复用性和可维护性。
pair list
回到SICP的教学中,书中scheme语言提供了复合数据类型string、pair、list、vector.
其中list最为常用,而pair(点对)是由一个点和被它分隔开的两个值组成的,pair是其他复合数据类型的基础类型,list也就是由它实现的。从pair入手,来体会复合数据类型的构建。
pair类型由cons来定义,(cons 1 2) => (1 . 2)点前的值被称为car,点后面的值被称为cdr,可通过car,cdr来取值。[3]
xxxxxxxxxx; 通过这样的方式结成链(cons 1 (cons 2 (cons 3 cons 4 (cons nil)))); 当然我们也可以直接使用 list (list 1 2 3 4)(list 1 2 3 4) 等价于 (cons 1 (cons 2 (cons 3 (cons 4 nil))))
如同C/C++实现线性表一样,node定义了节点值与指针,多个node组成了链表。
从这里,通过cons这一基本的抽象——序对,通过组合的手段构造出了更复杂的抽象——序列。再度体现了本书开篇提出的程序语言的三个基本机制。
基本的表达式和语句,它们由语言提供,表示最简单的构建代码块。 组合的手段,复杂的元素由简单的元素通过它来构建。 抽象的手段,复杂的元素可以通过它来命名,作为整体来操作。
有理数
给定一个有理数,我们都有办法来提取(或选中)它的分子和分母。让我们进一步假设,构造器(constructor)和选择器(selector)以下面三个函数来提供:
make_rat(n, d)返回分子为n和分母为d的有理数。numer(x)返回有理数x的分子。denom(x)返回有理数x的分母。
xxxxxxxxxxdef add_rat(x, y): # 有理数加法,分母通分,分子分别乘以分母相加。考虑约分的话,还要写个最大公约数函数 nx, dx = numer(x), denom(x) ny, dy = numer(y), denom(y) return make_rat(nx * dy + ny * dx, dx * dy) # 返回为meke_rat函数,这是一个高阶函数写法。def mul_rat(x, y): return make_rat(numer(x) * numer(y), denom(x) * denom(y))def eq_rat(x, y): return numer(x) * denom(y) == numer(y) * denom(x)但如何构造make_rat(n, d)、numer(x)、denom(x)这三个函数呢?
前面讲到了scheme中的cons构建序对来将分子分母组合为有理数
xxxxxxxxxx(define (make-rat x y) (cons x y))(define (number bundle) (car bundle))(define (denom bundle) (cdr bundle))