“完全”函数式编程

引子

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

函数式的理念主要是:

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

这些理念导致的结果是:函数式编程,更容易达到程序行为可预测和推断的目标。

本文将带着“函数式编程的有色眼镜”,重新审视日常的编程结构。重点是以一种新的视角来看待问题,暂不论及性能、软件工程等因素。示例代码采用 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 这种相似性抽离出来呢 ? 下面的代码展示了:

public static <T> T reduce(List<T> list, BinaryOperator<T> bifunc, Supplier<T> init) {
    return list.stream().reduce(init.get(), bifunc);
}

现在可以写作:

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) {
    List<T> result = supplier.get();
    for (Function<List<T>, List<T>> filter: filters) {
      result = filter.apply(result);
    }
    return format.apply(result);
  }

  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(6,8) + ":" + s.substring(9,11) + ":" + s.substring(12,14)
    ).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));

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

小结

函数式编程通过任意组合短小简单的函数,构造具有强大能力的复合函数,同时可以保持代码非常简洁。通过函数式编程训练,可以逐步收获更强大的结构抽象和提炼能力。

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

原文地址:https://www.cnblogs.com/lovesqcc/p/12075147.html

时间: 2024-08-29 06:17:16

“完全”函数式编程的相关文章

Python学习笔记八:文件操作(续),文件编码与解码,函数,递归,函数式编程介绍,高阶函数

文件操作(续) 获得文件句柄位置,f.tell(),从0开始,按字符数计数 f.read(5),读取5个字符 返回文件句柄到某位置,f.seek(0) 文件在编辑过程中改变编码,f.detech() 获取文件编码,f.encoding() 获取文件在内存中的编号,f.fileno() 获取文件终端类型(tty.打印机等),f.isatty() 获取文件名,f.name() 判断文件句柄是否可移动(tty等不可移动),f.seekable() 判断文件是否可读,f.readable() 判断文件是

PYTHON修饰器的函数式编程

转自:http://coolshell.cn/articles/11265.html Python修饰器的函数式编程 Python的修饰器的英文名叫Decorator,当你看到这个英文名的时候,你可能会把其跟Design Pattern里的Decorator搞混了,其实这是完全不同的两个东西.虽然好像,他们要干的事都很相似--都是想要对一个已有的模块做一些"修饰工作",所谓修饰工作就是想给现有的模块加上一些小装饰(一些小功能,这些小功能可能好多模块都会用到),但又不让这个小装饰(小功能

函数式编程

函数式编程 函数式编程的三大特性: immutable data 不可变数据 first class functions 尾递归优化 函数式编程的准则:不依赖于外部的数据,而且也不改变外部数据的值,而是返回一个新的值给你. 如何变得functional: 1. 没有共享变量 2.通过参数和返回值传递数据 3. 函数里没有临时变量 对现有的代码进行重构(refactoring) 使得代码具有functional programming的优点 lambda a, x: x(a) lambda函数可以

Python学习:映射函数(map)和函数式编程工具(filter和reduce)

在序列中映射函数map map函数会对一个序列对象中的每一个元素应用被传入的函数,并且返回一个包含了所有函数调用结果的一个列表. 例1: def sum(x):     return x + 10 L1 = [1,2,3,4,5,6,7] L = map(sum, L1) #结果为[11, 12, 13, 14, 15, 16, 17] map还有更高级的使用方法,例如提供了序列作为参数,它能够并行返回分别以每个序列中的元素作为函数对应参数得到的结果的列表.如例2所示. 例2: def sum(

javaScript函数式编程-包含闭包、链式优化及柯里化

本文着重介绍个人理解的函数式编程. 函数式编程个人理解为:以函数为主要载体的编程方式. 好处: 语义更加清晰 可复用性高 可维护性好 作用域局限.副作用少 基本函数式编程: //实现数组中每个单词首字母大写 //一般写法 const arr = ['apple','orange','pear']; for(const i in arr) { const c = arr[i][0]; arr[i] = c.toUpperCase() + arr[i].slice(1); //slice()从已有的

javascript - Underscore 与 函数式编程

<Javascript函数式编程 PDF> # csdn下载地址http://download.csdn.net/detail/tssxm/9713727 Underscore # githubhttps://github.com/jashkenas/underscore # 中文官方网站http://www.css88.com/doc/underscore/ # CDN<script src="https://cdn.bootcss.com/underscore.js/1.8

LUA 函数式编程demo

什么是函数式编程 http://www.zhihu.com/topic/19585411/hot 函数式编程的本质函数式编程中的函数这个术语不是指计算机中的函数(实际上是Subroutine),而是指数学中的函数,即自变量的映射.也就是说一个函数的值仅决定于函数参数的值,不依赖其他状态.比如sqrt(x)函数计算x的平方根,只要x不变,不论什么时候调用,调用几次,值都是不变的.在函数式语言中,函数作为一等公民,可以在任何地方定义,在函数内或函数外,可以作为函数的参数和返回值,可以对函数进行组合.

[Java] 函数式编程相关概念 - 笔记1

Java 8 引入了 lambda 表达式,以及函数式编程风格.在了解函数式编程过程中,做了些笔记,摘录于本文. 嵌套函数( Nested Function ) 1. 嵌套函数,是指在另一个函数里面定义的一个函数.外层的函数,这里简称为外层函数. 2. 函数的嵌套可以是多层嵌套.嵌套函数可以看到其全部的外层函数的非局部变量.在实际程序中,嵌套的层数一般很少.下面是一个三层嵌套的例子, innerOfInner 也可以访问在 outer 函数体重定义的变量 x . function outer()

python_way.day7 模块(configparser,xml,shutil,subprocess)、面向对象(上)(创建类,类的构成,函数式编程与面向对象编程的选择,类的继承)

python_way.day7 1.模块 configparser,xml,shutil,subprocess 2.面向对象(上) 创建类,类的构成,函数式编程与面向对象编程的选择,类的继承 1.模块 configparser 用于处理特定格式的文件,其本职上使用open来操作,只能是  [test1] 特定的格式 [test1] k1 = 123 k2 = True [test2] k1 = 123 k2 = v1 文件内容 1.获取 import configparser #打开文件找到文件

为什么函数式编程在Java中很危险?

摘要:函数式编程这个不温不火的语言由来已久.有人说,这一年它会很火,尽管它很难,这也正是你需要学习的理由.那么,为什么函数式编程在Java中很危险呢?也许这个疑问普遍存在于很多程序员的脑中,作者Elliotte对此发表了一些见解,我们一起来看看他是怎么说的. 在我的日常工作中,我身边的开发者大多是毕业于CS编程顶级院校比如MIT.CMU以及Chicago,他们初次涉及的语言是Haskell.Scheme及Lisp.他们认为函数式编程是一种自然的.直观的.美丽的且高效的编程样式.但奇怪的是,我和我