「读书笔记」让自己习惯于函数是“一等公民”

86 阅读4分钟

函数与方法

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 目前并不支持)。

往期回顾

关注我

掘金:XQGang

Github: XQ-Gang

参考

《Go 语言精进之路:从新手到高手的编程思想、方法和技巧》——白明