函数式编程

函数式编程

1. 函数

高中一年级,应该是最早接触函数这个概念的时间,印象很深刻,毕竟是高考压轴大题,但它却是必修一第二章的内容。

我们来看一个必修一中最简单的一个函数: $$ y=f(x) $$ 上面的函数由三个部分组成:

  • x 自变量,由它决定初始值
  • f 它代表一个规则,用来对 自变量 进行计算。它也是用来描述函数关系的。
  • y 因变量,它用来标识通过函数计算以后的结果

一个函数的定义是:一个自变量 x ,经过一个规则 f(x) 的运算,得到一个因变量。其中的规则 f(x) 称为函数

上面的函数在数学上有一个术语叫一元函数 $$ y=f(x1,x2) $$ 如上,它被称为二元函数,自变量的多少决定了这个函数的名称。

函数的概念,也被引入了计算机的领域中。很多的语言内置了函数的语法,帮助我们去实现类似于数学中函数的功能。

我们来看一个函数的定义

int main(int x) {
 return x + 1; 
}

这是 C 语言中定义一个函数的语法规则,可以看出它和数学中的函数完全一样,它传入了一个自变量 x 在计算机中,它叫参数,同时在函数内部,它对这个参数做了加法运算,并将其返回。此时这个加法运算就是数学中的规则 f(x) ,返回值就是因变量 y

类似,上面的函数叫一元函数,因为他只有一个参数。

同理,两个参数的,就称为二元函数,以此类推。

各种编程语言,提供了多种多样的函数的定义方式,但其本质和上面的函数完全一样,只是定义方式发生了变化而已。

  • Java
public static int method(int x) {
  return x + 1;
}
  • scala
def method(x: Int): Int = {
  x + 1
}
  • javascript
function method(x) {
  return x + 1
}

2. 面向对象编程和函数式编程

写 OOP 的人都有一个体会,以类作为最小的调度单元,实现一个功能,需要去定义一些数据结构和操作这些数据结构的方法

也基于此,衍生出了设计模式这个代码复用的规则。设计模式的出现就是为了解决 OOP 带来的一些弊端,一定程度上实现对方法级别的重用。

函数式编程(后文以 FP 代替)讲究不变性,如同一个数学函数一样,只要你的入参相同,那么你的返回值必然相同,这样做的好处在于,这个函数对你的代码没有任何副作用,他不会更改所有的变量,只会返回一个新的变量,这也意味着它没有线程安全的问题。

了解过一些支持 FP 的同学,一定在相关的书籍上看到过一句话:函数是一等公民,支持 FP 的语言,将函数作为一种数据类型而存在。

而当函数成为一种数据类型的时候,很多我们经常使用到的设计模式也就有了其他的一些玩法。

3. 函数式编程下的设计模式

策略

策略设计模式,用来解决参数相同场景下的 if|else 的问题,直接看类图

image-20220410130605412

  • Strategy(策略),定义所有支持的算法的公共接口。
  • ConcreteStrategy(具体策略,如 SimpleCompository , TeXCompositor)
  • Context (上下文,用来对具体的策略进行切换)

上面是典型的 OOP 的思路,我们来看一下 FP 下的代码实现

function test01(func) {
  func("HelloWorld")
}

function test02(str) {
  log.info(str)
}

test01(test02())
模版方法

模版方法,预留一些扩展(方法)留给子类自己实现,如生命周期函数。来看类图

image-20220412201310007

FP函数式代码实现:

function test01(doCreateDocument, aboutToOpenDocument) {
  log.info("start")
  doCreateDoCument(str)
  aboutToOpenDocument(str)
}

test01(data => {
  
}, error => {
  
})

可以发现无论是策略还是模版设计模式,都在使用函数作为数据类型,进而代替了 OOP 中的继承的作用。但对于一个函数而言,参数的个数问题成为了一个问题,实现一个功能我们可以依赖于外部的多个参数,此时一味的进行传参,对于后续代码维护、扩展都有很大的影响,于此函数式编程的一个特性也随之诞生。

4. 柯里化

闭包

闭包的概念来自于前端,通俗的话来讲,闭包就是引用了一个函数内部所有变量(包括参数)的一个组合。

闭包在函数创建的时候就会被默认创建,如同类的构造函数。

一个问题:没有返回值的函数存在闭包吗?

答:存在,没有使用而已

上面提到了一个多参数的问题,我们来看一段代码

function func1(str1) {
  return func2(str2) {
    return str1 === str2
  }
}

const func3 = func1("1")
const ans = func3("2")

可以看到,func1 返回了一个函数 func2 ,func2 函数访问了 func1 函数的参数。这个的实现就依赖于 闭包

func3 获取到了 func2 的一个引用。此时继续调用 func3 ,得到 func1 最终的处理结果。

上面将函数调用分为了两部,如果连在一起写就是 func1("1")("2")

Func1(1)(2) 这样写的好处在哪里?

  • 将一个多元函数拆分为多个低元函数,参数之间可以进行预处理,然后进行整合;

  • 一元函数方便复用

这种变形调用方式,在函数式编程中存在一个术语柯里化

5. Java 中的函数式

Java 中存在

对于支持函数数据类型的编程语言,实现一些基本的函数式编程是很容易的。而对于 Java 这种不支持函数的语言来说,需要通过一些特殊的方式进行实现。jdk1.8 提供了 lambda 表达式的能力,同时也提供了函数式接口的声明注解,在一定程度上可以实现一些简单的函数式能力。

函数作为参数

Java 1.8 以后提供了一个注解 @FunctionalInterface 。它的定义是:内部仅仅存在一个抽象方法的接口,即可声明为函数式接口。同时也提供了一些通用的类,来实现函数式编程。

图中最上面是四个基本的函数式接口,Consumer, Predicate, Supplier 三个类都是对 Function 类的一次封装,Consumer 类没有返回值,Predicate 返回值为 布尔值Supplier 类没有参数。而 Function 类是一个标准的函数式接口,看一下它的定义。

@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);
  
  	default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }
  	default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }
  
}

可以看到,他内部提供了一个抽象函数, R apply(T t) 接受一个泛型,返回一个泛型。这是最标准的一元函数(只有一个参数)。

那么问题来了,如果我需要两个参数怎么办,JDK,提供了一个 BiFunction 可以接受两个参数,当然只能返回一个值,如果想返回多个值,请封装一个 集合类型。

那么问题又来了,如果我需要三个,或者更多的参数怎么办,不可能 JDK 将所有的参数个数的函数都封装一遍吧。

我们来思考一下这个问题的解决方法:对于多元(多参)的函数,我们能否将它们拆成一个个的一元函数,然后让这个一元函数返回一个一元函数,来实现多参数的传递。这就是我们前面提到的柯里化的思想。

来看一下具体的实现

public void test() {
        System.out.println(test01(param01 -> param02 -> param03 -> param01 + param02 + param03));
    }

    public String test01(Function<String, Function<String, Function<String, String>>> function) {
        // condition

        String method01 = "111";
        String method02 = "222";
        String method03 = "333";

        return function.apply(method01).apply(method02).apply(method03);
    }

这个能力,结局的问题是:一个函数需要多个内部参数运行的时候,就可以使用这种方式解决。在之前编码的时候,碰到过类似的场景,有两个函数的执行的方法相同,只是根据类型去执行了不同的操作,于是后来我将它封装为了这种函数去做。

我们再来看另外一种场景:

开篇提了一个连续校验的问题,解决方法是通过传递函数的方式实现,那么 Java 可以这样实现?

前面看 Function 接口的时候看到他存在两个默认方法

default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }
  	default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

看这两个默认函数,做了什么事情

  • compose 他的执行过程是,先执行传入的 before 函数,然后将这个函数的返回值,传给当前的 function 函数进行执行。
  • andThen 函数和 compose 刚好反了过来。

所以对于传递函数的能力,我们可以这样实现:

public String test02(String data, Function<String, String> function01, Function<String, String> function02, Function<String, String> function03) {
        return function01.andThen(function02).andThen(function03).apply(data);
}

它适用的场景就是对一个参数进行多处理。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值