Go语言函数式编程

第二章 函数式编程

2.1 函数式编程

“正统”函数式编程:

  • 不可变性:不能有状态,只有常量和函数
  • 函数只能有一个参数
  • 但正统函数式编程的数学性较强可读性不高,本学习过程中不作上述严格规定

函数式编程的特点:

  • 函数是一等公民(函数的参数、变量、返回值都可以是函数)
  • 高阶函数
  • 闭包

2.1.1 闭包

在函数式编程中,应用最多的就是闭包。

在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。
来自维基百科

简单来说,闭包就是在函数内使用外部自由变量。

当一个函数返回值为一个函数时,其不仅仅是返回一个函数,而是返回一个闭包,闭包示意图如下:

image

具体案例如下,累加器案例:

其中的sum就是自由变量。

package main

import "fmt"

func adder() func(int) int {
   sum := 0
   return func(v int) int {
      sum += v
      return sum
   }
}

func main() {
   a := adder()
   for i := 0; i < 10; i++ {
      fmt.Printf("0 + 1 + ... + %d = %d\n", i, a(i))
   }
   //adder()中的sum在其返回的func外面,但是在每一次的循环中我们发现,sum会保存上一次累加的值
   //所以adder()函数return的不仅仅是一个函数,而是返回了一个闭包,adder()除了返回了一个func还返回了对sum的引用,sum会保存上一次的累加
}

控制台输出:

0 + 1 + ... + 0 = 0
0 + 1 + ... + 1 = 1
0 + 1 + ... + 2 = 3
0 + 1 + ... + 3 = 6
0 + 1 + ... + 4 = 10
0 + 1 + ... + 5 = 15
0 + 1 + ... + 6 = 21
0 + 1 + ... + 7 = 28
0 + 1 + ... + 8 = 36

顺带将上述的累加器案例转换成正统函数式编程,如下:

package main

import "fmt"

type iAdder func(int) (int, iAdder)

func adder(base int) iAdder {
   return func(v int) (int, iAdder) {
      return base + v, adder(base + v)
   }
}

func main() {
   a := adder(0)
   for i := 0; i < 10; i++ {
      var s int
      s, a = a(i)
      fmt.Printf("0 + 1 + ... + %d = %d\n", i, s)
   }
}

2.2 案例一 斐波那契数列

斐波那契数列:数列中的每个数字,都是由前两个数字累加而得到。

package main

import "fmt"

//1, 1, 2, 3, 5, 8, 13, ...
//  a, b
//    a, b
//       a, b
func fibonacci() func() int {
   a, b := 0, 1
   return func() int {
      a, b = b, a+b
      return a
   }
}

func main() {
   f := fibonacci()
   fmt.Println(f()) //1
   fmt.Println(f()) //1
   fmt.Println(f()) //2
   fmt.Println(f()) //3
   fmt.Println(f()) //5
   fmt.Println(f()) //8
   fmt.Println(f()) //13
   fmt.Println(f()) //21
   fmt.Println(f()) //34
   fmt.Println(f()) //55
}

2.2.1 为函数实现接口(修改斐波那契数列函数)

我们希望像读文件一样来,让其自动生成。所以就需要实现read接口:

将之前的打印函数拿过来:

func printFileContents(reader io.Reader) {
   scanner := bufio.NewScanner(reader)

   for scanner.Scan() {
      fmt.Println(scanner.Text())
   }
}

想直接调用printContentFile()方法就自动生成,所以传入的必须是reader接口类型。

通过type intGen func() int使intGen类型实现Reader接口。

//函数实现接口
//定义一个函数,使用type修饰,可以实现接口,也就是说只要是被type修饰的东西都可以实现接口
type intGen func() int

func (g intGen) Read(p []byte) (n int, err error) {
   next := g()
   if next > 10000 { //这里因为不设置退出会一直打印下去,所以做了限制
      return 0, io.EOF
   }
   s := fmt.Sprintf("%d\n", next)
   return strings.NewReader(s).Read(p)
}

函数返回为刚定义的函数。

//1, 1, 2, 3, 5, 8, 13, ...
//  a, b
//    a, b
//       a, b
func fibonacci() intGen {
   a, b := 0, 1
   return func() int {
      a, b = b, a+b
      return a
   }
}

main函数:

func main() {
   f := fibonacci()
   printFileContents(f)
}

控制台输出:

1
1
2
3
5
8
13
21
34
55
89
144
233
377
610
987
1597
2584
4181
6765

通过这个例子我们发现,在go语言中,函数也可以实现接口。也可以理解了函数为‌一等公民概念。

2.3 案例二 使用函数来遍历二叉树(修改中序遍历二叉树案例)

通过函数式编程,改造traversal.go中的InOrderTraversal()方法,使其更具有普适性,因为我们之前在定义的时候,方法体内部只是按照中序顺序对二叉树每个节点做了打印操作,而再实际工作中可能业务上可能会按照中序顺序对每个节点做很多其他的事情,而且在编写方法时还不知道调用者会按照中序顺序对二叉树进行什么操作,为了函数的可拓展性,运用函数式编程来达到此目的。

traversal.go

package tree

import "fmt"

//TraverseFunc()相当于一个中序模板,不仅可以用来按照中序顺序打印二叉树每个节点还可以按照中序的顺序干其他事情
//在编写TraverseFunc()时是不知道调用者会用此函数干什么的,此函数用途为接收一个函数来进行中序的顺序对二叉树进行某项操作
func (node *Node) InOrderTraverseFunc(f func(*Node)) {
   if node == nil {
      return
   }

   node.Left.InOrderTraverseFunc(f)
   f(node)
   node.Right.InOrderTraverseFunc(f)
}

//传递打印节点的函数给中序模板函数TraverseFunc()实现按照中序顺序打印二叉树
func (node *Node) InOrderTraversal() {
   //将打印节点的函数传给TraverseFunc()这样就实现了按照中序的顺序打印二叉树每个节点
   node.InOrderTraverseFunc(func(node *Node) {
      node.Print()
   })
   fmt.Println()
}

//普通的中序遍历
func (node *Node) InOrderTraversal2() {
   if node == nil {
      return
   }

   node.Left.InOrderTraversal2()
   node.Print()
   node.Right.InOrderTraversal2()
}

main:

package main

import (
   "fmt"
   "learngo/tree"
)

type myTreeNode struct {
   *tree.Node
}

func main() {
   root := myTreeNode{&tree.Node{Value: 3}} //直接让root成为myTreeNode类型
   root.Left = &tree.Node{}
   root.Right = &tree.Node{Value: 5}
   root.Right.Left = new(tree.Node)
   root.Left.Right = tree.CrateNode(2)
   root.Right.Left.SetValue(4)

   root.InOrderTraversal() //0 2 3 4 5

   //可以运用中序模板函数InOrderTraverseFunc()来对二叉树节点个数进行统计
   nodeCount := 0
   root.InOrderTraverseFunc(func(node *tree.Node) {
      nodeCount++
   })
   fmt.Println(nodeCount) //5
}
posted @   雪碧锅仔饭  阅读(76)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示