Lambdas不意味着函数式编程

18-11-24 banq
              

Java世界中没有人正在进行函数式编程,如果因为你使用Lambda表达式,但不意味着你正在进行函数式编程。

Java的Lambda表达式只是一种不那么冗长的创建对象的方式,因此在没有很好地理解核心函数概念的情况下,冒然采用Lambda的最可能的结果是粗糙、扭曲,难以理解。

到底是什么函数编程呢?

Java世界Java-land中的很多人都试图将传统的命令式Java代码和函数性代码混合在一起,并取得了不同程度的成功。Java不是一种函数式语言,它基本上是一种面向对象的语言,它允许我们采用一些函数概念,因为我们通过开发人员纪律强制执行它们的正确实现。与Haskell,Idris,Ocaml(甚至Scala)不同,Java编译器不会帮助我们。

如果我们要采用函数原则在Java中试图成功地混合面向对象oop和函数编程fp,我们最好知道它们分别定义是什么,函数式语言的核心是什么?

函数编程核心是laziness惰延迟吗?

Java中的Streams确实是很lazy (虽然Optional和CompletableFuture不是)。也许lazy是fp的核心内容。Haskell是一种lazy的语言,但遗憾的是,Ocaml,Idris和ML(名称为3)在默认情况下都是严格的(是eager,而非lazing)。

懒加载和性能

在所有性能最佳的代码未执行代码之后,懒加载(惰延迟)可以提高性能。在下面的简单示例中,一旦我们从某处加载了10条好记录(而不是先加载所有记录然后过滤它们),我们就会停止处理。

Stream.generate(this::loadFromSomewhere)
      .filter(this::identifyStuffWeWant)
      .limit(10)
      .collect(Collectors.toList());

这是否真正具有性能优势是值得商榷的,我们总是能够以某种方式将我们的过滤和限制逻辑与我们的加载逻辑耦合,即使在需要立即处理数据以避免做多余的工作时也是这么做的。

懒加载可以成为提高性能的有用工具,但常常会添加持续开销来降低性能。由于懒加载,编译器无法计算函数参数并将值传递给函数,它必须在挂起(或thunk)中记录堆中的表达式,这样才能在以后进行真正运行加载计算。存储和计算的暂停是昂贵的,如果表达式无论如何都要进行计算评估则这样做就是不必要的。

至少从2015年开始,可以将Haskell配置为默认使用严格评估

懒加载是函数式语言的一个非常酷的特性,我们可以利用它来利用Java,但我不认为它是函数式语言的关键特性之一。

函数编程核心是函数组合吗?

函数组合绝对是函数编程的重要组成部分。前面提到的所有函数语言都将函数作为一等公民,一旦我们拥有它,我们就可以通过匹配类型将函数调用链接在一起。

组合函数的能力打开了高阶函数的大门,并扩展到类别理论(如Monads和Functors)的概念/模式的实现。

但是所有这些只有在函数是纯粹的时候才有效,>那就是它们没有变异或影响函数之外的状态。这将我们带入下一个潜在的核心特征。

函数编程核心是不变性吗?

这对于函数式编程也非常重要。所有上述语言都默认鼓励不变性,并完全避免(在不同程度上)可变性。在Java生态系统中,我们的大多数核心数据结构(在JDK本身中)都是可变的,并且对创建和使用不可变对象的本机支持有限。

函数编程核心是类型系统吗?

使用更强大的类型系统来强制执行该函数是纯粹的,这可能是OO开发人员必须跳到函数编程的最大障碍。Scala,OCaml,ML,Haskell和Idris的类型系统的函数和严格程度各不相同 - 所有这些都提供了比Javas更强大的类型系统。像Haskell和Idris这样的高级函数语言不仅具有极其先进的类型系统,而且编译器对正确性的强制执行也是非常不容忍的。

相比之下,Java是弱类型系统,也有一个高度宽容的编译器

我们可以使用ServiceLoader(和其他机制)根据存在的jar完全更改应用程序的运行时行为。在您的测试环境中有两个特定提供程序的实现(因为您有编译时依赖项和测试代表),但只有一个在生产中 - 那么您的应用程序在prod中的行为可能与在单元测试中完全不同。祝你调试好运!

我们甚至可以在没有声明CheckedExceptions的方法上抛出CheckedExceptions(通过异常软化)。

public void doIO(){
   throw uncheck(new IOException("not declared!!");
}
private static <T extends Throwable> T uncheck(final Throwable throwable) throws T {
    throw (T) throwable;
}

我们可以编写这样的代码,编译器不会抱怨: -

List<String> createList(){
   List strs = new ArrayList();
   strs.add(“hello”);
   strs.add(“hello”);
   strs.add(10);
  return strs;
}

相比之下,函数式语言通常具有非常不容忍的编译器,这些编译器会消除我们最喜欢的Java hacks。我们花费了数十年的时间来寻找绕过(有限的)Java类型系统的方法,使其更像是动态类型语言,而不是强大的静态类型语言。我们大多数流行的核心库都严重依赖这些hacks(祝你好运!)。这些受限制的函数式语言因此(至少在开始时)更难以使用,但是一旦习惯它们也会更加可靠。

函数编程核心是monads 和pro-functor ?

将类别理论中的概念引入类型化函数编程有助于我们使用编译器强制约束。它们可以为编程提供一些非常好的模式,但正是这些约束使我们摆脱了(通常非常难以调试)运行时错误的暴政。上周我花了很多时间慢慢地确定了一个只在运行时在Java中出现的jar冲突 - 一次删除一个依赖项,深入了解复杂的第三方库。

函数编程核心是编译时的正确性

函数式语言有助于我们摆脱导致运行时错误的坏习惯。

如果Javac不帮助我们,我们能做些什么?​​​​​​​

我们不应该花时间试图欺骗Javac,而应该:

  1. 好好利用泛型类型
  2. 避免可变状态,避免空值,不抛出异常, 避免锁和同步,
  3. 使我们的数据类不可变。使用不变集合。尽可能避免使用instanceof检查,并在使用它们时确保类型不可扩展。

 

              

1