java 函数式编程_Java开发人员的函数式编程,第1部分

java 函数式编程

Java 8向Java开发人员介绍了使用lambda表达式的函数式编程 。 该Java版本有效地通知开发人员,仅从命令式,面向对象的角度考虑Java编程已不再足够。 Java开发人员还必须能够使用声明性功能范例进行思考和编码 。

本教程介绍了函数式编程的基础。 我将从术语开始,然后我们将探讨函数式编程概念。 最后,我向您介绍五种函数式编程技术。 这些部分中的代码示例将使您开始使用纯函数,高阶函数,惰性求值,闭包和currying。

函数式编程正在上升

电气与电子工程师学会(IEEE)在2018年的前25种编程语言中都包含了函数式编程语言,并且Google趋势目前将函数式编程评为比面向对象的编程更受欢迎。

显然,函数式编程不容忽视,但是为什么它变得越来越流行? 功能编程尤其使验证程序正确性变得更加容易。 它还简化了并发程序的创建。 并发(或并行处理)对于提高应用程序性能至关重要 。

下载
获取代码
下载本教程中示例应用程序的源代码。 由Jeff Friesen为JavaWorld创建。

什么是函数式编程?

计算机通常实现冯·诺依曼体系结构 ,该体系结构是基于数学家和物理学家约翰·冯·诺依曼(及其他人)于1945年的描述而被广泛使用的计算机体系结构。 该体系结构倾向于命令式编程 ,这是一种使用语句来更改程序状态的编程范例。 C,C ++和Java都是命令式编程语言。

1977年,杰出的计算机科学家John Backus(以他在FORTRAN上的工作而著称 )作了主题为“ 可以从von Neumann风格中解放编程吗? ”的演讲。 Backus断言,冯·诺依曼体系结构及其相关的命令式语言在根本上存在缺陷,并提出了一种功能级编程语言(FP)作为解决方案。

澄清巴克斯

由于Backus讲座是几十年前提出的,因此其某些想法可能很难掌握。 Blogger TomaszJaskuła从2018年1月开始在其博客文章中添加了清晰度和脚注。

函数式程序设计概念和术语

函数式编程是一种编程风格,其中将计算编码为函数式编程函数 这些是类似于数学函数的构造(例如lambda函数),它们在表达式上下文中进行评估。

函数式编程语言是声明性的 ,这意味着在不描述其控制流程的情况下表达了计算的逻辑。 在声明式编程中,没有语句。 相反,程序员使用表达式来告诉计算机需要完成什么,而不是如何完成任务。 如果您熟悉SQL或正则表达式,那么您对声明式样式会有所了解; 两者都使用表达式来描述需要做什么,而不是使用语句来描述如何去做。

函数式编程中的计算由在表达式上下文中评估的函数来描述。 这些函数与命令式编程(例如返回值的Java方法)中使用的函数不同。 取而代之的是, 函数式编程函数就像数学函数一样,其产生的输出通常仅取决于其自变量。 每次使用相同的参数调用功能编程功能时,都会获得相同的结果。 据说函数式编程中的函数具有参照透明性 这意味着您可以使用函数的结果值替换函数调用,而无需更改计算的含义。

函数式编程支持不变性 ,这意味着状态不能改变。 在命令式编程中,命令式功能可能与状态(例如Java实例变量)相关联,在命令式编程中通常不是这种情况。 在不同的时间使用相同的参数调用此函数可能会导致返回值不同,因为在这种情况下,状态是可变的 ,这意味着状态会改变。

命令式和函数式编程的副作用

状态更改是命令式编程的副作用,阻止了引用透明性。 还有许多其他副作用值得了解,尤其是在您评估在程序中使用命令式还是函数式时。

命令式编程的一个常见副作用是,赋值语句通过更改变量的存储值来对其进行突变。 函数式编程中的函数不支持变量分配。 由于变量的初始值永远不变,因此函数编程可以消除这种副作用。

当基于抛出的异常修改命令式函数的行为时,会发生另一种常见的副作用,这是与调用方的可观察的交互。 有关更多信息,请参见堆栈溢出讨论,“ 为什么引发异常会产生副作用? ”

当I / O操作输入无法读取的文本或输出无法写入的文本时,会发生第三种常见的副作用。 请参阅Stack Exchange讨论“ IO如何在函数式编程中引起副作用? ”以了解有关此副作用的更多信息。

消除副作用使理解和预测计算行为变得更加容易。 它还有助于使代码更适合于并行处理 ,这通常可以提高应用程序性能。 尽管函数式编程中存在副作用,但它们通常比命令式编程中的副作用要少。 使用函数式编程可以帮助您编写更易于理解,维护和测试并且更可重用的代码。

函数式编程的起源(和发起者)

函数式编程起源于Alonzo Church引入的lambda演算 。 另一个起源是组合逻辑 ,它是由MosesSchönfinkel提出并随后由Haskell Curry开发的。

面向对象与函数式编程

我创建了一个Java应用程序,该应用程序与命令式,面向对象声明式的函数式编程方法进行了对比。 研究下面的代码,然后指出两个示例之间的区别。

清单1. Employees.java

import java.util.ArrayList;
import java.util.List;

public class Employees
{
   static class Employee
   {
      private String name;
      private int age;

      Employee(String name, int age)
      {
         this.name = name;
         this.age = age;
      }

      int getAge()
      {
         return age;
      }

      @Override
      public String toString()
      {
         return name + ": " + age;
      }
   }

   public static void main(String[] args)
   {
      List employees = new ArrayList<>();
      employees.add(new Employee("John Doe", 63));
      employees.add(new Employee("Sally Smith", 29));
      employees.add(new Employee("Bob Jone", 36));
      employees.add(new Employee("Margaret Foster", 53));
      printEmployee1(employees, 50);
      System.out.println();
      printEmployee2(employees, 50);
   }

   public static void printEmployee1(List employees, int age)
   {
      for (Employee emp: employees)
         if (emp.getAge() < age)
            System.out.println(emp);
   }

   public static void printEmployee2(List employees, int age)
   {
      employees.stream()
               .filter(emp -> emp.age < age)
               .forEach(emp -> System.out.println(emp));
   }
}

清单1展示了一个Employees应用程序,该应用程序创建了一些Employee对象,然后打印了所有50岁以下的雇员的列表。此代码演示了面向对象和函数式编程风格。

printEmployee1()方法揭示了命令式,面向语句的方法。 按照指定的方法,该方法遍历雇员列表,将每个雇员的年龄与参数值进行比较,并且(如果年龄小于参数),则打印雇员的详细信息。

printEmployee2()方法揭示了一种声明式的,面向表达式的方法,在这种情况下,是通过Streams API实现的。 表达式不是强制性地指定如何打印雇员(逐步),而是指定所需的结果,并将如何执行此操作的详细信息留给Java。 filter()视为if语句的功能等效项,并将forEach()视为与for语句的功能等效项。

您可以按以下方式编译清单1:

javac Employees.java

使用以下命令运行生成的应用程序:

java Employees

输出应如下所示:

Sally Smith: 29
Bob Jone: 36

Sally Smith: 29
Bob Jone: 36

函数式编程实例

在下一部分中,我们将探讨函数式编程中使用的五种核心技术:纯函数,高阶函数,惰性求值,闭包和currying。 本节中的示例使用JavaScript编码,因为相对于Java,它的简单性使我们可以专注于这些技术。 在第2部分中,我们将使用Java代码回顾这些相同的技术。

清单2将源代码呈现给RunScript ,这是一个Java应用程序,它使用Java的Scripting API来方便运行JavaScript代码。 RunScript将是所有即将出现的示例的基础程序。

清单2. RunScript.java

import java.io.FileReader;
import java.io.IOException;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

import static java.lang.System.*;

public class RunScript
{
   public static void main(String[] args)
   {
      if (args.length != 1)
      {
         err.println("usage: java RunScript script");
         return;
      }
      ScriptEngineManager manager = 
         new ScriptEngineManager();
      ScriptEngine engine = 
         manager.getEngineByName("nashorn");
      try
      {
         engine.eval(new FileReader(args[0]));
      }
      catch (ScriptException se)
      {
         err.println(se.getMessage());
      }
      catch (IOException ioe)
      {
         err.println(ioe.getMessage());
      }      
   }
}

此示例中的main()方法首先验证是否已指定单个命令行参数(脚本文件的名称)。 否则,它将显示使用情况信息并终止应用程序。

假定存在此参数, main()实例化javax.script.ScriptEngineManager类。 ScriptEngineManager是Java脚本API的切入点。

接下来,调用ScriptEngineManager对象的ScriptEngine getEngineByName(String shortName)方法以获得与所需的shortName值相对应的脚本引擎。 Java 10支持Nashorn脚本引擎 ,该引擎是通过将"nashorn"传递给getEngineByName() 返回的对象的类实现javax.script.ScriptEngine接口。

ScriptEngine声明了几种用于评估脚本的eval()方法。 main()调用Object eval(Reader reader)方法从其java.io.FileReader对象参数读取脚本,然后(假设未抛出java.io.IOException )然后对该脚本进行评估。 此方法返回任何脚本返回值,但我将其忽略。 另外,当脚本中发生错误时,此方法将引发javax.script.ScriptException

如下编译清单2:

javac RunScript.java

在介绍了第一个脚本之后,我将向您展示如何运行此应用程序。

具有纯函数的函数式编程

纯函数是仅依赖于其输入参数而不依赖于外部状态的功能编程函数。 不纯函数是违反这些要求之一的功能编程函数。 因为纯函数与外界没有交互作用(除了调用其他纯函数),所以纯函数对于相同的参数始终返回相同的结果。 纯函数也没有可观察到的副作用。

纯函数可以执行I / O吗?

如果I / O是副作用 ,那么纯函数可以执行I / O吗? 答案是肯定的。 Haskell使用monad解决此问题。 有关纯函数和I / O的更多信息,请参见“ 纯函数和I / O ”。

纯函数与不纯函数

清单3中JavaScript将不纯的calculatebonus()函数与纯的calculatebonus2()函数进行了对比。

清单3.比较纯函数和不纯函数( script1.js

// impure bonus calculation

var limit = 100;

function calculatebonus(numSales) 
{
   return(numSales > limit) ? 0.10 * numSales : 0
}

print(calculatebonus(174))

// pure bonus calculation

function calculatebonus2(numSales)
{
   return (numSales > 100) ? 0.10 * numSales : 0
}

print(calculatebonus2(174))

calculatebonus()不纯,因为它访问外部limit变量。 相反, calculatebonus2()是纯净的,因为它同时满足两个纯净度要求。 运行script1.js ,如下所示:

java RunScript script1.js

这是您应该观察的输出:

17.400000000000002
17.400000000000002

假设将calculatebonus2()重构为return calculatebonus(numSales) calculatebonus2() bonus2 calculatebonus2()仍然是纯净的吗? 答案是否定的:当纯函数调用不纯函数时,“纯函数”将变为不纯函数。

当纯函数之间不存在数据依赖关系时,可以按任何顺序对它们进行求值而不会影响结果,从而使其适合于并行执行。 这是函数式编程的好处之一。

有关不纯函数的更多信息

并非所有功能编程功能都必须是纯函数。 正如“ 函数式编程:纯函数”所解释的那样,有可能(有时是希望的)“将应用程序的基于纯函数,基于值的核心与外部命令性外壳分开”。

具有高阶函数的函数式编程

高阶函数是一个数学函数,它接收作为参数的函数,将函数返回给其调用者或两者。 一个例子是微积分的微分运算符d / dx ,它返回函数f的导数。

一流的职能是一流的公民

与数学高阶函数概念密切相关的是一类函数 ,该函数是一种功能编程函数,它以其他功能编程函数为参数并/或返回一个功能编程函数。 一流函数是一流公民,因为它们可以出现在其他一流程序实体(例如数字)可以出现的任何地方,包括分配给变量或作为参数传递给函数或从函数返回。

清单4中JavaScript演示了如何将匿名比较函数传递给一流的排序函数。

清单4.传递匿名比较函数( script2.js

function sort(a, cmp)
{
   for (var pass = 0; pass < a.length - 1; pass++)
      for (var i = a.length - 1; i > pass; i--)
         if (cmp(a[i], a[pass]) < 0)
         {
            var temp = a[i]
            a[i] = a[pass]
            a[pass] = temp
         }
}

var a = [22, 91, 3, 45, 64, 67, -1]
sort(a, function(i, j)
        {
           return i - j;
        })
a.forEach(function(entry) { print(entry) })
print('\n')
sort(a, function(i, j)
        {
           return j - i;
        })
a.forEach(function(entry) { print(entry) })
print('\n')
a = ["X", "E", "Q", "A", "P"]
sort(a, function(i, j)
        {
           return i < j ? -1 : i > j;
        })
a.forEach(function(entry) { print(entry) })
print('\n')
sort(a, function(i, j)
        {
           return i > j ? -1 : i < j;
        })
a.forEach(function(entry) { print(entry) })

在此示例中,初始sort()调用接收一个数组作为其第一个参数,后跟一个匿名比较函数。 当被调用时,匿名比较函数执行return i - j; 实现升序排序。 通过反转ij ,第二个比较函数实现了降序排序。 第三和第四sort()调用接收匿名比较功能,这些功能略有不同,以便正确比较字符串值。

运行如下的script2.js示例:

java RunScript script2.js

这是预期的输出:

-1
3
22
45
64
67
91

91
67
64
45
22
3
-1

A
E
P
Q
X

X
Q
P
E
A

筛选并映射

函数式编程语言通常提供一些有用的高阶函数。 两个常见的示例是filter和map。

  • 过滤器以某种顺序处理列表,以产生一个新列表,该列表完全包含原始列表中给定谓词(认为布尔表达式)返回true的那些元素。
  • 映射将给定功能应用于列表的每个元素,并以相同顺序返回结果列表。

JavaScript通过filter()map()高阶函数支持过滤和映射功能。 清单5演示了这些函数,用于过滤出奇数并将其映射到其多维数据集。

清单5.过滤和映射( script3.js

print([1, 2, 3, 4, 5, 6].filter(function(num) { return num % 2 == 0 }))
print('\n')
print([3, 13, 22].map(function(num) { return num * 3 }))

运行script3.js示例,如下所示:

java RunScript script3.js

您应该观察以下输出:

2,4,6

9,39,66

减少

另一个常见的高阶函数是reduce ,这通常称为fold 。 此功能将列表缩小为单个值。

清单6使用JavaScript的reduce()高阶函数将数字数组简化为一个数字,然后将其除以数组的长度以获得平均值。

清单6.将数字数组script4.js为一个数字( script4.js

var numbers = [22, 30, 43]
print(numbers.reduce(function(acc, curval) { return acc + curval })
      / numbers.length)

如下运行清单6的脚本(在script4.js ):

java RunScript script4.js

您应该观察以下输出:

31.666666666666668

您可能认为filter,map和reduce高阶函数消除了对if-else和各种循环语句的需求 ,这是正确的。 它们的内部实现负责决策和迭代。

一个高阶函数使用递归来实现迭代。 递归函数将调用自身,从而允许操作重复进行直到达到基本情况为止。 您还可以利用递归来实现功能代码中的迭代。

具有延迟评估的函数式编程

功能编程的另一个重要功能是惰性评估 (也称为非严格评估 ),它是尽可能长时间推迟表达评估的时间。 惰性评估提供了一些好处,包括以下两个方面:

  • 可以将昂贵的(按时间)计算推迟到绝对必要时再进行。
  • 无限收集是可能的。 他们将继续提供要求的内容。

惰性评估是Haskell不可或缺的。 除非绝对必要,否则它不会计算任何内容(包括在调用函数之前的函数参数)。

Java的Streams API利用了惰性评估 。 流的中间操作(例如filter() )总是很懒。 在执行终端操作(例如forEach() )之前,它们什么也不做。

尽管惰性求值是功能语言的重要组成部分,但即使许多命令式语言也为某些形式的惰性提供了内置支持。 例如,大多数编程语言都在布尔AND和OR运算符的上下文中支持短路评估 。 这些运算符是惰性的,当左侧操作数为false(AND)或true(OR)时,拒绝评估其右侧操作数。

清单7是一个JavaScript脚本中的惰性评估示例。

清单7. JavaScript中的惰性评估( script5.js

var a = false && expensiveFunction("1")
var b = true && expensiveFunction("2")
var c = false || expensiveFunction("3")
var d = true || expensiveFunction("4")

function expensiveFunction(id)
{
   print("expensiveFunction() called with " + id)
}

如下运行script5.js的代码:

java RunScript script5.js

您应该观察以下输出:

expensiveFunction() called with 2
expensiveFunction() called with 3

惰性评估通常与备忘录结合使用, 备忘录是一种优化技术,主要用于通过存储昂贵的函数调用的结果并在再次出现相同的输入时返回缓存的结果来加速计算机程序。

由于惰性求值不适用于副作用(例如产生异常和I / O的代码),因此命令式语言主要使用急切求值 (也称为严格求值 ),即表达式在绑定到变量后立即求值。

有关延迟评估和记忆的更多信息

通过Google搜索,无论有无备忘,都会显示许多有关懒惰评估的有用讨论。 一个示例是“ 使用函数式编程优化JavaScript ”。

带闭包的函数式编程

一流的函数与闭包的概念相关联, 闭包是一个持久作用域,即使代码执行离开了定义了局部变量的块,该持久范围也保留了局部变量。

手工封盖

在操作上, 闭包是存储功能及其环境的记录。 环境将函数的每个自由变量 s(局部使用,但在封闭范围内定义的变量)与创建闭包时绑定了变量名称的值或引用进行映射。 它允许函数通过闭包的值或引用的副本访问那些捕获的变量,即使函数在其作用域之外被调用也是如此。

为了帮助阐明这个概念,清单8提供了一个JavaScript脚本,它引入了一个简单的闭包。 该脚本基于此处提供的示例。

清单8.一个简单的闭包( script6.js

function add(x)
{
   function partialAdd(y)
   {
      return y + x
   }
   return partialAdd
}

var add10 = add(10)
var add20 = add(20)

print(add10(5))
print(add20(5))

清单8定义了一个名为add()的一流函数,带有一个参数x和一个嵌套函数partialAdd() 嵌套函数partialAdd()可以访问x因为xadd()的词法范围内。 函数add()返回一个闭包,该闭包包含对partialAdd()的引用以及add()周围环境的副本,其中x具有在add()的特定调用中为其分配的值。

由于add()返回函数类型的值,因此变量add10add20也具有函数类型。 add10(5)调用返回15因为调用受让人5到参数y在调用partialAdd()使用保存的环境partialAdd()其中x10 add20(5)调用返回25原因是,尽管它还在对partialAdd()的调用partialAdd() y分配了5 ,但现在它为partialAdd()使用了另一个保存的环境,其中x20 因此,虽然add10()add20()使用相同的函数partialAdd() ,但关联的环境有所不同,并且调用闭包将在两次调用中将x绑定到两个不同的值,从而将函数评估为两个不同的结果。

如下运行清单8的脚本(在script6.js ):

java RunScript script6.js

您应该观察以下输出:

15
25

带有函数的函数式编程

Currying是一种将多参数函数的求值转换成等效的单参数函数序列的求值的方法。 例如,一个函数带有两个参数: xy Currying将函数转换为仅吸收x并返回仅吸收y的函数。 Currying与部分应用程序相关,但又不同于部分应用程序 , 部分应用程序是将多个参数固定到一个函数,从而产生另一个arity较小的函数的过程。

清单9提供了一个演示curryingJavaScript脚本。

清单9.用JavaScript编写script7.jsscript7.js

function multiply(x, y)
{
   return x * y
}

function curried_multiply(x)
{
   return function(y)
   {
      return x * y
   }
}

print(multiply(6, 7))
print(curried_multiply(6)(7))
var mul_by_4 = curried_multiply(4)
print(mul_by_4(2))

该脚本提供了一个非咖喱的两元参数multiple multiply()函数,后跟一个一流的curried_multiply()函数,该函数接收被乘数参数x并返回一个闭包,该闭包包含对匿名函数的引用(该函数接收乘数参数y )和一个副本。 curried_multiply()周围的环境,其中x具有在调用curried_multiply()curried_multiply()分配的值。

脚本的其余部分首先调用带有两个参数的multiply()并输出结果。 然后,它以两种方式调用curried_multiply()

  • curried_multiply(6)(7)导致curried_multiply(6)首先执行。 返回的闭包使用闭包的保存x6乘以7执行匿名函数。
  • var mul_by_4 = curried_multiply(4)执行curried_multiply(4)并将闭包分配给mul_by_4 mul_by_4(2)使用闭包的4值和传递的参数2执行匿名函数。

如下运行清单9的脚本(在script7.js ):

java RunScript script7.js

您应该观察以下输出:

42
42
8

为什么使用咖喱呢?

休·杰克逊(Hugh Jackson)在他的博客文章“ 咖喱为什么有帮助 ”中指出,“可以轻松配置和重用小块,而不会造成混乱”。 Quora的“ 函数式编程中curring的优点是什么? ”将curring描述为“一种便宜的依赖注入形式”,它简化了映射/过滤/折叠(通常是高阶函数)的过程。 该问答还指出,“帮助我们创建抽象功能”这一说法是不正确的。

结论

在本教程中,您学习了函数式编程的一些基础知识。 我们已经使用JavaScript中的示例研究了五种核心功能编程技术,我们将在第2部分中使用Java代码进行进一步探讨 。 除了游览Java 8的功能编程功能外,本教程的下半部分还将将面向对象的Java代码的示例转换为等效的 功能 ,从而帮助您开始进行功能性思考

了解有关函数式编程的更多信息

我发现《 函数式编程简介》 (Richard Bird和Philip Wadler,Prentice Hall国际计算机科学丛书,1992年)对学习函数式编程的基础很有帮助。

这个故事“ Java开发人员的功能编程,第1部分”最初由JavaWorld发布

翻译自: https://www.infoworld.com/article/3314640/functional-programming-for-java-developers-part-1.html

java 函数式编程

你可能感兴趣的:(python,java,编程语言,javascript,人工智能)