在 Go 中练习函数式编程

在使用新语言时,您总是会尝试找到与您已经知道的语言的相似之处。我在 JavaScript 和 Java 方面经验丰富,可以得出结论,Go 与 JS 更相似。为什么呢?嗯,在我看来,这是因为支持一些功能范式特性。

纯函数

它是函数式编程的基本实体。来自维基:

对于相同的参数,函数返回值相同

下面是一个纯函数的例子:

func multiply(a, b int) int {
  return a * b
}

作为一等公民的职能

根据维基,这意味着:

给定实体(例如函数)支持其他实体固有的所有操作属性;属性,例如能够分配给变量、作为函数参数传递、从函数返回等。

我们可以返回一个函数:

package main

import "fmt"

func main() {
	myFunc := makeCounter()
	myFunc()
	myFunc()
}

func makeCounter() func() {
	counter := 0
	return func() {
		fmt.Println(counter)
		counter++
	}
}

在这里你应该注意的是,makeCounter函数不仅返回一个新的匿名函数,而且还有一个保存在匿名函数词法闭包中的变量,就像在 JavaScript 中一样。

在编程语言中,闭包,也称为词法闭包或函数闭包,是 一种在具有一流函数的语言中实现词法范围名称绑定的技术。在操作上,闭包是将函数与环境一起存储的记录。

包装

在撰写本文时,Go 不支持通过 Go 版本 1.18 的泛型,该版本将包括对泛型类型的支持,应于 2022 年 3 月发布

Go 1.18 尚未发布。这些是正在进行中的发行说明。Go 1.18 预计将于 2022 年 3 月发布。

尽管如此,您也可以使用一些包装函数,例如,如果您想在调用某些函数时向输出提供一些数据,如下所示:

func multiply(a, b int) int {
	return a * b
}

func wrapWithLogs(fn func(a, b int) int) func(a,b int) int {
	return func(a, b int) int {
		start := time.Now()
		r := fn(a, b)
		duration := time.Since(start)
		fmt.Println("function took ", duration)
		return r
	}
}

当然,使用泛型会更有效。所以我们只需要等待一段时间,直到新版本发布:)

如果要动态获取函数的名称,还可以使用:

runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name()

Wrapping 打开了一扇门,可以使用一些很酷的东西,比如 memoization

Memoization

在计算中,记忆化或记忆化是 一种优化技术,主要用于通过存储昂贵的函数调用的结果并在再次出现相同的输入时返回缓存的结果来加速计算机程序

就像它说的那样,如果你有一些繁重的计算 – 最好将已经计算的结果缓存在某处并使用它们,而不是再次进行相同的计算。

func sum(a,b int) int {
	return a + b
}

func memo(fn func(a, b int) int) func(a, b int) int {
	cache := make(map[string]int)
	return func(a, b int) int {
		key := strconv.Itoa(a) + " " + strconv.Itoa(b)
		v, ok := cache[key]
		if !ok {
			fmt.Println("calculating...")
			cache[key] = fn(a, b)
			v = cache[key]
		}
		return v
	}
}

func main() {
	mSum := memo(sum)
	fmt.Println(mSum(2, 3))
	fmt.Println(mSum(2, 3))
}

结果将是:

计算… 5 5

当然,您可以避免包装函数并直接在函数内部编写记忆逻辑。但是你仍然会使用闭包在里面存储缓存。

递归

如果我们需要计算像阶乘这样的东西,我们可以使用递归:

func funcFactorial(num int) int {
	if num == 0 {
		return 1
	}
	return num * funcFactorial(num-1)
}

但请注意尽可能使用递归。问题在于,在大多数情况下,最好使用一些堆栈或队列而不是递归。通常,内存限制比调用堆栈的限制更远。

Currying

还记得我们在第一个例子中讨论过闭包吗?柯里化是关于获取一个参数并在闭包中返回它。例如,我们有一个函数 multiply:

func multiply(a, b int) int {
  return a * b
}

我们可以像这样调用这个函数multiply(2, 3)。使用柯里化,我们可以以某种方式调用这个函数multiply(2)(3)。为此,我们需要重写我们的 main 函数:

func multiply(a int) func(b int) int {
  return func(b int) int {
    return a * b
  }
}

但是仅仅为了使用柯里化而重写现有函数并不是一个好习惯。使用一些包装函数,我们可以以更好的方式做到这一点:

func multiply(a, b int) int {
	return a * b
}

func curry(a int) func(b int) int {
	return func(b int) int {
		return multiply(a, b)
	}
}

结论

Go 支持函数式范式的一些特性以使事情变得更容易。但不要忘记它是结构化语言,没有必要尝试以函数式或 OOP 风格编写 Go 代码。这两种情况你都会输。