java中的函数

1 现实世界中的函数

在现实世界里,函数主要是数学上的概念。它是被称为函数定义域(function domain)的源集(source set)和被称为函数值域(function codomain)的目标集(target set)之间的关系。定义域和值域无须完全不同。例如,一个函数的定义域和值域可以有相同的整数集,也可以不相同。

y = 2x; 定义域是全体实数,但是值域只是全体实数的子集(偶数)

1.1 如何让两个集之间的关系成为函数

函数或者说就是描述两个集合(定义域和值域)的关系:

这个关系需要满足一个条件:定义域内的所有元素都必须在值域内有且仅有一个对应元素

  1. 定义域里的一个元素在值域里不会有两个对应的元素。
  2. 值域里的元素可能在定义域里有多个相对应的元素(换件话说,定义域里的多个元素可以映射为值域里的同一个元素,比如 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 函数式方法

一个方法可以是函数式的,只要它满足纯函数的要求:

  1. 它不能修改函数外的任何东西。外部观测不到内部的任何变化。
  2. 它不能修改自己的参数。
  3. 它不能抛出错误或异常。
  4. 它必须返回一个值。
  5. 只要调用它的参数相同,结果也必须相同。

不理解这几个特点,可参考:什么是函数式编程

2.2 对象标记与函数标记

  1. 实例方法访问类属性可以视为一个外围类实例的隐式参数。
  2. 可以把不访问外围类实例的方法安全地标记为静态方法。
  3. 访问外围类实例的那些方法也可以被标记为静态方法,只需显式地标记它们的隐式参数(外围类实例)。

回顾一下什么是函数式编程中的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));
}
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享