Go函数式编程以及在Tendermint/Cosmos-SDK中的应用

Go函数式编程以及在Tendermint/Cosmos-SDK中的应用

函数式编程Functional Programming)实际是很是古老的概念,不过近几年大有愈来愈流行之势,连不少老牌语言(好比Java)也增长了对函数式编程的支持。本文结合Temdermint/Cosmos-SDK源代码,介绍函数式编程中最重要的一些概念,以及如何使用Go语言进行函数式编程。如下是本文将要讨论的主要内容:java

  • 一等函数
  • 高阶函数
  • 匿名函数
  • 闭包
  • λ表达式

一等函数

若是在一门编程语言里,函数(或者方法、过程、子例程等,各类语言叫法不一样)享有一等公民的待遇,那么咱们就说这门语言里的函数是一等函数First-class Function )。那怎样才能算是“一等公民”呢?简单来讲就是和其余数据类型待遇差很少,不会被区别对待。好比说:能够把函数赋值给变量或者结构体字段;能够把函数存在array、map等数据结构里;能够把函数做为参数传递给其余函数;也能够把函数当其余函数的返回值返回;等等。下面结合Cosmos-SDK源代码举几个具体的例子。node

普通变量

以auth模块的AccountKeeper为例,这个Keeper提供了一个GetAllAccounts()方法,返回系统中全部的帐号:python

// GetAllAccounts returns all accounts in the accountKeeper.
func (ak AccountKeeper) GetAllAccounts(ctx sdk.Context) []Account {
	accounts := []Account{}
	appendAccount := func(acc Account) (stop bool) {
		accounts = append(accounts, acc)
		return false
	}
	ak.IterateAccounts(ctx, appendAccount)
	return accounts
}
复制代码

从代码能够看到,函数被赋值给了普通的变量appendAccount,进而又被传递给了IterateAccounts()方法。git

结构体字段

Cosmos-SDK提供了BaseApp结构体,做为构建区块链App的"基础":github

// BaseApp reflects the ABCI application implementation.
type BaseApp struct {
	// 省略无关字段
	anteHandler    sdk.AnteHandler  // ante handler for fee and auth
	initChainer    sdk.InitChainer  // initialize state with validators and state blob
	beginBlocker   sdk.BeginBlocker // logic to run before any txs
	endBlocker     sdk.EndBlocker   // logic to run after all txs, and to determine valset changes
	addrPeerFilter sdk.PeerFilter   // filter peers by address and port
	idPeerFilter   sdk.PeerFilter   // filter peers by node ID
	// 省略无关字段
}
复制代码

这个结构体定义了大量的字段,其中有6个字段是函数类型。这些函数起到callback或者hook的做用,影响具体区块链app的行为。下面是这些函数的类型定义:golang

// cosmos-sdk/types/handler.go
type AnteHandler func(ctx Context, tx Tx, simulate bool) (newCtx Context, result Result, abort bool) // cosmos-sdk/types/abci.go type InitChainer func(ctx Context, req abci.RequestInitChain) abci.ResponseInitChain type BeginBlocker func(ctx Context, req abci.RequestBeginBlock) abci.ResponseBeginBlock type EndBlocker func(ctx Context, req abci.RequestEndBlock) abci.ResponseEndBlock type PeerFilter func(info string) abci.ResponseQuery 复制代码

Slice元素

Cosmos-SDK提供了Int类型,用来表示256比特整数。下面是从int_test.go文件中摘出来的一个单元测试,演示了如何把函数保存在slice里:express

func TestImmutabilityAllInt(t *testing.T) {
	ops := []func(*Int){
		func(i *Int) { _ = i.Add(randint()) },
		func(i *Int) { _ = i.Sub(randint()) },
		func(i *Int) { _ = i.Mul(randint()) },
		func(i *Int) { _ = i.Quo(randint()) },
		func(i *Int) { _ = i.AddRaw(rand.Int63()) },
		func(i *Int) { _ = i.SubRaw(rand.Int63()) },
		func(i *Int) { _ = i.MulRaw(rand.Int63()) },
		func(i *Int) { _ = i.QuoRaw(rand.Int63()) },
		func(i *Int) { _ = i.Neg() },
		func(i *Int) { _ = i.IsZero() },
		func(i *Int) { _ = i.Sign() },
		func(i *Int) { _ = i.Equal(randint()) },
		func(i *Int) { _ = i.GT(randint()) },
		func(i *Int) { _ = i.LT(randint()) },
		func(i *Int) { _ = i.String() },
	}

	for i := 0; i < 1000; i++ {
		n := rand.Int63()
		ni := NewInt(n)

		for opnum, op := range ops {
			op(&ni) // 调用函数

			require.Equal(t, n, ni.Int64(), "Int is modified by operation. tc #%d", opnum)
			require.Equal(t, NewInt(n), ni, "Int is modified by operation. tc #%d", opnum)
		}
	}
}
复制代码

Map值

仍是以BaseApp为例,这个包里定义了一个queryRouter结构体,用来表示“查询路由”:编程

type queryRouter struct {
	routes map[string]sdk.Querier
}
复制代码

从代码能够看出,这个结构体的routes字段是一个map,值是函数类型,在queryable.go文件中定义:数据结构

// Type for querier functions on keepers to implement to handle custom queries
type Querier = func(ctx Context, path []string, req abci.RequestQuery) (res []byte, err Error) 复制代码

把函数做为其余函数的参数和返回值的例子在下一小节中给出。闭包

高阶函数

高阶函数Higher Order Function)听起来很高大上,但其实概念也很简单:若是一个函数有函数类型的参数,或者返回值是函数类型,那么这个函数就是高阶函数。之前面出现过的AccountKeeperIterateAccounts()方法为例:

func (ak AccountKeeper) IterateAccounts(ctx sdk.Context, process func(Account) (stop bool)) {
	store := ctx.KVStore(ak.key)
	iter := sdk.KVStorePrefixIterator(store, AddressStoreKeyPrefix)
	defer iter.Close()
	for {
		if !iter.Valid() { return }
		val := iter.Value()
		acc := ak.decodeAccount(val)
		if process(acc) { return }
		iter.Next()
	}
}
复制代码

因为它的第二个参数是函数类型,因此它是一个高阶函数(或者更严谨一些,高阶方法)。一样是在auth模块里,有一个NewAnteHandler()函数:

// NewAnteHandler returns an AnteHandler that checks and increments sequence
// numbers, checks signatures & account numbers, and deducts fees from the first
// signer.
func NewAnteHandler(ak AccountKeeper, fck FeeCollectionKeeper) sdk.AnteHandler {
	return func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, res sdk.Result, abort bool) {
		// 代码省略
  }
}
复制代码

这个函数的返回值是函数类型,因此它也是一个高阶函数。

匿名函数

像上面例子中的NewAnteHandler()函数是有本身的名字的,可是在定义和使用高阶函数时,使用匿名函数Anonymous Function)更方便一些。好比NewAnteHandler()函数里的返回值就是一个匿名函数。匿名函数在Go代码里面很是常见,好比不少函数都须要使用defer关键字来确保某些逻辑推迟到函数返回前执行,这个时候用匿名函数就很方便。仍然以NewAnteHandler函数为例:

// NewAnteHandler returns an AnteHandler that checks and increments sequence
// numbers, checks signatures & account numbers, and deducts fees from the first
// signer.
func NewAnteHandler(ak AccountKeeper, fck FeeCollectionKeeper) sdk.AnteHandler {
	return func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, res sdk.Result, abort bool) {
		// 前面的代码省略

		// AnteHandlers must have their own defer/recover in order for the BaseApp
		// to know how much gas was used! This is because the GasMeter is created in
		// the AnteHandler, but if it panics the context won't be set properly in
		// runTx's recover call.
		defer func() {
			if r := recover(); r != nil {
				switch rType := r.(type) {
				case sdk.ErrorOutOfGas:
					log := fmt.Sprintf(
						"out of gas in location: %v; gasWanted: %d, gasUsed: %d",
						rType.Descriptor, stdTx.Fee.Gas, newCtx.GasMeter().GasConsumed(),
					)
					res = sdk.ErrOutOfGas(log).Result()

					res.GasWanted = stdTx.Fee.Gas
					res.GasUsed = newCtx.GasMeter().GasConsumed()
					abort = true
				default:
					panic(r)
				}
			}
		}() // 就地执行匿名函数

		// 后面的代码也省略
	}
}
复制代码

再好比使用go关键字执行goroutine,具体例子参见定义在cosmos-sdk/server/util.go文件中的TrapSignal()函数:

// TrapSignal traps SIGINT and SIGTERM and terminates the server correctly.
func TrapSignal(cleanupFunc func()) {
	sigs := make(chan os.Signal, 1)
	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
	go func() {
		sig := <-sigs
		switch sig {
		case syscall.SIGTERM:
			defer cleanupFunc()
			os.Exit(128 + int(syscall.SIGTERM))
		case syscall.SIGINT:
			defer cleanupFunc()
			os.Exit(128 + int(syscall.SIGINT))
		}
	}()
}
复制代码

闭包

若是匿名函数可以捕捉到词法做用域Lexical Scope)内的变量,那么匿名函数就能够成为闭包Closure)。闭包在Cosmos-SDK/Temdermint代码里也可谓比比皆是,以bank模块的NewHandler()函数为例:

// NewHandler returns a handler for "bank" type messages.
func NewHandler(k Keeper) sdk.Handler {
	return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
		switch msg := msg.(type) {
		case MsgSend:
			return handleMsgSend(ctx, k, msg) // 捕捉到k
		case MsgMultiSend:
			return handleMsgMultiSend(ctx, k, msg) // 捕捉到k
		default:
			errMsg := "Unrecognized bank Msg type: %s" + msg.Type()
			return sdk.ErrUnknownRequest(errMsg).Result()
		}
	}
}
复制代码

从代码不难看出:NewHandler()所返回的匿名函数捕捉到了外围函数的参数k,所以返回的实际上是个闭包。

λ表达式

匿名函数也叫作λ表达式Lambda Expression),不过不少时候当咱们说λ表达式时,通常指更简洁的写法。之前面出现过的TestImmutabilityAllInt()函数为例,下面是它的部分代码:

ops := []func(*Int){
	func(i *Int) { _ = i.Add(randint()) },
	func(i *Int) { _ = i.Sub(randint()) },
	func(i *Int) { _ = i.Mul(randint()) },
	// 其余代码省略
}
复制代码

从这个简单的例子不难看出,Go的匿名函数写法仍是有必定冗余的。若是把上面的代码翻译成Python的话,看起来像是下面这样:

ops = [
  lambda i: i.add(randint()),
  lambda i: i.sub(randint()),
  lambda i: i.mul(randint()),
  # 其余代码省略
]
复制代码

若是翻译成Java8,看起来则是下面这样:

IntConsumer[] ops = new IntConsumer[] {
  (i) -> {i.add(randint())},
  (i) -> {i.sub(randint())},
  (i) -> {i.mul(randint())},
  // 其余代码省略
}
复制代码

能够看到,不管是Python仍是Java的写法,都要比Go简洁一些。当匿名函数/闭包很短的时候,这种简洁的写法很是有优点。目前有一个Go2的提案,建议Go语言增长这种简洁的写法,可是并不知道可否经过以及什么时候能添加进来。

总结

Go虽然不是存粹的函数式编程语言,可是对于一等函数/高阶函数、匿名函数、闭包的支持,使得用Go语言进行函数式编程很是方便。

本文由CoinEx Chain团队Chase写做,转载无需受权。

相关文章
相关标签/搜索