读函数式编程思维笔记03_权责让渡

1. 观点

1.1. 抽象隐藏了繁杂的细节,只是有时候会连同重要的考虑因素一起隐藏掉

1.2. 理解掌握的抽象层次永远要比日常使用的抽象层次更深一层

1.3. 交出控制权的观点:放弃对繁琐细节的掌控,关注问题域,而非关注问题域的实现

2. 函数式思维的好处

2.1. 将低层次细节(如垃圾收集)的控制权移交给运行时,从而消弭了一大批注定会发生的程序错误

2.2. 函数式语言的简洁语法和灵活配合,才使递归成为简单可行的代码重用选项之一

2.3. 运行时有能力在涉及效率的问题上替我们做决定

2.4. 从频繁出现的场景中消灭掉烦人的实现细节

3. 闭包(closure)

3.1. 一种特殊的函数,在生成的时候,会把引用的变量全部圈到代码块的作用域里,封闭、包围起来(故名闭包)

3.1.1. 闭包作为一种对行为的建模手段,让我们把代码和上下文同时封装在单一结构,也就是闭包本身里面,像传统数据结构一样可以传递到其他位置,然后在恰当的时间和地点完成执行

3.2. 闭包的每个实例都保有自己的一份变量取值,包括私有变量也是如此

3.2.1. 代码块实例从它被创建的一刻起,就持有其作用域内一切事物的封闭副本

3.3. 在缺乏闭包特性的旧版Java平台上,Functional Java利用匿名内部类来模仿“真正的”闭包的某些行为,但语言的先天不足导致这种模仿是不彻底的

3.4. 当作一种异地执行的机制,用来传递待执行的变换代码

3.5. 是推迟执行原则的绝佳样板

3.6. 抓住上下文,而非状态

3.6.1. “让运行时去管理状态”

4. 柯里化(currying)和函数的部分施用(partial application)

4.1. 向一部分参数代入一个或多个默认值的办法来实现的

4.1.1. 这部分参数被称为“固定参数”

4.2. 柯里化

4.2.1. 从一个多参数函数变成一连串单参数函数的变换

4.2.2. 结果是返回链条中的下一个函数

4.3. 部分施用

4.3.1. 通过提前代入一部分参数值,使一个多参数函数得以省略部分参数,从而转化为一个参数数目较少的函数

4.3.2. 把参数的取值绑定到用户在操作中提供的具体值上,因而产生一个“元数”(参数的数目)较少的函数

4.4. Groovy

4.4.1. curry()函数实现柯里化

4.5. Clojure

4.5.1. (partial f a1 a2 …)函数

4.5.2. 没有将柯里化实现成一种语言特性,相关的场景交由部分施用去处理

4.6. Scala

4.6.1. 柯里化

4.6.2. 部分施用函数

4.6.3. 偏函数

4.6.3.1. PartialFunction trait是为了密切配合语言中的模式匹配特性

4.6.3.2. trait并不生成部分施用函数。它的真正用途是描述只对定义域中一部分取值或类型有意义的函数

4.6.3.3. Case语句是偏函数的一种用法

4.6.3.4. 偏函数的参数被限定了取值范围

4.6.3.5. 可以把偏函数用在任何类型上,包括Any

4.7. 大多数函数式语言都具备柯里化和部分施用这两种特性,但实现上各有各的做法

4.8. 用途

4.8.1. 函数工厂

4.8.1.1. 工厂方法的场合,正适合柯里化(以及部分施用)表现它的才干

4.8.2. Template Method(模板方法)模式

4.8.2.1. 在固定的算法框架内部安排一些抽象方法,为后续的具体实现保留一部分灵活性

4.8.3. 隐含参数

5. 递归

5.1. 以一种自相似的方式来重复事物的过程

5.2. 对一个不断变短的列表反复地做同一件事,把递归用在这样的场合,写出来的代码就容易理解

5.3. 递归操作往往受制平台而存在一些固有的技术限制,因此这种技法绝非万灵药

5.4. 但对于长度不大的列表来说,递归操作是安全的

5.5. 语言在管理返回值,它从递归栈里收集每次方法调用的返回结果,构造出最终的返回值

5.6. 利用递归,把状态的管理责任推给运行时

6. 记忆(memoization)

6.1. 用更多的内存(我们一般不缺内存)去换取长期来说更高的效率

6.1.1. 缓存可以提高性能,但缓存有代价:它提高了代码的非本质复杂性和维护负担

6.1.2. 负责编写缓存代码的开发者不仅要顾及代码的正确性,连它的执行环境也要考虑在内

6.1.3. 代码中的状态,开发者不仅要费心照应它,还要条分缕析它的一切明暗牵连

6.2. 记忆的内容应该是值不可变的

6.3. 保证所有被记忆的函数

6.3.1. 没有副作用

6.3.2. 不依赖任何外部信息

6.4. 只有纯(pure)函数才可以适用缓存技术

6.4.1. 纯函数是没有副作用的函数

6.4.1.1. 它不引用其他值可变的类字段

6.4.1.2. 除返回值之外不设置其他的变量

6.4.1.3. 其结果完全由输入参数决定

6.4.2. 只有在函数对同样一组参数总是返回相同结果的前提下,我们才可以放心地使用缓存起来的结果

6.5. 缓存是很常见的一种需求,同时也是制造隐晦错误的源头

6.6. 两种情况

6.6.1. 类内部缓存

6.6.1.1. 类中的缓存就代表类有了状态,所有与缓存打交道的方法都不可以是静态的,于是产生了更多的连锁效应

6.6.2. 外部调用

6.7. 两种实现方式

6.7.1. 手工进行状态管理

6.7.2. 采用记忆机制

6.8. 在命令式的思路下,开发者是代码的主人(以及一切责任的承担者)

6.9. 我们写出来的缓存绝不可能比语言设计者产生的更高效,因为语言设计者可以无视他们给语言定的规矩:开发者无法触碰的底层设施,不过是语言设计者手中的玩物,他们拥有的优化手段和空间是“凡人”无法企及的

6.9.1. 上帝视角

6.10. Groovy

6.10.1. 先将要记忆的函数定义成闭包,然后对该闭包执行memoize()方法来获得一个新函数,以后我们调用这个新函数的时候,其结果就会被缓存起来

6.10.2. memoizeAtMost(1000)

6.11. Clojure

6.11.1. (memoize )

6.12. Scala

6.12.1. 没有直接提供记忆机制,但它为集合提供的getOrElseUpdate()方法已经替我们承担了大部分的实现工作

6.13. Java 8

6.13.1. 没有直接提供记忆特性,但只要借助它新增的lambda特性,就可以轻松地实现记忆功能

7. 缓求值(lazy evaluation)

7.1. 尽可能地推迟求解表达式

7.1.1. 昂贵的运算只有到了绝对必要的时候才执行

7.1.2. 可以建立无限大的集合,只要一直接到请求,就一直送出元素

7.1.3. 按缓求值的方式来使用映射、筛选等函数式概念,可以产生更高效的代码

7.1.4. 减少占用的存储空间。假如能够用推导的方法得到后续的值,那就不必预先存储完整的列表了——这是牺牲速度来换取存储空间的做法

7.2. 非严格求值(non-strict)的(也叫缓求值,lazy)

7.2.1. 常用的非严格求值语言有Haskell

7.3. Totally Lazy框架(Java)

7.4. Groovy

7.4.1. 缓求值列表是函数式语言普遍具备的特性

7.4.1.1. LazyList

7.4.2. 暂缓初始化昂贵的资源,除非到了绝对必要的时候

7.4.3. 可以用来构建无限序列,也就是没有上边界的列表

7.4.4. 缓求值列表特别适用于资源的生产成本较高的情况

7.5. Clojure

7.5.1. 数据结构都是默认缓求值的

7.6. Scala

7.6.1. 没有把一切都默认安排成缓求值的,而是在集合之上另外提供了一层缓求值的视图

7.7. 缓求值的字段初始化

7.7.1. Scala

7.7.1.1. val声明前面加上“lazy”字样

7.7.1.1.1. 令字段从严格求值变成按需要求值

7.7.2. Groovy

7.7.2.1. 抽象语法树(Abstract Syntax Tree,AST)

7.7.2.1.1. @Lazy标注

8. 元函数技法

8.1. 操纵的对象是函数本身,而非函数的结果

8.2. 柯里化

posted @ 2023-01-22 13:16  躺柒  阅读(52)  评论(0编辑  收藏  举报