zoukankan      html  css  js  c++  java
  • “完全”函数式编程

    引子

    有了面向对象编程,还需要函数式编程吗 ? 函数式编程,有什么妙处 ?

    函数式的理念主要是:

    • 函数组合。函数式编程是将程序看成是一系列函数的组合。可以将函数作为变量进行赋值,作为函数参数传入,也可以作为返回值返回,函数无处不在。
    • 不可变。函数式编程不会改变传入的对象,返回的也是新创建的对象。
    • 确定性。相同的输入,经过函数式处理后,必然得到相同的输出。

    这些理念导致的结果是:函数式编程,使得程序行为更容易预测和推断,尤其是在并发和并行的情形下。

    本文将带着“函数式编程的有色眼镜”,重新审视日常的编程结构。重点是以函数视角来看待问题,暂不论及性能、软件工程等因素。

    示例代码采用 Java 编写,其目的是贴近 Java 开发者的阅读习惯。同时,也应注意,由于函数式并不是 Java 的主要特性,因此在 Java 中是受限的能力。

    基本结构

    赋值

    赋值是程序中最基础的行为。从函数视角来看,赋值实际上是一个常量函数。如下代码所示。 int i = 100 ,实际上可以用 Supplier<Integer> f = () -> 100 来替代。任何值出现的地方,都可以用值提供器来替代。这样做,可以增强函数的灵活性:因为值是固定的,但值源可以多种多样:变量、函数、文件、网络等。

    public class Assignment {
    
      public static void main(String[]args) {
        int i = 100;
        System.out.println(i);
    
        Supplier<Integer> f = () -> 100;
        System.out.println(f.get());
      }
    }
    

    条件

    来看条件怎么用函数式编程来实现。如下代码所示,是一个极为普通的函数,分别根据三种不同的条件返回不同的值。

    public static Integer plainIfElse(int x) {
        if (x > 0) {
          return 1;
        }
        else if (x < 0) {
          return -1;
        }
        else {
          return 0;
        }
      }
    

    怎么用函数式编程来改造呢 ? 前面说了,函数式编程是将程序看成是一系列函数的组合。首先从plainIfElse 提取出六个基本函数:

    public static boolean ifPositive(int x) {
        return x > 0;
      }
    
      public static boolean ifNegative(int x) {
        return x < 0;
      }
    
      public static boolean ifZero(int x) {
        return x == 0;
      }
    
      public static Integer positiveUnit() {
        return 1;
      }
    
      public static Integer negativeUnit() {
        return -1;
      }
    
      public static Integer zero() {
        return 0;
      }
    

    现在的问题是:怎么组合这些基本函数得到与 plainIfElse 一样的效果呢?很容易,将 if-elseif-else 解析为:

    if (A) { actA }
    if (B) { actB }
    if (C) { actC }
    

    这是一个 Map 结构。key 是条件函数,value 是行为函数。因此可以考虑用 Map[Predicate,Supplier] 来模拟,如下代码所示:

    public static Supplier<Integer> mapFunc(int x) {
        Map<Predicate<Integer>, Supplier<Integer>> condMap = new HashMap<>();
        condMap.put(Condition::ifPositive, Condition::positiveUnit);
        condMap.put(Condition::ifNegative, Condition::negativeUnit);
        condMap.put(Condition::ifZero, Condition::zero);
        return travelWithGeneric(condMap, x);
      }
    

    接下来,只要遍历所有的 key ,找到满足条件函数的第一个 key ,然后调用 value 即可:

    public static <T,R> Supplier<R> travelWithGeneric(Map<Predicate<T>, Supplier<R>> map, T x) {
        return map.entrySet().stream().filter((k) -> k.getKey().test(x)).findFirst().map((k) -> k.getValue()).get();
      }
    

    Emmm ... Seems Perfect .


    不过,完全消除了 if-else 了吗?并没有。 事实上 filter + findFirst 隐式地含有了 if-else 的味道。这说明:无法彻底地消除条件,只是在适当的抽象层次上隐藏了。

    进一步思考,if-else 可以拆成两个 if-then 语句。 if-then 可以说是最原子的操作了。顺序语句,本质上也是 if-then : if exec current ok , then next ; if exec not ok, exit .

    通用IF

    既然 if-then 是原子操作,可以提供几个方便的函数:

    public class CommonIF {
    
      public static <T, R> R ifElse(Predicate<T> cond, T t, Function<T, R> ifFunc, Supplier<R> defaultFunc ) {
        return cond.test(t) ? ifFunc.apply(t) : defaultFunc.get();
      }
    
      public static <T, R> R ifElse(Predicate<T> cond, T t, Supplier<R> ifSupplier, Supplier<R> defaultSupplier ) {
        return cond.test(t) ? ifSupplier.get() : defaultSupplier.get();
      }
    
      public static <T, R> Supplier<R> ifElseReturnSupplier(Predicate<T> cond, T t, Supplier<R> ifSupplier, Supplier<R> defaultSupplier ) {
        return cond.test(t) ? ifSupplier : defaultSupplier;
      }
    
      public static <T> void ifThen(Predicate<T> cond, T t, Consumer<T> action) {
        if (cond.test(t)) {
          action.accept(t);
        }
      }
    
      public static <T> boolean alwaysTrue(T t) {
        return true;
      }
    }
    

    应用 CommonIF 的函数,可以改写 if-elseif-else 为嵌套的 if-else :

    public static Supplier<Integer> ifElseWithFunctional(int x) {
        return CommonIF.ifElseReturnSupplier(Condition::ifPositive, x,
                                             Condition::positiveUnit,
                                             CommonIF.ifElseReturnSupplier(Condition::ifNegative, x, Condition::negativeUnit, Condition::zero ) );
      }
    

    循环

    现在,来看下循环。下面是一段很普通的循环代码:

        Integer sum = 0;
        List<Integer> list = Arrays.asList(1,2,3,4,5);
        for (int i=0; i < list.size(); i++) {
          sum += list.get(i);
        }
        System.out.println(sum);
    
        Integer multiply = 1;
        for (int i=0; i < list.size(); i++) {
          multiply *= list.get(i);
        }
        System.out.println(multiply);
    

    聪明的读者很快就看出了:上述两段代码的结构都是对容器的元素进行 reduce ,差异只在于:一个初始值和 reduce 操作符。怎么将 reduce 这种相似性抽离出来呢 ? 如下代码所示。 reduct 的操作是:1. 将结果 result 赋值为初始值 init ; 2. 每次从 list 取出一个元素 e , 与 result 进行指定运算,然后存入 result; 3. 列表遍历结束后,返回 result .

    NOTE:list 中的元素,既可以是数据,也可以是函数。 是函数的情况,将在“循环”这一节的函数 pipe 展示出来。泛型结合函数式,可以使得一个简单函数的能力变得非常强大。

    public static <E,T> T reduce(List<E> list, BiFunction<E,T,T> biFunc, Supplier<T> init) {
        T result = init.get();
        for (E e: list) {
          result = biFunc.apply(e, result);
        }
        return result;
      }
    

    现在可以写作:

    System.out.println("func sum:" + reduce(list, (x,y) -> x+y, () -> 0));
    System.out.println("func multiply: " + reduce(list, (x,y) -> x*y, () -> 1));
    

    是不是足够简洁 ?我们发现了函数式编程的一大妙处。

    实际应用

    在运用函数视角解析基本编程结构之后,来看一点实际应用。

    PipeLine

    PipeLine 是函数式编程的典型应用。PipeLine 通俗地说,就是一个流水线,通过一系列工序共同协作完成一个确定目标。比如说,导出功能,就是“查询-详情-过滤-排序-格式化-生成文件-上传文件”的流水线。如下代码,展示了如何用函数式实现一个 PipeLine : supplier 提供的数据集,经过一系列的 filters 加工,最后经过 format 格式化输出。

    public class PipeLine {
    
      public static void main(String[] args) {
        List<String> result = pipe(PipeLine::supplier, Arrays.asList(PipeLine::sorter, PipeLine::uniq), PipeLine::format);
        System.out.println(result);
      }
    
      public static <T,R> R pipe(Supplier<List<T>> supplier, List<Function<List<T>, List<T>>> filters,
                                       Function<List<T>,R> format) {
        return format.apply(Loop.reduce(filters, (f, mid) -> f.apply(mid), supplier));
      }
    
    
      public static List<String> supplier() {
        return Arrays.asList("E20191219221321025200001", "E20181219165942035900001", "E20181219165942035900001", "E20191119165942035900001");
      }
    
      public static List<String> sorter(List<String> list) {
        Collections.sort(list);
        return list;
      }
    
      public static List<String> uniq(List<String> list) {
        return list.stream().distinct().collect(Collectors.toList());
      }
    
      public static List<String> format(List<String> list) {
        return list.stream().map(
            (s) -> s + " " + s.substring(1,5) + " " + s.substring(5,7) + ":" + s.substring(7,9) + ":" + s.substring(9,11)
        ).collect(Collectors.toList());
      }
    }
    

    装饰器

    最后,来看一个装饰器栗子,展示函数组合的强大威力。

    大家还隐约记得一个公式: (sinx)^2 + (cosx)^2 = 1。如果要写成程序,也是很容易的:

    double x = Math.pow(sin(x),2) + Math.pow(cos(x), 2); 
    

    如果我需要的是 f(x)^2 + g(x)^2 呢?细心的读者发现了,这里的结构都是 f(x)^n ,因此将这个结构抽离出来。 pow 对 f 做了个幂次封装,现在我们得到了 F(x) = f(x)^n + g(x)^n 的能力。

      /** 将指定函数的值封装幂次函数 pow(f, n) = (f(x))^n */
      public static <T> Function<T, Double> pow(final Function<T,Double> func, final int n) {
        return x -> Math.pow(func.apply(x), (double)n);
      }
    

    现在可以写作:double x = pow(Math::sin, 2).apply(x) + pow(Math::cos, 2).apply(x);

    请注意,这里 + 仍然是固定的,我希望也不局限于加号,而是任意可能的操作符,也就是想构造: H(x) = Hop(f(x), g(x))。这样,就需要支持将 + 这个操作符,以函数参数的形式传入:

    public static <T> Function<BiFunction<T,T,T>, Function<T,T>> op(Function<T,T> funcx, Function<T,T> funcy) {
        return opFunc -> aT -> opFunc.apply(funcx.apply(aT), funcy.apply(aT));
    }
    

    现在可以写作:

     Function<Double,Double> sumSquare = op(pow(Math::sin, 2), pow(Math::cos, 2)).apply((a,b)->a+b);
    System.out.println(sumSquare.apply(x));
    

    对于 f(x)^n ,事实上,可以写成更抽象的形式: f(g(x)) = y -> f(y), y = x -> g(x) :

    /** 将两个函数组合成一个叠加函数, compose(f,g) = f(g) */
      public static <T> Function<T, T> compose(Function<T,T> funcx, Function<T,T> funcy) {
        return x -> funcx.apply(funcy.apply(x));
      }
    
      /** 将若干个函数组合成一个叠加函数, compose(f1,f2,...fn) = f1(f2(...(fn))) */
      public static <T> Function<T, T> compose(Function<T,T>... extraFuncs) {
        if (extraFuncs == null || extraFuncs.length == 0) {
          return x->x;
        }
        return x -> Arrays.stream(extraFuncs).reduce(y->y,  FunctionImplementingDecrator::compose).apply(x);
      }
    

    现在,我们获得了更强的灵活性,可以任意构造想要的函数:

    Function<Double,Double> another = op(compose((d)->d*d, Math::sin), compose((d)->d*d, Math::cos)).apply((a,b)->a+b);
    System.out.println(another.apply(x));
    
    Function<Double,Double> third = compose(d->d*d, d->d+1, d->d*2, d->d*d*d); // (2x^3+1)^2
    System.out.println(third.apply(3d));
    

    这里展示了函数式编程的强大之处:通过短小的简单函数,很容易组合出具有强大功能的复合函数。

    小结

    函数式编程通过任意组合短小简单的函数,构造具有强大能力的复合函数,同时可以保持代码非常简洁。

    通过函数式编程训练,将会习惯于识别出程序中的基本结构,善于提炼、抽象和组合基本结构来构造更复杂的结构,逐步收获强大的结构提炼和抽象能力。而结构化抽象,正是软件开发与设计的第一性原理,也是最基本最强大的内功。

    读完本文,你是否从中受到了启发呢 ?

  • 相关阅读:
    Linux IO接口 监控 (iostat)
    linux 防火墙 命令
    _CommandPtr 添加参数 0xC0000005: Access violation writing location 0xcccccccc 错误
    Visual Studio自动关闭
    Linux vsftpd 安装 配置
    linux 挂载外部存储设备 (mount)
    myeclipse 9.0 激活 for win7 redhat mac 亲测
    英文操作系统 Myeclipse Console 乱码问题
    Linux 基本操作命令
    linux 查看系统相关 命令
  • 原文地址:https://www.cnblogs.com/lovesqcc/p/12075147.html
Copyright © 2011-2022 走看看