使用Golang进行函数式编程

Golang

为什么要用Go练习函数式编程?简而言之,正是由于缺少状态和可变数据,函数式编程使您的代码更易读,更易于测试且不太复杂。如果遇到错误,只要不违反函数式编程规则,就可以快速调试应用程序。当函数被隔离时,您不必处理影响输出的隐藏状态的更改。

软件工程师兼作者Eric Elliot定义了以下函数编程。

函数式编程是通过组合纯函数,避免共享状态,可变数据和副作用来构建软件的过程。函数式编程是声明性的,而不是命令性的,应用程序状态通过纯函数流动。与面向对象的编程相反,后者通常将应用程序状态与对象中的方法共享并放置在对象中。

我将更进一步:函数式编程(如面向对象和过程式编程)代表着范式的转变。它在编写代码时采用了独特的思维方式,并引入了一套全新的规则。

4个重要概念

要完全掌握函数式编程,必须首先了解以下相关概念。

  1. 纯函数和幂等
  2. 副作用
  3. 函数构成
  4. 共享状态和不变数据

让我们快速回顾一下。

纯函数和幂等

如果给纯函数提供相同的输入,则它总是会返回相同的输出。此属性也称为幂等。幂等意味着函数应始终返回相同的输出,而与调用次数无关。

副作用

纯函数不能有任何副作用。换句话说,您的函数无法与外部环境进行交互。

例如,函数式编程将API调用视为副作用。为什么?因为API调用被认为是不受您直接控制的外部环境。一个API可能有几个不一致的地方,例如超时或失败,或者甚至可能返回意外的值。它不适合纯函数的定义,因为每次调用API时都需要一致的结果。

其他常见的副作用包括:

  • 数据变化
  • DOM操作
  • 请求有冲突的数据,例如当前时间time.Now()

函数构成

函数构成的基本思想很简单:将两个纯函数组合在一起以创建一个新函数。这意味着为相同输入产生相同输出的概念在这里仍然适用。因此,从简单的纯函数开始创建更高级的函数很重要。

共享状态和不变数据

函数式编程的目的是创建不保持状态的函数。共享状态尤其会在纯函数中引入副作用或可变性问题,使它们变得不纯粹。

但是,并非所有状态都不好。有时,必须有一个状态才能解决特定的软件问题。函数式编程的目的是使状态可见和显式,以消除任何副作用。程序使用不可变数据结构从纯函数中派生新数据。这样,就不需要可能引起副作用的可变数据。


现在我们已经涵盖了基础,让我们定义一些在Go中编写功能代码时要遵循的规则。

功能编程规则

如前所述,函数式编程是一种范例。因此,很难为这种编程风格定义确切的规则。也不一定总是遵循这些规则。有时,您确实需要依赖拥有状态的功能。

但是,为了尽可能严格地遵循函数式编程范例,我建议坚持以下准则。

  • 没有可变数据以避免副作用
  • 无状态(或者隐式状态,例如循环计数器)
  • 给变量赋值后请勿修改
  • 避免副作用,例如API调用

我们在函数式编程中经常遇到的一个好的“副作用”是强大的模块化。函数式编程不是自上而下地进行软件工程,而是鼓励自下而上的编程风格。首先定义模块,把将来可能使用的同类纯函数组合起来。接下来,开始编写那些小的,无状态的独立函数,以创建您的第一个模块。

实质上我们是在创建黑匣子。稍后,我们将按照自下而上的方式将各个块捆绑在一起。这使您可以建立强大的测试基础,尤其是可以验证纯函数正确性的单元测试。

一旦您可以信任您的模块,就可以将模块捆绑在一起了。开发过程中的这一步还涉及编写集成测试,以确保两个组件的正确集成。

5个示例

为了更全面地描述Go函数编程的工作原理,让我们探索五个基本示例。

  1. 更新字符串

这是纯函数的最简单示例。通常,当您要更新字符串时,请执行以下操作。

	name= "first name"
	name= name + "last name"

上面的代码片段不符合函数式编程的规则,因为不能在函数内修改变量。因此,我们应该重写代码段,以便每个值都具有自己的变量。

下面的代码段中的代码更具可读性。

	firstname := "first"
	lastname := "last"
	fullname := firstname + " " + lastname

在查看非函数式代码段时,我们必须浏览程序以确定最新状态,才可以找到name变量的结果值。这需要更多的精力和时间来了解该功能的作用。

  1. 避免更新数组

如前所述,函数式编程的目的是使用不变数据通过纯函数得出新的不变数据状态。我们可以在每次需要更新数组时创建一个新数组来实现

在非函数式编程中,更新数组如下:

	names := [3]string{"Tom", "Ben"}

	// Add Lucas to the array
	names[2] = "Lucas"

让我们根据功能编程范例进行尝试。

	names := []string{"Tom", "Ben"}
	allNames := append(names, "Lucas")
  1. 避免更新map

这是函数编程的极端示例。想象一下,我们有一个带有字符串类型的键和整数类型的值的map。该map包含我们仍然留在家中的水果数量。但是,我们刚购买了苹果,并希望将其添加到列表中。

	fruits := map[string]int{"bananas": 11}

	// Buy five apples
	fruits["apples"] = 5

我们可以在功能编程范例下完成相同的功能。

	fruits := map[string]int{"bananas": 11}
    newFruits := map[string]int{"apples": 5}

    allFruits := make(map[string]int, len(fruits) + len(newFruits))


    for k, v := range fruits {
        allFruits[k] = v
    }


    for k, v := range newFruits {
        allFruits[k] = v
    }

由于我们不想修改原始map,因此代码会遍历两个map,并将值添加到新map。这样,数据保持不变。

正如您可能通过代码的长度可以看出的那样,此代码段的性能比对map进行简单的可变更新要差得多,因为我们要遍历两个map。这是您为代码性能交换更好的代码质量的时间。

  1. 高阶函数和柯里化

大多数程序员在他们的代码中通常不会使用高阶函数,但是在函数式编程中柯里化很方便。

假设我们有一个简单的函数,将两个整数相加。尽管这已经是一个纯粹的功能,但我们希望详细说明该示例,以展示如何通过curring创建更高级的功能。

在这种情况下,我们只能接受一个参数。接下来,该函数返回另一个函数作为闭包。因为该函数返回一个闭包,所以它将记住外部范围,该范围包含初始输入参数。

	func add x intfunc y intint {
		return funcy intint {
			return x + y
		}
	}

现在,让我们尝试currying并创建更多高级纯函数。

	func main() {
		// Create more variations
		add10 := add(10)
		add20 := add(20)

		// Currying
		fmt.Println(add10(1)) // 11
		fmt.Println(add20(1)) // 21
}

这种方法在函数式编程中很常见,尽管您通常不会在范式之外看到它。

  1. 递归

递归是一种通常用于规避循环使用的软件模式。因为循环始终保持内部状态以明确循环在哪一轮,所以我们不能在函数式编程范式下使用循环。

例如,下面的代码片段尝试计算数字的阶乘。阶乘是一个整数与其下所有整数的乘积。因此,阶乘4等于24(= 4 * 3 * 2 * 1)。

通常,您将为此使用循环。

	func factorial(fac int) int {
		result := 1
		for ; fac > 0; fac-- {
			result *= fac
		}
		return result
	}

为了在函数式编程范例中完成此任务,我们需要使用递归。换句话说,我们将一遍又一遍地调用相同的函数,直到达到阶乘的最低整数为止。

	func calculateFactorial(fac int) int {
		if fac == 0 {
			return 1
		}
		return fac * calculateFactorial(fac - 1)
	}

结论

让我们总结一下我们从函数式编程中学到的知识:

  • 尽管Golang支持函数式编程,但它并非为此目的而设计的,如缺少Map,Filter和Reduce函数。
  • 函数式编程提高了代码的可读性,因为函数是纯粹的,因此易于理解
  • 纯函数更易于测试,因为没有内部状态会改变输出

原文在此: https://blog.logrocket.com/functional-programming-in-go/