Go函數語言程式設計以及在Tendermint/Cosmos-SDK中的應用

CET_Talk發表於2019-07-12

Go函數語言程式設計以及在Tendermint/Cosmos-SDK中的應用

Go函數語言程式設計以及在Tendermint/Cosmos-SDK中的應用

函數語言程式設計Functional Programming)實際是非常古老的概念,不過近幾年大有越來越流行之勢,連很多老牌語言(比如Java)也增加了對函數語言程式設計的支援。本文結合Temdermint/Cosmos-SDK原始碼,介紹函數語言程式設計中最重要的一些概念,以及如何使用Go語言進行函數語言程式設計。以下是本文將要討論的主要內容:

  • 一等函式
  • 高階函式
  • 匿名函式
  • 閉包
  • λ表示式

一等函式

如果在一門程式語言裡,函式(或者方法、過程、子例程等,各種語言叫法不同)享有一等公民的待遇,那麼我們就說這門語言裡的函式是一等函式First-class Function )。那怎樣才能算是“一等公民”呢?簡單來說就是和其他資料型別待遇差不多,不會被區別對待。比如說:可以把函式賦值給變數或者結構體欄位;可以把函式存在array、map等資料結構裡;可以把函式作為引數傳遞給其他函式;也可以把函式當其他函式的返回值返回;等等。下面結合Cosmos-SDK原始碼舉幾個具體的例子。

普通變數

以auth模組的AccountKeeper為例,這個Keeper提供了一個GetAllAccounts()方法,返回系統中所有的賬號:

// 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()方法。

結構體欄位

Cosmos-SDK提供了BaseApp結構體,作為構建區塊鏈App的"基礎":

// 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的行為。下面是這些函式的型別定義:

// 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裡:

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寫作,轉載無需授權。

相關文章