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));
    

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

    小结

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

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

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

  • 相关阅读:
    你的想像力智商有多高?
    Visual FoxPro 9.0 发布
    Google的社会网络
    女人永远是对的
    如何保存ICQ聊天历史
    7 30 个人赛
    Linux下利用文件描述符恢复的成功失败实验
    蓝鲸社区版部署
    Oracle 10.2.0.5升级至11.2.0.4
    手动创建Oracle实例
  • 原文地址:https://www.cnblogs.com/lovesqcc/p/12075147.html
Copyright © 2011-2022 走看看