函数式编程:合集
目录
虽然听起来唬人,但函数式编程出现也已经有数十年的历史了。本文从应用角度出发,记录一些基础的函数式编程思想的应用方式。
本文主要参考Gitbook《Professor Frisby’s Mostly Adequate Guide to Functional Programming》
基本概念
-
函数是一等公民:函数和内置类型变量、对象一样,可以存储到变量,数组中,可以作为参数传递。
-
纯函数:相同的函数,永远得到相同的输出,而且没有任何可以观察到的副作用。纯函数的优越性:
- 可缓存性:同样的输入,永远有相同的输出。
- 可移植性/自文档化:函数是自给自足的,不需要依赖外部,很好观测。可以很方便的通过网络通信发送给其他机器执行。
- 可测试性:只需要给定输入,就能观测输出
- 可并行化:显然没有数据竞争
副作用包括但不限于:修改文件、修改数据库、发送通讯请求、打印日志、获取输入、访问系统状态等等
-
柯里化(Curry):将一个多参数函数,分成多层使用单个或多个参数的函数,返回新的函数。形式上从
f(a,b,c)
变为f(a)(b)(c)
。 -
函数组合(Compose):将多个函数按需求结合调用,并返回新的函数。在内部完成了上完成了
f(g(h(x)))
。- 无值Pointfree模式:定义一个函数,但不使用形参的模式,就是pointfree。在封装过程中也体现为,不调用数据的操作,而是用操作调用数据。下面给出一个简单的示例。说白了就是,返回函数的包装函数不要写参数,也没有必要写参数。
const _ = require('lodash'); const compose = function(f,g) { return function(x) { return f(g(x)); } }; const myJoin = _.curry((sep,arr)=> _.join(arr,sep)); const mySplit = _.curry((sep,data)=>_.split(data,sep)); const initials = compose(myJoin('.'),mySplit(' ')) console.log(initials("rex lyc love code"));
- 无值Pointfree模式:定义一个函数,但不使用形参的模式,就是pointfree。在封装过程中也体现为,不调用数据的操作,而是用操作调用数据。下面给出一个简单的示例。说白了就是,返回函数的包装函数不要写参数,也没有必要写参数。
-
范畴学(Category Theory):数学的一个抽象分支,对集合论、类型论、群论、逻辑学等数学分支中的概念进行形式化。范畴学的讨论内容包括:对象(Object)、态射(Morphism)、变化式(Transformation)。
- 范畴:一个组件的搜集,具体应包含对象搜集(Object Collection)、态射及其组合的搜集、identity态射。
- 范畴学在程序语言中的对应:
- 对象:数据类型(数据类型可以认为是所有可能取值的一个集合)
- 态射:纯函数。一个范畴内的一个态射,表示了两个对象之间的关系。
- 态射组合:也就是前文所说的函数组合
- identity:单位元(接收输入并原样返回)
-
声明式代码:相比于命令式给出完整的执行方式,声明式是给出一个表达式的形式(比如SQL)。当你在编写函数式代码时,实际上就是在完成声明式代码。工程上,这种形式能够提供给底层实现保留更大的优化空间。
-
Map、Reduce、Filter:最常见的三种函数式编程接口。
- Map:对一个对象的元素进行遍历。并返回每一个元素的处理结果成为一个列表。
- Reduce:对一个对象的元素进行遍历。并将处理结果聚合成一个值。
- Filter:对一个对象的元素进行遍历,过滤并处理一些,返回一个子集。
-
Hindley-Milner类型签名:Hindley-Milner系统对不同类型的定义
- 函数:
a -> b
,其中a和b分别代表输入和输出,而且都可以是函数类型 - 参数化:函数签名的所包含的隐藏语义。应当是在不考虑具体参数类型(或者假定参数类型可以是任意类型)的前提下,而且不再依赖任何已知信息,所能完成的功能的最小集合。这种特性能够使得我们具备对函数签名的搜索能力,根据类型签名去搜索我们所需的函数。
原书在这里举了个例子,对函数
[a] -> [a]
,不能认为它可以具有排序功能,因为没有给定任何排序规则,而且类型a也不一定支持排序。但它可以是一个重排列函数。 - 自由定理
- 类型约束:将签名用作类型约束
- 函数:
-
函子:
- Functor函子:一种函子,是实现了map函数接口,并遵守特定规则的容器类型。是最基本的运算和功能单位。将一个容器变为另一个容器。函数式编程某种程度上就是编写各种函子并进行组合的过程。
- Pointed函子:实现了Of方法,将new封装在Of内部,并返回函子
- Maybe函子:这一类函子在任意时刻,对容器内的值考虑其为空的可能性。
- Either函子:包含Left函子、Right函子两种情况。右值是正常,左值为异常。表达条件运算,也更适合用于处理异常。
- Monad函子:在函子运算过程中,经常会出现函子嵌套的场景(尤其是用函子包装不纯操作时)。Monad函子作用于有嵌套结构的函子,将其最外层的函子剥离。一个monad函子是同时实现Of、join方法的函子。是Pointed函子的升级。此外,Monad函子也经常将join和map结合,形成接口chain,简化了配对的join/map,并支持链式调用。
- applicative函子:实现了ap方法的Pointed函子,而且这类函子所保管的值是一个函子。对applicative函子
F
,有F.of(x).map(f) == F.of(f).ap(F.of(x))
,对单个对象来说,map实际上等价于of + ap(获取一个函数,作用于当前对象,并返回一个对象)。就是说,ap方法能够将一个Functor作用于另一个Functor。替代Monad函子,尤其是在希望打破链式顺序,能够并行执行的场合。
注1:applicative函子在ap方法操作过程中还保持容器类型不变。但Monad函子不保证,他可能在chain方法调用流程中变动容器类型。
-
如何处理不纯的操作:包装不纯的部分,并延后执行,将最终的执行交给用户
-
ToDo进阶概念:自然变换(用于对函子进行类型变换)、伴随函子、极限和余极限。(11、12、13章等未学习)
范畴学
函数式编程背后由范畴学提供理论支撑,这里有必要对相关概念再进行一点展开。
Wiki百科中给出了一些基本的描述。范畴论是对于数学结构的抽象描述。
一个范畴内:对象、态射。
对范畴的抽象:将不同的范畴本身看作一个对象,态射就是表示这些范畴之间的关系。
函子(编程中的概念):将范畴学概念中的函子引入到编程中,是函数式编程最重要的一种数据类型、基本的功能和数据单位。
- 定义:一个容器,包含值,以及值之间的变形关系(函数)。根据需要,函子来负责实现不同的映射。
- 例子:
- Functor函子:实现了map函数并遵循特定规则的容器。特定规则指(只含有一个属性值、实现map函数)。map函数中的输入函数,就是单个范畴内的态射。此时的Functor函子就是实现了map语义的,一个范畴间的映射。
各类函子的实现目的,并不是给出什么必不可少的语言功能。只是在满足范畴学理论的情况下,拓展出来的各种更好用的基础设施。
函数式JS
柯里化(Curry)可以使用lodash.curry
,或者ramda
库。形如
// Node.js
const curry = require('lodash').curry;
const threeParamFunc = (a, b, c) => {
return a + b + c;
}
const curryFunc = curry(threeParamFunc);
// 以下调用都是合法的
curryFunc(1)(2)(3);
curryFunc(1,2)(3);
curryFunc(1)(2,3);
curryFunc(1,2,3);
关于函数组合,在基本概念这一节已经有详细的一个描述。理解Pointfree的写法,就可以说是理解了组合。
在书中集中给出了关于重要函数、数学结构、Pointfree工具的示例代码。
ToDo函数式C++
在C++中实现curry、compose
// compose 简单实现,模板递归
// 存在的问题:对调用链上的输入输出类型未作检验、只支持单一参数、参数拷贝性能问题
template<typename F>
auto compose(F fh) {
return [&](auto x) {return fh(x); };
}
template<typename F,typename ...Funcs>
auto compose(F fh,Funcs...ft) {
return [&](auto x) {
return fh(compose(ft...)(x));
};
}
// curry 简单实现,模板递归
template <typename Func, typename... Args>
auto curry_helper(Func func, Args... args) {
// is_invocable_v是C++17支持的,判断
if constexpr (std::is_invocable_v<Func, Args...>) {
// 如果函数、参数已经可以被调用了
return std::invoke(func, args...);
}
else {
// 返回一个能继续接收实参lambda函数
return [=](auto... moreArgs) {
// 折叠表达式
return curry_helper(func, args..., moreArgs...);
};
}
}
// curry化接口
template <typename Func>
auto curry(Func func) {
// 返回柯里化后的函数,一个可以接收任意参数的lambda表达式
return [func](auto... args) {
return curry_helper(func, args...);
};
}
ToDo 关于协变反变逆变不变
参考
Professor Frisby’s Mostly Adequate Guide to Functional Programming
上面的翻译Gitbook:函数式编程指北