函数与方法
21 让自己习惯于函数是“一等公民”
Go 语言没有典型的面向对象语法,Go 语言中的方法本质上是函数的一个变种。所以本质上,我们可以说 Go 程序就是一组函数的集合。并且,函数在 Go 语言中属于“一等公民”。
一等公民:一门编程语言对某种语言元素的创建和使用没有限制,我们可以像对待值一样对待这种语法元素。拥有“一等公民”待遇的语法元素可以存储在变量中,可以作为参数传递给函数,可以在函数内部创建并可以作为返回值从函数返回。在动态类型语言中,语言运行时还支持对“一等公民”类型的检查。
Go 语言函数的“一等公民”身份体现在:
- 直接创建
- 在函数内创建 匿名函数
- 作为类型
- 存储到变量中;放入数组、切片或 map 等结构中;赋值给 interface{};建立元素为函数的 channel
- 作为参数传入函数
- 作为返回值从函数返回
函数作为“一等公民”的特殊运用:
-
像对整型变量那样对函数进行显式类型转换
type BinaryAdder interface { Add(int, int) int } type MyAdderFunc func(int, int) int func (f MyAdderFunc) Add(x, y int) int { return f(x, y) } func MyAdd(x, y int) int { return x + y } func main() { var i BinaryAdder = MyAdderFunc(MyAdd) fmt.Println(i.Add(5, 6)) // 11 // MyAdderFunc类型实现了BinaryAdder接口 } -
函数式编程
-
柯里化(currying)函数:柯里化是把接受多个参数的函数变换成接受一个单一参数(原函数的第一个参数)的函数,并返回接受余下的参数和返回结果的新函数的技术。
func times(x, y int) int { return x * y } func partialTimes(x int) func(int) int { return func(y int) int { return times(x, y) } } func main() { timesTwo := partialTimes(2) timesThree := partialTimes(3) timesFour := partialTimes(4) fmt.Println(timesTwo(5)) // 10 fmt.Println(timesThree(5)) // 15 fmt.Println(timesFour(5)) // 20 }这个例子利用了函数的两点性质:在函数中定义,通过返回值返回;闭包。
闭包是函数内部定义的匿名函数,并且允许该匿名函数访问定义它的外部函数的作用域。本质上,闭包是将函数内部和函数外部连接起来的桥梁。
-
函子(functor):函子本身是一个容器类型,以 Go 语言为例,这个容器可以是切片、map 甚至 channel;该容器类型需要实现一个方法,该方法接受一个函数类型参数,并在容器的每个元素上应用那个函数,得到一个新函子,原函子容器内部的元素值不受影响。
type IntSliceFunctor interface { Fmap(fn func(int) int) IntSliceFunctor } type intSliceFunctorImpl struct { ints []int } func (isf intSliceFunctorImpl) Fmap(fn func(int) int) IntSliceFunctor { newInts := make([]int, len(isf.ints)) for i, elt := range isf.ints { retInt := fn(elt) newInts[i] = retInt } return intSliceFunctorImpl{ints: newInts} } func NewIntSliceFunctor(slice []int) IntSliceFunctor { return intSliceFunctorImpl{ints: slice} } func main() { // 原切片 intSlice := []int{1, 2, 3, 4} fmt.Printf("init a functor from int slice: %#v\n", intSlice) // []int{1, 2, 3, 4} f := NewIntSliceFunctor(intSlice) fmt.Printf("original functor: %+v\n", f) // {ints:[1 2 3 4]} mapperFunc1 := func(i int) int { return i + 10 } mapped1 := f.Fmap(mapperFunc1) fmt.Printf("mapped functor1: %+v\n", mapped1) // {ints:[11 12 13 14]} mapperFunc2 := func(i int) int { return i * 3 } mapped2 := mapped1.Fmap(mapperFunc2) fmt.Printf("mapped functor2: %+v\n", mapped2) // {ints:[33 36 39 42]} fmt.Printf("original functor: %+v\n", f) // 原functor没有改变 fmt.Printf("composite functor: %+v\n", f.Fmap(mapperFunc1).Fmap(mapperFunc2)) // {ints:[1 2 3 4]} }我们可以对最初的函子实例连续组合应用转换函数;无论如何应用转换函数,原函子中容器内的元素值不受影响。
函子非常适合用来对容器集合元素进行批量同构处理,而且代码也比每次都对容器中的元素进行循环处理要优雅、简洁许多。但要想在 Go 中发挥函子最大效能,还需要 Go 泛型的支持,否则我们就需要为每一种容器类型都实现一套对应的 Functor 机制。
-
延续传递式(Continuation-passing Style, CPS):在 CSP 风格中,函数是不允许有返回值的。一个函数 A 应该将其想返回的值显式传给一个 continuation 函数(一般接受一个参数),而这个 continuation 函数自身是函数 A 的一个参数。
// 求阶乘函数 - 递归方法 func factorial(n int) int { if n == 1 { return 1 } else { return n * factorial(n-1) } } // 求阶乘函数 - CPS风格 func factorialCPS(n int, f func(int)) { if n == 1 { f(1) //基本情况 } else { factorialCPS(n-1, func(y int) { f(n * y) }) } } func main() { fmt.Printf("%d\n", factorial(5)) // 120 factorialCPS(5, func(y int) { fmt.Printf("%d\n", y) }) // 120 }这里的 CPS 风格写法是一个反例,尽管“一等公民”的函数给 Go 带来了强大的表达能力,但是如果选择了不适合的风格或者为了函数式而进行函数式编程,那么就会出现代码难于理解且代码执行效率不高的情况(CPS 需要语言支持尾递归优化,但 Go 目前并不支持)。
-
往期回顾
关注我
参考
《Go 语言精进之路:从新手到高手的编程思想、方法和技巧》——白明