Functional paradigm on Go: basic techniques





Hello everyone, we remind you that this month in OTUS a new set will start at the Golang Developer course. Despite some hate from a previous article on Golang, our freelance writer decided to take the chance to continue the series of articles devoted to this language. We will try to go through this thin ice once more, relying on what Golang seems to rely on - the functional paradigm.






We remind you that this article is a certain material for “extracurricular reading” and is not related to the curriculum, which can be found here .



It is clear that for professional programmers in other languages ​​Golang calls

irritation is like an adult compiled language, but the concept of classes and inheritance is absent in principle (although OOP is implemented in a language, albeit in a rather unusual way, through a system of structures and interfaces). However, today we look at the main implementations of familiar constructions in the functional paradigm and try to explain both them and the language syntax itself.







Now there is a lot of hype around the functional paradigm (FP). However, it is also not a panacea for all problems, and also has its pros and cons.



Briefly about what a functional paradigm is



The functional paradigm came to programming from mathematics. It forms the following requirements for the program:





What does this give us?



Our functions work without third-party effects. In other words, the function should only return a value and should not affect any external data.



Use of pure functions. They increase the retest reliability of functions regardless of incoming data - in other words, programs become more reliable for testing and their results become more predictable.



So, what opportunities does Golang have for implementing a functional paradigm:



First class functions



First-class functions are available in many programming languages. The reader of this article most likely already knows their concept from such widespread JavaScript, but I will repeat it again. The functions of the first class (high order function) are functions that can return another function as knowledge, take a function as an argument, and pass the value of the function to another variable.

Let's agree from the very beginning : to save space, I threw out the first two lines of the code that is presented here: 'package main' and import 'import "fmt"'. But to run the code on your machine, remember to add them).




func main() { var list = []int{15, 16, 45, 34} //      var out = forEach(list, func(it int) int { //      //forEach   ""  return (it * it) //      }) fmt.Println(out) // [225, 256, 2025, 1156] fmt.Println(list) //      } func forEach(arr []int, fn func(it int) int) []int { //      ,   ,     var newArray = []int{} //     ""   for _, it := range arr { newArray = append(newArray, fn(it)) //      for } return newArray }
      
      







In fact, it is not at all necessary to invent your own map



or foreach



from scratch. There are many libraries that implement this, it remains only to connect them. For example, this one .



Closures and currying functions



There are short circuits in many modern programming languages. Closures are a function that refers to the free scope variables of its parent function. Function currying is a change of function from the form func(a,b,c)



to the form func(a)(b)(c)



.



Here is an example of closures and currying in Go:



 //  func multiply(x int) func(y int) int { //    return func(y int) int { //   ,       JS return x * y } } func main() { //     var mult10 = multiply(10) var mult15 = multiply(15) fmt.Println(mult10(5)) //50 fmt.Println(mult15(15))//225 }
      
      







Pure functions



As we said before, pure functions are those that return values ​​that are associated only with arguments coming to the input and not affecting the global state.



Here is an example of a failed, dirty function:



 var arrToSave = map[string]int{} //map -    -   Golang func dirtySum(a, b int) int { c := a + b arrToSave[fmt.Sprintf("%d", a, b)] = c //   ,  "%d" -       return c }
      
      





Here, our function should accept to work as predictably as possible:



 func simpleSum(x, y int) int { return x + y } func main() { fmt.Printf("%v", dirtySum(13, 12)) //      //   ""      fmt.Printf("%v", simpleSum(13, 12)) }
      
      





“Somehow the recursion enters the bar, and no one else enters the bar”

From the collection of unfunny jokes.



Recursion



In a functional paradigm, it is customary to give preference to recursion - for purity and transparency, instead of using simple iteration through for



.



Here is an example of calculating factorial using the imperative and declarative paradigm:



 func funcFactorial(num int) int { if num == 0 { return 1 } return num * funcFactorial(num-1) } func imperativeFactorial(num int) int { var result int = 1 for ; num > 0; num-- { //    for result *= num } return result } func main() { fmt.Println(funcFactorial(20)) //        fmt.Println(imperativeFactorial(20)) //      }
      
      







Now the recursion function works quite inefficiently. Let's try to rewrite it a bit in order to optimize the speed of its calculation:



 func factTailRec(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() { fmt.Println(factTailRec(20)) // 2432902008176640000 }
      
      







Our factorial computing speed has increased slightly. I will not give benchmarks).



Unfortunately, Go does not implement optimization of recursion out of the box, so you have to optimize the tail of recursion yourself. Although, no doubt, a useful library on this topic can certainly be found. For example, there is such a “Loadash for Golang” cool on this topic .



Lazy computing



In programming theory, lazy computing (also known as “deferred computing”) is the process of deferring computing until it is needed. Golang has no support for lazy computing right out of the box so we can only simulate this:



 func mult(x, y int) int { fmt.Println(" ") return x * x. } func divide(x, y int) int { fmt.Println(" ") return x / y //    -  } func main() { fmt.Println(multOrDivide(true, mult, divide, 17, 3)) //   ""   ,   1  , //         fmt.Println(multOrDivide(false, mult, divide, 17, 3)) } //  if - else    ""  func multOrDivide(add bool, onMult, onDivide func(t, z int) int, t, z int) int { if add { return onMult(t, z) } return onDivide(t, z) }
      
      







Most often, “emulated” lazy expressions are not worth it because they overly complicate the code, but if your functions are quite difficult to manage, then you should use this method. But you can turn to other solutions, for example, to these .







That's all. We only got an introduction to the functional paradigm on Golang. Unfortunately, part of the possibilities had to be simulated. Part, fully developed functional techniques, such as monads, were not included here, because there are a lot of articles about them in Go on the hub Much can still be improved in the language itself, for example with the next big version (GO 2) generics are expected to appear in the language. Well, we will wait and hope).



All Articles