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#Lexical_scoping))內的變量,那麼匿名函數就能夠成爲閉包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寫做,轉載無需受權。

相關文章
相關標籤/搜索