banner
biuaxia

biuaxia

"万物皆有裂痕,那是光进来的地方。"
github
bilibili
tg_channel

Functional Programming: Anonymous and Closure

title: Functional Programming: Anonymous Functions and Closures
date: 2022-07-03 21:38:00
toc: false
index_img: http://api.btstu.cn/sjbz/?lx=m_dongman&cid=8
category:

  • Go
    tags:
  • Java
  • Creation
  • Support
  • Environment
  • Request
  • Strings
  • Different
  • Occurrence
  • Print
  • add
  • Leave
  • JavaScript
  • Method
  • Function

Definition and Usage of Anonymous Functions#

An anonymous function is a function declaration without specifying a function name (in contrast, a named function is called a named function). Many programming languages, such as PHP and JavaScript (think about the implementation of Ajax requests), support and implement anonymous functions. Go language also provides support for anonymous functions, and the syntax is similar to other languages:

func(a, b int) int { 
    return a * b
}

Like other languages, anonymous functions in Go can be assigned to a variable or executed directly:

// 1. Assigning the anonymous function to a variable
mul := func(a, b int) int {
    return a * b
}

// Calling the anonymous function mul
fmt.Println(mul(1, 2))  

// 2. Calling the anonymous function directly during definition
func(a, b int) {
    fmt.Println(a * b)
} (1, 2) 

Anonymous Functions and Closures#

To answer this question, we need to understand the concept of closures.

A closure refers to a function that references free variables (variables not bound to a specific object, usually defined outside the function). The referenced free variables will exist together with this function, even if they have left the context in which they were created (such as being passed to other functions or objects). In simple terms, "closure" means "enclosing external state", which means that even if the external state becomes invalid, the closure still retains a copy of the variable referenced from the outside.

Obviously, closures can only be implemented through anonymous functions. We can consider closures as stateful anonymous functions. Conversely, if an anonymous function references an external variable, it forms a closure.

The value of closures lies in their ability to act as function objects or anonymous functions that hold external variables. For the type system, this means representing both data and code. Languages that support closures treat functions as first-class objects (also known as first-class citizens, first-class entities, etc.), and Go language is no exception. This means that Go functions and ordinary Go data types (integers, strings, arrays, slices, maps, structures, etc.) have equal status. They can be assigned to variables, passed as arguments to other functions, and can also be dynamically created and returned by functions.

Note: The so-called first-class objects refer to entities that can be created at runtime and passed as arguments to other functions or assigned to variables. In most languages, numbers and basic types are first-class objects. In programming languages that support closures (such as Go, PHP, JavaScript, Python, etc.), functions are also first-class objects. In languages like C and C++ that do not support anonymous functions, functions cannot be created at runtime, so functions are not first-class objects.

In simple terms: closures and anonymous functions appear together, and if they reference external variables, they are closures! The following code can help deepen the understanding!

package main

import "fmt"

var iotaVal int

func iotaFunc() int {
	val := 1 << (10 * iotaVal)
	iotaVal++
	return val
}

func main() {
	fmt.Printf("%d: KB -> %-14d byte \n", iotaVal, iotaFunc())
	fmt.Printf("%d: MB -> %-14d byte \n", iotaVal, iotaFunc())
	fmt.Printf("%d: GB -> %-14d byte \n", iotaVal, iotaFunc())
	fmt.Printf("%d: TB -> %-14d byte \n", iotaVal, iotaFunc())
	fmt.Printf("%d: PB -> %-14d byte \n", iotaVal, iotaFunc())
}

Output:

1: KB -> 1              byte
2: MB -> 1024           byte
3: GB -> 1048576        byte
4: TB -> 1073741824     byte
5: PB -> 1099511627776  byte

Common Use Cases of Anonymous Functions#

Now let's look at some typical use cases of anonymous functions in Go.

Ensuring the Safety of Local Variables#

Local variables declared within an anonymous function cannot be modified from the outside, ensuring safety (similar to private attributes of a class):

var j int = 1

f := func() {
    var i int = 1
    fmt.Printf("i, j: %d, %d\n", i, j)
}

f()
j += 2
f()

The above code will output:

i, j: 1, 1
i, j: 1, 3

In the above example, the anonymous function references an external variable, so it is also a closure. The variable f points to the closure that references local variables i and j. i is defined within the closure and its value is isolated and cannot be modified from the outside. On the other hand, j is defined outside the closure, so it can be modified from the outside. The closure only holds a reference to it.

Using Anonymous Functions as Function Parameters#

In addition to assigning anonymous functions to regular variables, they can also be passed as parameters to functions and called, just like ordinary data types:

add := func(a, b int) int {
    return a + b
}

// Passing a function type as a parameter
func(call func(int, int) int) {
    fmt.Println(call(1, 2))
}(add)

When declaring a function type, we need to strictly specify the type of each parameter and the return value. This is a complete function type. Therefore, the function add corresponds to the function type func(int, int) int.

We can also extract the second anonymous function outside the main function and make it a named function handleAdd. Then, we can define different addition algorithm implementation functions and pass them as parameters to handleAdd:

func main() {
	// Regular addition operation
	add1 := func(a, b int) int {
		return a + b
	}

	// Define multiple addition algorithm implementations
	base := 10
	add2 := func(a, b int) int {
		return a*base + b
	}

	handleAdd(1, 2, add1) // 3
	handleAdd(1, 2, add2) // 1*10 + 2 = 12
}

// Passing an anonymous function as a parameter
func handleAdd(a, b int, call func(int, int) int) {
	fmt.Println(call(a, b))
}

The output of the above code is:

3
12

In this example, the second anonymous function add2 references an external variable base, forming a closure. When calling the outer function handleAdd, the closure add2 is passed as a parameter. When the closure add2 is executed in the outer function, although the scope has left the main function, it can still access the variable base.

In this way, we can implement multiple different addition algorithm implementations through one function, improving code reusability. Based on this feature, we can implement more complex business logic. For example, the underlying route handler in Go's official net/http package is implemented in this way:

// HandleFunc registers the handler function for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	DefaultServeMux.HandleFunc(pattern, handler)
}

Returning Anonymous Functions from Functions#

Finally, anonymous functions can also be returned from functions:

// Returning a function as the return value type
func deferAdd(a, b int) func() int {
    return func() int {
        return a + b
    }
}

func main() {
    // The return value is an anonymous function
    addFunc := deferAdd(1, 2)
    // The addition operation is performed here
    fmt.Println(addFunc())
}

The output of the above code is:

3

In the above example, calling the deferAdd function returns an anonymous function. However, this anonymous function references the parameters passed into the outer function, forming a closure. As long as this closure exists, the variables it holds will also exist, even if they are outside the scope of the deferAdd function, they can still be accessed.

In addition, the closure is not executed when calling the deferAdd function. The closure is only executed when addFunc() is called, and the business logic inside the closure (in this case, addition operation) is executed. Therefore, by declaring the return value as a function type, we can achieve delayed execution of business logic, allowing developers to have complete control over the execution timing.

Try reading the code and obtaining the program's execution result:

// Returning a function as the return value type
func deferAdd(a, b int) func() int {
	return func() int {
		a++
		return a + b
	}
}

func main() {
	// The return value is an anonymous function
	addFunc := deferAdd(1, 2)
	// The addition operation is performed here
	fmt.Println(addFunc())
	fmt.Println(addFunc())
}
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.