1 现实世界中的函数
在现实世界里,函数主要是数学上的概念。它是被称为函数定义域(function domain)的源集(source set)和被称为函数值域(function codomain)的目标集(target set)之间的关系。定义域和值域无须完全不同。例如,一个函数的定义域和值域可以有相同的整数集,也可以不相同。
y = 2x; 定义域是全体实数,但是值域只是全体实数的子集(偶数)
1.1 如何让两个集之间的关系成为函数
函数或者说就是描述两个集合(定义域和值域)的关系:
这个关系需要满足一个条件:定义域内的所有元素都必须在值域内有且仅有一个对应元素
- 定义域里的一个元素在值域里不会有两个对应的元素。
- 值域里的元素可能在定义域里有多个相对应的元素(换件话说,定义域里的多个元素可以映射为值域里的同一个元素,比如 y = x^2 这个函数,2和-2都会映射成值域里4这个元素)
1.2 复合函数
函数就像积木,可以复合为其他函数。比如:
f(x) = x + 2;
g(x) = x * 2;
f(g(x)) = f(x *2) = (x * 2) + 2
f(g(5)) = f(5 *2) = 10 +2 = 12
复制代码
1.3 多参函数
如果函数有多个参数会如何?简单来说,并没有多参函数这回事。还记得函数的定义吗?一个函数是源集和目标集之间的关系。它并不是多个源集与一个目标集之间的关系。一个函数不允许有多个参数。
但是要学会抽象的想问题,比如
f(x,y) = x + y
复制代码
这个源集是不是就可以可以抽象理解集合里的每个元素是一对数,这似乎就变成了是一个N x N与N之间的关系,在这种情况下,它是一个函数。但是它只有一个参数,即N x N的元素。
在更发散一下:这个集的元素就是一对整数,更通用的元组(tuple)表示多个元素的组合,这就是所谓的多参函数
1.4 函数柯里化
上面那种元组函数,换一种思考方式:
f(x,y) = x + y;
f(3,5) = 3 + 5 = 8
复制代码
可以认为函数f(3,5)是一个从N到N的函数集的函数:
f(x,y) = g(y), 其中 g(y) = x + y
复制代码
在这种情况下,可以这样写:
f(x) = g
复制代码
它的意思是将函数f应用于参数x,结果是一个新的函数g。将函数g应用于y将会得到:
1. 函数f应用于参数x,结果是一个新的函数g
f(x) = g
2. 将函数g应用于y将会得到
g(y) = x + y
复制代码
当应用g的时候,x就不再是一个参数了。它并不依赖于参数或者其他什么东西。它就是一个常量
如果将其应用于(3,5),你就会得到:
f(3)(5) = g(5) = 3 + 5 = 8
复制代码
这里唯一的新知识就是f的陪域现在不是数字集了,而是一个函数集。将f应用于一个整数的结果是一个函数。将这个新函数应用于一个整数的结果是一个整数。
函数f(x))(y)是f(x,y)的柯里化形式。对一个元组函数(如果你喜欢可以称为多参函数)应用这种转换就称为柯里化(currying),源于数学家Haskell Curry(虽然他并非是这种转换的发明者)。
上面这个加法函数的柯里化,好像看起来没啥用,可以从两个参数中的任意一个开始,并没有什么不同。虽然中间函数会不一样,但是最终结果总会是一样的。
下面看一个新的函数
f(rate,price) = price / 100 * (100 + rate)
复制代码
这个函数看起来跟以下函数等价:
g(price,rate) = price / 100 * (100 + rate)
复制代码
这两个函数分别柯里化:
f(rate,price) = f(rate)(price)
其中f(rate)得到就是函数g(price) = price / x
g(price,rate) = g(price)(rate)
其中g(price)得到就是函数f(rate) = x /100 * (100 + rate)
复制代码
f(rate)是接收一个价格并返回一个价格的函数。如果rate=9,这个函数对一个价格应用9%的税,得到一个新价格。
g(price)是一个接收税率并返回一个价格的函数。如果价格是100美元,它的新函数就是对100应用一个可变税率。
貌似看来g(price)是一个没有太大意义的函数,实际环境中很少有这种问题,如果还不是很理解,想象你正在外国旅行,用一个手持计算器(或者你的智能手机)来转换货币。当要计算价格的时候,你希望每次都需要输入汇率,还是把汇率存储在内存里?哪种办法更不容易出错?显然是汇率存储在内存里,输入价格,更不容易出错
2 java中的函数
java中的方法是一种在传统的Java里在某种程度上表示函数的方式。
2.1 函数式方法
一个方法可以是函数式的,只要它满足纯函数的要求:
- 它不能修改函数外的任何东西。外部观测不到内部的任何变化。
- 它不能修改自己的参数。
- 它不能抛出错误或异常。
- 它必须返回一个值。
- 只要调用它的参数相同,结果也必须相同。
不理解这几个特点,可参考:什么是函数式编程
2.2 对象标记与函数标记
- 实例方法访问类属性可以视为一个外围类实例的隐式参数。
- 可以把不访问外围类实例的方法安全地标记为静态方法。
- 访问外围类实例的那些方法也可以被标记为静态方法,只需显式地标记它们的隐式参数(外围类实例)。
回顾一下什么是函数式编程中的Payment类的combine方法
public class Payment {
public final CreditCard creditCard;
public final Double amount;
public Payment(CreditCard creditCard, Double amount) {
this.creditCard = creditCard;
this.amount = amount;
}
public void pay() {
this.creditCard.charge(amount);
}
public Payment combine(Payment payment) {
if (payment.creditCard.equals(this.creditCard)) {
// 如果是一个信用卡账号,那就可以将两次的支付金额合并起来
// 返回一个新的Payment
return new Payment(this.creditCard, payment.amount + this.amount);
}
// 如果不是同一个支付账号,那么就不能合并抛出异常
throw new IllegalStateException("current creditCard not match");
}
}
复制代码
combine方法访问了外围类payment(形参传进来的)以及本身中的amount。结果就是它不能成为静态方法。
这个方法将本身中的amount视为一个隐式参数,可以将其变为显式参数,这样便可以使这个方法成为静态方法:
public static Payment combine(Payment p1 ,Payment p2) {
if (p1.creditCard.equals(p2.creditCard)) {
// 如果是一个信用卡账号,那就可以将两次的支付金额合并起来
// 返回一个新的Payment
return new Payment(p1.creditCard, p2.amount + p2.amount);
}
// 如果不是同一个支付账号,那么就不能合并抛出异常
throw new IllegalStateException("current creditCard not match");
}
复制代码
就可以在内部这么调用了,静态方法可以从类的内部被调用,只需传入this引用即可
public Payment combine(Payment payment) {
return combine(this,payment);
}
复制代码
如果从类的外部调用方法,你需要使用类名:
Payment.combine(p1,p2)
复制代码
此时合并多个结果:
public void testBuyBread4(){
BreadShop breadShop = new BreadShop();
CreditCard creditCard = new CreditCard();
// 第一次购买
Payment payment1 = breadShop.buyBread2(creditCard).payment;
// 第二次购买
Payment payment2 = breadShop.buyBread2(creditCard).payment;
Payment payment3 = breadShop.buyBread2(creditCard).payment;
// 合并,使用实例方法
payment1.combine(payment2).combine(payment3);
// 或者使用静态方法combine(这个显然不易读)
Payment.combine(Payment.combine(payment1,payment2),payment3)
}
复制代码
2.3 方法与函数以及复合函数
方法可以是函数式的,但是在函数式编程中,它缺少了一些可以用来表示函数的东西:除了应用于参数以外,它无法被控制。你无法将一个方法作为参数传递给另一个方法。结果就是你无法只是复合方法而不应用它们。你可以复合方法的应用,但无法复合方法本身。
比如:
定义这么一个接口,来理解一下上面说的
public interface Function {
int apply(int a);
}
复制代码
@Test
public void test1(){
Function dou = new Function() {
@Override
public int apply(int a) {
return a * 2;
}
};
Function square= new Function() {
@Override
public int apply(int a) {
return a * a;
}
};
// 可以分开调用
dou.apply(3);
square.apply(3);
// 你无法将一个方法作为参数传递给另一个方法, 比如就无法复合这两个函数
// 但是如果把函数当做方法那你就可以了,但这不是复合函数
dou.apply(square.apply(3));
}
复制代码
复合函数是函数的二元操作,正如加法是数字的二元操作。因此你能够以编码的方式用一个方法来复合函数:
如何使用编码的方式用一个方法来复合函数呢:Function提供一个compose方法
public interface Function {
int apply(int a);
static Function compose(Function function1, Function function2) {
return new Function() {
@Override
public int apply(int a) {
return function1.apply(function2.apply(a));
}
};
}
}
复制代码
此时就可以这么实现dou.apply(square.apply(3));
:
@Test
public void test2(){
Function dou = new Function() {
@Override
public int apply(int a) {
return a * 2;
}
};
Function square= new Function() {
@Override
public int apply(int a) {
return a * a;
}
};
int apply = Function.compose(dou, square).apply(3);
System.out.println(apply);
}
复制代码
为了让Function接口更通用,可以使用泛型:
public interface Function<R, I> {
R apply(I a);
static <U, T> Function<U, T> compose(Function<U, T> function1, Function<T, T> function2) {
return new Function<U, T>() {
@Override
public U apply(T a) {
return function1.apply(function2.apply(a));
}
};
}
}
复制代码
2.4 复合函数的问题
复合函数是一个非常强大的概念,但是如果用Java来实现,会有一个大隐患。复合几个函数没什么问题。但是思考一下,构建10 000个函数并把它们复合成一个。
在命令式编程里,每个函数都在计算之后才把结果传递给下一个函数当作输入。但是在函数式编程里,复合函数意味着无须计算便直接构建结果函数。复合函数非常强大,因为函数无须计算就可以被复合。但是结果就是,在大量内嵌的方法调用中应用复合函数最终会导致栈溢出。
@Test
public void test3() {
int num = 100000;
Function<Integer, Integer> g = x -> x;
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> compose = null;
for (int i = 0; i < num; i++) {
g = Function.compose(f, g);
}
System.out.println(g.apply(0));
}
复制代码