Paradigma funcional em movimento: técnicas básicas



Olá pessoal, lembramos que este mês no OTUS um novo conjunto será iniciado no curso Golang Developer . Apesar do ódio ao artigo anterior sobre Golang, nosso escritor freelancer decidiu correr o risco de continuar uma série de artigos dedicados a esse idioma. Vamos tentar passar por esse gelo fino novamente, contando com o que Golang parece confiar - o paradigma funcional.



Lembramos que este artigo é um tipo de material para "leitura extracurricular" e não está relacionado ao programa do curso, que pode ser encontrado aqui .

É claro que programadores profissionais em outros idiomas Golang chama
irritação é como uma linguagem compilada para adultos, mas o conceito de classes e herança está ausente em princípio (embora o OOP seja implementado em uma linguagem, embora de maneira bastante incomum, através de um sistema de estruturas e interfaces). No entanto, hoje analisamos as principais implementações de construções familiares no paradigma funcional e tentamos explicar elas e a própria sintaxe da linguagem.



Agora, há muito hype em torno do paradigma funcional (FP). No entanto, também não é uma panacéia para todos os problemas, e também tem seus prós e contras.

Brevemente sobre o que é um paradigma funcional


O paradigma funcional veio para a programação da matemática. Ele forma os seguintes requisitos para o programa:

  • Nenhuma alteração nos dados existentes.
  • Não há estado oculto.

O que isso nos dá?

Nossas funções funcionam sem efeitos de terceiros. Em outras palavras, a função deve retornar apenas um valor e não deve afetar nenhum dado externo.

Usando recursos puros. Eles aumentam a confiabilidade das funções de reteste, independentemente dos dados recebidos - em outras palavras, os programas se tornam mais confiáveis ​​para testes e seus resultados se tornam mais previsíveis.

Então, quais são as possibilidades da Golang para implementar o paradigma funcional:

Funções de primeira classe


Funções de primeira classe estão disponíveis em várias linguagens de programação. O leitor deste artigo provavelmente já conhece seu conceito a partir de um JavaScript tão difundido, mas vou repetir novamente. As funções da primeira classe (função de alta ordem) são funções que podem retornar outra função como conhecimento, assumir uma função como argumento e passar o valor da função para outra variável.
Vamos concordar desde o início : para economizar espaço, joguei as duas primeiras linhas do código que é apresentado aqui: 'package main' e import 'import "fmt"'. Mas, para executar o código em sua máquina, lembre-se de adicioná-los).


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 } 


De fato, não é necessário inventar seu próprio map ou foreach do zero. Existem muitas bibliotecas que implementam isso, resta apenas conectá-las. Por exemplo, este .

Fechos e funções de curry


Existem curtos-circuitos em muitas linguagens de programação modernas. Encerramentos são uma função que se refere às variáveis ​​de escopo livre de sua função pai. Função currying é uma mudança de função da forma func(a,b,c) para a forma func(a)(b)(c) .

Aqui está um exemplo de encerramentos e currying no 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 } 


Funções puras


Como dissemos antes, funções puras são aquelas que retornam valores associados apenas a argumentos que entram e não afetam o estado global.

Aqui está um exemplo de uma função suja com falha:

 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 } 

Aqui, nossa função deve aceitar trabalhar da maneira mais previsível possível:

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

“De alguma forma, a recursão entra no bar e ninguém mais entra no bar”
Da coleção de piadas sem graça.

Recursão


Em um paradigma funcional, é habitual dar preferência à recursão - por pureza e transparência, em vez de usar iteração simples por meio for .

Aqui está um exemplo de cálculo de fatorial usando o paradigma imperativo e declarativo:

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


Agora, a função de recursão funciona de maneira bastante ineficiente. Vamos tentar reescrevê-lo um pouco para otimizar a velocidade de seu cálculo:

 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 } 


Nossa velocidade de computação fatorial aumentou um pouco. Não darei benchmarks).

Infelizmente, o Go não implementa a otimização de recursão pronta para uso, portanto, você deve otimizar a cauda da recursão. Embora, sem dúvida, uma biblioteca útil sobre esse tópico possa certamente ser encontrada. Por exemplo, existe um "Loadash for Golang" interessante sobre esse tópico .

Computação preguiçosa


Na teoria da programação, a computação lenta (também conhecida como “computação diferida”) é o processo de diferir a computação até que seja necessária. Golang não tem suporte para computação lenta imediatamente, então podemos simular isso:

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


Na maioria das vezes, expressões preguiçosas “emuladas” não valem a pena, porque complicam demais o código, mas se suas funções são bastante difíceis de gerenciar, você deve usar esse método. Mas você pode recorrer a outras soluções, por exemplo, para essas .



Só isso. Nós apenas tivemos uma introdução ao paradigma funcional em Golang. Infelizmente, parte das possibilidades teve que ser simulada. Parte, técnicas funcionais totalmente desenvolvidas, como mônadas, não foram incluídas aqui, porque há muitos artigos sobre eles no Go on the hub Muito ainda pode ser aprimorado no próprio idioma, por exemplo, com a próxima versão grande (GO 2), os genéricos devem aparecer no idioma. Bem, vamos esperar e esperar).

Source: https://habr.com/ru/post/pt476346/


All Articles