Original text: hedzr. Making. IO/golang/fp/g…
Vomit vomit first
In general, Golang’s Functional programming is bad. On the surface, Golang lacks some necessary grammatical sugar; In essence, badness stems from its lack of high-level abstraction capabilities, as does the absence of generics.
Bad shape
What’s the ugliness? Here’s an example:
func main(a) {
var list = []string{"Orange"."Apple"."Banana"."Grape"}
// we are passing the array and a function as arguments to mapForEach method.
var out = mapForEach(list, func(it string) int {
return len(it)
})
fmt.Println(out) // [6, 5, 6, 5]
}
// The higher-order-function takes an array and a function as arguments
func mapForEach(arr []string, fn func(it string) int) []int {
var newArray = []int{}
for _, it := range arr {
// We are executing the method passed
newArray = append(newArray, fn(it))
}
return newArray
}
Copy the code
Very good. The packing looks good, doesn’t it? Fp forms are also more comfortable to look at. I want to… B: Well, I want to make it generic and give it to others. So bad, support int64 needs to be like this:
func mapInt64ForEach(arr []int64, fn func(it int64) int) []int {
var newArray = []int{}
for _, it := range arr {
// We are executing the method passed
newArray = append(newArray, fn(it))
}
return newArray
}
Copy the code
This is just the beginning, you start as bool, uint64… Write out n versions, and remember, the function name has to change as well.
Contrast: C++ template implementation
So I would say that Golang’s higher-order function, functional, is actually quite naturally malformed.
God knows, I use Golang for architecture most of the time now, and yet THERE’s always a sense of unspeakable disappointment. If in C++11:
class Print {
public:
void operator(a)(int elem) const {
std::cout << elem << ""; }};func a(a){
std::vector<int> vect;
for (int i=1; i<10; ++i) {
vect.push_back(i);
}
Print print_it;
std::for_each (vect.begin(), vect.end(), print_it);
std::cout << std::endl;
}
Copy the code
To save bytes, I borrowed stdlib’s for_each instead of implementing a copy myself, but foreach’s implementation is really simple.
The point is, now THAT I’m going to manipulate strings, ALL I have to do is rewrite Print, I don’t have to do n copies of the for_each implementation. If necessary, I can implement a generic Print template class, so I don’t have to re-implement a copy of anything, just use it.
The finale
Golang Functional Programming has not yet begun to study the beautiful place, but first derogatory, really sorry!
Ok, now for a good use of functional.
Although functional is not easy to generically reuse, it is the best way to improve the structure, appearance, content, and quality of an application in a concrete type or in an indirect generic model abstracted through an interface.
So you will see the functional pattern widely adopted in mature libraries, both standard and third-party.
So, these applications are summarized and presented below, with the goal of providing a list of best practices that will hopefully help improve your specific coding skills.
What is Functional Programming
First we need to look at what is higher-order functional programming? Functional Programming is generally translated as Functional Programming (based on the lambda calculus 3).
Functional programming, it is to point to ignore (usually) is not allowed in variable data (to avoid it can change the data of the marginal effect), and ignore the program execution status (do not allow the implicit, hidden and not visible state), through the function as the reference, functions as the way to the return value is calculated, through continuous propulsion (iteration and recursion) this calculation, A programming paradigm that gets output from input. In the functional programming paradigm, there are no concepts common to procedural programming: statements, procedural controls (conditions, loops, and so on). In addition, in the functional programming paradigm, there is Referential Transparency. This concept implies that the operation of a function is only related to the input parameters. If the input parameters are the same, the output parameters must always be the same, and the transformation performed by the function itself (regarded as F (x)) is determined.
Incidentally, Currization 4 is an important theory and technique in functional programming. Completely discarding the if, then, while stuff of procedural programming, completely iterating functions are generally favored by proponents of pure functions, while things like Start(…) .Then(…) .Then(…) .Else(…) .Finally(…) The.stop () style is often seen as pagan.
It’s really interesting. Fundamentalism is established and exists everywhere.
Characterization of
To summarize, functional programming has the following characteristics:
- No Data mutations has No Data variability
- No implicit state indicates No implicit state
- No side effects
- Pure functions Only Pure functions with no procedure controls or statements
- First-class function
- The first-class citizen function has first-class citizenship
- Higher-order functions, which can appear anywhere
- Closures – Instances of functions with superior environment capture capabilities
- The calculus of Currying 4 – allows multiple entries into a single one, etc
- Recursion – nested iteration of a function to find a value. There is no concept of procedure control
- Lazy Evaluation/Evaluation strategy Lazy Evaluation – Delay Evaluation of captured variables until they are used
- Referential transparency – The expression must have the same value for the same input, and its evaluation must have no side effects
Because the focus is not on advanced FP programming and related learning, there is no in-depth discussion of purebred FP Coriolization transformations, which are more difficult for traditional C programmers to turn around.
Functional programming in Golang: Higher-order functions
In Golang, the concept of functional programming has been repackaged and reinterpreted as everything is a function, functions are values, and so on. Therefore, functional programming may be avoided in this article and will be replaced with higher-order functional programming.
It is important to note that functional programming is not just higher-order functional programming, nor is higher-order functional programming inclusive of functional programming. These are two different concepts that overlap in their representations. For Golang, there is neither pure functional programming, nor pure object-oriented programming. Golang takes a different, slightly extreme approach to both, and also incorporates some progressive theory with the time. Of course, for the most part, we agree that Golang has adopted its own philosophy to support such multi-paradigm programming.
In Golang, higher-order functions are often the key glue for implementing an algorithm.
For example,
- The basic closure structure
- recursive
- Functor/operator
- The inertia calculation
- Variable parameters: Functional Options
The basic Closure structure
In a programming language where functions, higher-order functions, are first-order citizens, you can, of course, assign a function to a variable, copy it to a member, pass it as an argument (or one of) to another function, and return (or one of) to another function.
Golang has that support.
However, Golang does not have the syntactic sugar of anonymous function enlargement or reduction. In fact, Golang does not have most syntactic sugar, as determined by its design philosophy. So you have to write code that’s a little bit verbose, and you can’t make the syntax concise. At this point, C++ is able to abbreviateoperator () and to abbreviateclosure functions using the [] capture syntax. Java 8 has come a long way with the simplified syntax for anonymous closures, but not quite as far as Kotlin, Kotlin takes it a step further by allowing the last closure of a function call to be expanded beyond the calling syntax and in the form of a statement block:
fun invoker(p1 string, fn fun(it int)) {
// ...
}
invoker("ok") { /* it int */ ->
// ...
}
Copy the code
In Golang, however, you need to fully prototype higher-order functions, even if you define type for them:
type Handler func (a int)
func xc(pa int, handler Handler) {
handler(pa)
}
func Test1(a){
xc(1.func(a int){ // <- Write the prototype again honestly
print (a)
})
}
Copy the code
It’s worth noting that library authors and library users alike are painstakingly searching for and modifying Handler prototypes whenever they change.
That’s right, you’ll learn an important principle of programming here, interface design must consider robustness. As long as the interface is solid, there’s certainly no possibility that the Handler prototype will need to be tweaked, right? Ha ha.
Puking is not my cup of tea, so leave it at that.
Transport operator Functor
The operator is usually a simple function (but not necessarily so). The total control part replaces different operators to achieve the actual implementation algorithm of the replacement business logic:
func add(a, b int) int { return a+b }
func sub(a, b int) int { return a-b }
var operators map[string]func(a, b int) int
func init(a){
operators = map[string]func(a, b int) int {
"+": add,
"-": sub,
}
}
func calculator(a, b int, op string) int {
iffn, ok := operators[op]; op && fn! =nil{
return fn(a, b)
}
return 0
}
Copy the code
Recursive Recursion
Fibonacci, factorial, Hanoi tower, fractal are typical recursive problems.
In programming languages that support recursion, how to use recursion is often a difficult knowledge. In terms of personal experience, thinking day and night, suddenly enlightened is the inevitable process of fully mastering recursion.
In functional programming, recursion is an overarching concept. This is represented as a higher-order function return value in Golang.
The following example simply implements the factorial operation:
package main
import "fmt"
func factorial(num int) int {
result := 1
for ; num > 0; num-- {
result *= num
}
return result
}
func main(a) {
fmt.Println(factorial(10)) / / 3628800
}
Copy the code
But we should re-implement it in Functional Programming style:
package main
import "fmt"
func factorialTailRecursive(num int) int {
return factorial(1, num)
}
func factorial(accumulator, val int) int {
if val == 1 {
return accumulator
}
return factorial(accumulator*val, val- 1)}func main(a) {
fmt.Println(factorialTailRecursive(10)) / / 3628800
}
Copy the code
Most modern programming languages optimize tail-recursion implicitly at compile time, which is an important optimization point in the compilation principle: tail-recursion can always degenerate into a loop structure that does not require nested function calls.
Therefore, we have made some changes above, so that the factorial operation can be implemented in Functional mode, which makes it readable and avoids the stack consumption problem in the case of nested function calls.
Use recursion of higher order functions
Borrowing from Fibonacci’s implementation, we simply return a function as an example to achieve recursion:
package main
import "fmt"
func fibonacci(a) func(a) int {
a, b := 0.1
return func(a) int {
a, b = b, a+b
return a
}
}
func main(a) {
f := fibonacci()
for i := 0; i < 10; i++ {
fmt.Println(f())
}
}
// Outputs: 1 1 2 3 5 8 13 21 34 55
Copy the code
Delayed calculation
An important use of high-order/anonymous functions is for capture variables and delayed calculations, also known as Lazy calculations.
In the following example,
func doSth(a){
var err error
defer func(a){
iferr ! =nil {
println(err.Error())
}
}()
// ...
err = io.EOF
return
}
doSth() // printed: EOF
Copy the code
In the higher-order function of defer, the ERR variables in the outer scope were captured, and the err Settings during doSth’s entire run cycle were eventually calculated correctly in the body of the defer function. Without capture and delay, the access to err in the higher-order function body would only get nil because that is the value of err at the capture time. Note that we have used defer to demonstrate this to reduce the size of the sample code, but actually use Go Routines to get the same effect; in other words, access to the external scope is evaluated dynamically and latently in higher-order functions.
Exception: loop variables
There is, of course, a famous pitfall here: loop variables are not evaluated lazily (loop variables are in some ways non-existent pseudo-variables because loop optimization always happens).
func a(a){
for i:=0; i<10; i++ {
go func(a){
println(i)
}()
}
}
func main(a){ a() }
// 1. The result will be all zeros
// 2. In the new version of Golang, it will not compile, and the error is:
// loop variable i captured by func literal
Copy the code
To get intuitive results, you need to pass in the loop variable:
func a(a){
for i:=0; i<10; i++ {
go func(ix int){
println(ix)
}(i)
}
}
Copy the code
I’ll be honest with you. I stepped on this pit, and I just found it. Finding such a bug in a large system can be exhausting. Does it mean you’re not good at programming? Don’t worry, it’s not that I’m lowering my standards because I’ve eaten it myself, it’s that Golang is disgusting.
Functional Options
Sooner or later, as a library author, you have to deal with interface changes. Or because of the external environment changes, or because the function upgrade and expand the denotation, or need to be scrapped imperfect design in the past, or because individual level, no matter what kind of reason, you may find that must modify the original interface, replace it with a more perfect new interface.
The old way
Imagine an early class library:
package tut
func New(a int) *Holder {
return &Holder{
a: a,
}
}
type Holder struct {
a int
}
Copy the code
Later, we found that we needed to add a Boolean quantity b, so we changed the tuT library to:
package tut
func New(a int, b bool) *Holder {
return &Holder{
a: a,
b: b,
}
}
type Holder struct {
a int
b bool
}
Copy the code
After a few days, now we think it is necessary to add a string variable, the TUt library has to be changed to:
package tut
func New(a int, b bool, c string) *Holder {
return &Holder{
a: a,
b: b,
c: c,
}
}
type Holder struct {
a int
b bool
c string
}
Copy the code
Imagine how many MMPS a user of the TUT library would have to throw when faced with three upgrades to interface New().
We need Functional Options to save it.
A new way
Suppose we implemented the first version of TUT like this:
package tut
type Opt func (holder *Holder)
func New(opts ... Opt) *Holder {
h := &Holder{ a: - 1,}for _, opt := range opts {
opt(h)
}
return h
}
func WithA(a int) Opt {
return func (holder *Holder) {
holder.a = a
}
}
type Holder struct {
a int
}
/ /...
// You can:
func vv(a){
holder := tut.New(tut.WithA(1))
// ...
}
Copy the code
Similarly, after a requirement change occurs, we add B and C to the existing version, so the tuT now looks like this:
package tut
type Opt func (holder *Holder)
func New(opts ... Opt) *Holder {
h := &Holder{ a: - 1,}for _, opt := range opts {
opt(h)
}
return h
}
func WithA(a int) Opt {
return func (holder *Holder) {
holder.a = a
}
}
func WithB(b bool) Opt {
return func (holder *Holder) {
holder.b = b
}
}
func WithC(c string) Opt {
return func (holder *Holder) {
holder.c = c
}
}
type Holder struct {
a int
b bool
c string
}
/ /...
// You can:
func vv(a){
holder := tut.New(tut.WithA(1), tut.WithB(true), tut.WithC("hello"))
// ...
}
Copy the code
I don’t have to go through the example code line by line because the code isn’t very complex. You’ll get an intuitive sense that the legacy client-side code of the original TUT (such as Vv ()) can actually remain completely unchanged and respond transparently to updates to the TUT library itself.
The features and functions of this coding paradigm include:
A. When instantiating the Holder, we can now use as many variadic parameters as we want for different data types.
B. With the help of the existing paradigm model, we can also implement arbitrary complex initialization operations for different build operations for the Holder.
C. Since it is a paradigm, its readability and extensibility need to be studied — obviously, the current paradigm gets high marks.
D. In major version upgrades, New(…) The interface stability is pretty good, and no matter how you tweak the built-in algorithm and its implementation, nothing needs to change for the caller of such a third-party library.
summary
This article refers to some knowledge mentioned in DCODE7, in addition, 7 Easy Functional Programming Techniques in Go 8 also introduces a lot of FP knowledge.
This paper does not intend to expand on FP, because in the author’s understanding, it is meaningful to discuss FP in Lisp, Haskell and other languages. Although Golang has many tendencies towards FP, it is of course procedural PL, which only says that FP has strong support.
But these nuanced distinctions are merely academic. Therefore, this paper only summarizes some relevant idiomatic methods in the aspect of concrete implementation.
Maybe in the future, I will generalize about this aspect, maybe I will have a deeper understanding.
References
🔚
- Zh.wikipedia.org/wiki/%CE%9B…↩
- En.wikipedia.org/wiki/Curryi…↩
- Arschles.com/blog/functi…↩
- 7 Easy functional programming techniques in Go↩