Go Functional Programming and Its Application in Tendermint/Cosmos-SDK

0.192字数 1138阅读 38

“Functional Programming” (https://en.wikipedia.org/wiki/Functional_programming) is actually a very old concept, but is gaining popularity in recent years. Even many old languages such as Java have increased their support for functional programming. Combined with the source code of Temdermint/Cosmos-SDK, this article explains the most important concepts in functional programming, and how to use Golang for functional programming. The main content of this article is outlined as below:

  • First-class Function

  • Higher-order Function

  • Anonymous Function

  • Closure

  • λ Expression

First-class Function

A programming language is said to have first-class functions if it treats functions (or methods, procedures, subroutines, etc., and they are called differently in different languages) as first-class citizens (First-class Function ). So what are “first-class citizens”? In short, they can be treated the same way as any other data types.For example, you can assign a function to a variable or to a structure and a field.You can store a function in data structures such as an array, a map, etc.You can pass a function as a parameter to another function. You can also return a function as a return value of another function. Such examples can go on and on. Here are a few specific examples in combination with the Cosmos-SDK source code.

Ordinary Variable

Take the Account Keeper (https://github.com/cosmos/cosmos-sdk/blob/release/v0.35.0/x/auth/keeper.go) in the auth module as an example. This Keeper provides a GetAllAccounts method to return all accounts in the system.


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

}

As you can see from the code, a function is assigned to an ordinary variable “appendAccount” which is passed to the “IterateAccounts()” method.

Structures and Fields

Cosmos-SDK provides “BaseApp” (https://github.com/cosmos/cosmos-sdk/blob/release/v0.35.0/baseapp/baseapp.go) structures as the “basis” for building blcokchain Apps.


// BaseApp reflects the ABCI application implementation.

type BaseApp struct {

// Omit irrelevant fields

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

// Omit irrelevant fields

}

This structure defines a large number of fields among which 6 are function types. These functions act as callbacks or hooks that affect the behavior of a particular blockchain app. Here are the type definitions for these functions.


// 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 Factor

Cosmos-SDK provides the [Int] (https://github.com/cosmos/cosmos-sdk/blob/release/v0.35.0/types/int.go) type to represent 256-bit integers. The following unit test is extracted from the [int_test.go] (https://github.com/cosmos/cosmos-sdk/blob/release/v0.35.0/types/int_test.go#L186) file, demonstrating how to store functions in a 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) // Calling functions

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 Value

Take BaseApp as an example again. This package defines a [queryRouter] (https://github.com/cosmos/cosmos-sdk/blob/release/v0.35.0/baseapp/queryrouter.go#L15) structure to “query route”.


type queryRouter struct {

routes map[string]sdk.Querier

}

As you can see from the code, the “routes” field of this structure is a “map”, and the value is a function type which is defined in the [queryable.go] (https://github.com/cosmos/cosmos-sdk/blob/release/ Definition in v0.35.0/types/queryable.go) file.


// 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)

Examples showing a function as a parameter and a return value of another function are given in the next section.

Higher-order Function

“Higher-order Function” (https://en.wikipedia.org/wiki/Higher-order_function)) sounds fancy, but its concept is actually simple. If a function takes another function as an argument, or a return value is a function type, then this function is a higher-order function. Take the above-mentioned “IterateAccounts()” method of “AccountKeeper” as an example.


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()

}

}

As its second “argument” is a function type, it is a higher-order function (or higher-order method if more stringent). Similarly, there is a NewAnteHandler() function in the auth module.


// 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) {

// code omitted

 }

}

The “return value” of this function is a function type, so it’s also a higher-order function.

Anonymous Function

The “NewAnteHandler()” function as shown above has its own name, but it’s more convenient to use the “anonymous function” ([Anonymous Function](https://en.wikipedia.org /wiki/Anonymous_function))when defining and using higher-order functions. For example, the return value in the “NewAnteHandler()” function is an anonymous function. Anonymous functions are very common in Golang code. For example, many functions need to use a keyword such as “defer” to secure that certain logic is postponed and executed before functions return. It is convenient to use anonymous functions at this time. Let’s take the “NewAnteHandler” function as an example again.


// 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) {

// 前面的代码省略 previous code omitted

// 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)

}

}

}() // self-executing anonymous functions

// following code omitted

}

}

For example, how to use a keyword such as “go” to execute goroutine. Please refer to the “TrapSignal()” function in the [cosmos-sdk/server/util.go] file (https://github.com/cosmos/cosmos-sdk/blob/release/v0.35.0/server/util.go#L211) for specific examples.


// 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))

}

}()

}

Closure

If an anonymous function can capture the variables in the “lexical scope” ([Lexical Scope] (https://en.wikipedia.org/wiki/Scope_(computer_science)#Lexical_scoping)), such anonymous function can be a “closure” (Closure). Closures are omnipresent in the Cosmos-SDK/Temdermint code. Let’s take the [NewHandler()](https://github.com/cosmos/cosmos-sdk/blob/release/v0.35.0/x/ Bank/handler.go#L8) function in the bank module as an example.


// 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 capture k

case MsgMultiSend:

return handleMsgMultiSend(ctx, k, msg) // 捕捉到k capture k

default:

errMsg := "Unrecognized bank Msg type: %s" + msg.Type()

return sdk.ErrUnknownRequest(errMsg).Result()

}

}

}

It can easily tell from the code that the anonymous function returned by “NewHandler()” captures an argument “k” of a peripheral function, so the returned is actually a closure.

λ Expression

Anonymous functions are also called “λ expressions” (Lambda Expression), but in most cases we refer to a “more concise” way of writing when we say λ expression. Take the above-mentioned “TestImmutabilityAllInt()” function as an example. Here are part of its code.


ops := []func(*Int){

func(i *Int) { _ = i.Add(randint()) },

func(i *Int) { _ = i.Sub(randint()) },

func(i *Int) { _ = i.Mul(randint()) },

// other code omitted

}

From this simple example, we can easily tell that there is still redundancy in writing Go anonymous functions. If you translate the above code into Python, it looks like as below.


ops = [

 lambda i: i.add(randint()),

 lambda i: i.sub(randint()),

 lambda i: i.mul(randint()),

 # other code omitted

]

If it is translated into Java8, it looks like as below.


IntConsumer[] ops = new IntConsumer[] {

 (i) -> {i.add(randint())},

 (i) -> {i.sub(randint())},

 (i) -> {i.mul(randint())},

 // other code omitted

}

As you can see, it’s more concise to write in either Python or Java than in Go. Such concise way of writing has its advantage when anonymous functions/closures are short. Currently, there is a [Go2 proposal] (https://github.com/golang/go/issues/21498), suggesting that Golang add this concise writing, but it’s unknown if this will be approved and when it can be added.

Summary

Although Golang is not a pure language for functional programming, it’s very easy to use as it supports first-class functions/higher-order functions, anonymous functions and closures.

This article was written by Team Chase [CoinEx Chain] (https://www.coinex.org/). Authorization not needed for repost.

推荐阅读更多精彩内容