Lembre-se: programação funcional não é programação com funções! ! !
23.4 Programação Funcional
23.4.1 O
que é programação funcional? Se você perguntar de forma tão direta, descobrirá que é um conceito que não é fácil de explicar. Muitos veteranos com muitos anos de experiência na área de programação não conseguem explicar claramente o que a programação funcional está estudando. A programação funcional é de fato um campo desconhecido para programadores familiarizados com programação processual. Os conceitos de fechamento, continuação e currying parecem tão estranhos para nós. Os familiares if, else e while não têm nada em comum. Embora a programação funcional tenha belos protótipos matemáticos que a programação processual não consegue igualar, ela é tão misteriosa que apenas aqueles com doutorado podem dominá-la.
Dica: Esta seção é um pouco difícil, mas não é uma habilidade necessária para dominar JavaScript. Se você não quiser usar JavaScript para concluir as tarefas feitas em Lisp, ou não quiser aprender as habilidades esotéricas de Lisp. programação funcional, você pode ignorá-la e entrar no próximo capítulo de sua jornada.
Então, voltando à questão: o que é programação funcional? A resposta é longa…
A primeira lei da programação funcional: As funções são do primeiro tipo.
Como esta frase em si deve ser entendida? O que é um verdadeiro Tipo Um? Vejamos os seguintes conceitos matemáticos:
equação binária F(x, y) = 0, x, y são variáveis, escreva como y = f(x), x é um parâmetro, y é o valor de retorno, f é de x para y O relacionamento de mapeamento é chamado de função. Se houver, G(x, y, z) = 0, ou z = g(x, y), g é a relação de mapeamento de x, y para z, e também é uma função. Se os parâmetros xey de g satisfazem a relação anterior y = f(x), então obtemos z = g(x, y) = g(x, f(x)). x) é uma função em x e um parâmetro da função g. Em segundo lugar, g é uma função de ordem superior a f.
Desta forma, usamos z = g(x, f(x)) para representar a solução associada das equações F(x, y) = 0 e G(x, y, z) = 0, que é uma função iterativa . Também podemos expressar g de outra forma, lembre-se de z = g(x, y, f), de modo que generalizemos a função g em uma função de ordem superior. Comparado com o anterior, a vantagem desta última representação é que se trata de um modelo mais geral, como a solução associada de T(x,y) = 0 e G(x,y,z) = 0. Também pode ser expresso da mesma forma (seja f = t). Neste sistema de linguagem que suporta a iteração de conversão da solução de um problema em uma função de ordem superior, a função é chamada de "primeiro tipo".
Funções em JavaScript são claramente de “primeiro tipo”. Aqui está um exemplo típico:
Array.prototype.each = função (fechamento)
{
retornar este.comprimento?[fechamento(este[0])].concat(este.slice(1).each(fechamento)) : [];
}
Este é realmente um código mágico, que dá pleno uso ao charme do estilo funcional. Existem apenas funções e símbolos em todo o código. É simples na forma e infinitamente poderoso.
[1,2,3,4].each(function(x){return x * 2}) obtém [2,4,6,8], enquanto [1,2,3,4].each(function(x ){retornar x-1}) obtém [0,1,2,3].
A essência do funcional e orientado a objetos é que “Tao segue a natureza”. Se a orientação a objetos é uma simulação do mundo real, então a expressão funcional é uma simulação do mundo matemático. Em certo sentido, seu nível de abstração é superior ao da orientação a objetos, porque os sistemas matemáticos possuem inerentemente características que são incomparáveis na natureza. de abstração.
A segunda lei da programação funcional: Closures são os melhores amigos da programação funcional.
Os encerramentos, como explicamos nos capítulos anteriores, são muito importantes para a programação funcional. Sua maior característica é que você pode acessar diretamente o ambiente externo a partir da camada interna sem passar variáveis (símbolos). Isso traz grande comodidade para programas funcionais em aninhamento múltiplo
.
{
função de retorno innerFun(y)
{
retornar x * y;
}
})(2)(3);
A terceira lei da programação funcional: funções podem ser Currying.
O que é Currying? É um conceito interessante. Vamos começar com a matemática: dizemos, considere uma equação espacial tridimensional F(x, y, z) = 0, se limitarmos z = 0, então obteremos F(x, y, 0) = 0, denotado como F '(x, y). Aqui F' é obviamente uma nova equação, que representa a projeção bidimensional da curva espacial tridimensional F(x, y, z) no plano z = 0. Denote y = f(x, z), seja z = 0, obtemos y = f(x, 0), denote-o como y = f'(x), dizemos que a função f' é uma solução Currying de f .
Um exemplo de JavaScript Currying é fornecido abaixo:
função adicionar (x, y)
{
if(x!=nulo && y!=nulo) retornar x + y;
senão if(x!=null && y==null) return function(y)
{
retornar x + y;
}
senão if(x==nulo && y!=nulo) função de retorno(x)
{
retornar x + y;
}
}
var a = adicionar(3, 4);
var b = adicionar(2);
var c = b(10);
No exemplo acima, b=add(2) resulta em uma função Currying de add(), que é uma função do parâmetro y quando x = 2. Observe que ela também é usada acima. de fechamentos.
Curiosamente, podemos generalizar Currying para qualquer função, por exemplo:
function Foo(x, y, z, w)
{
var args = argumentos
if(Foo.length <args.length)
função de retorno()
{
retornar
args.callee.apply(Array.apply([], args).concat(Array.apply([], argumentos)));
}
outro
retornar x + y – z * w;
}
A quarta lei da programação funcional: avaliação atrasada e continuação.
//TODO: Pense nisso novamente aqui
23.4.2 Vantagens do
teste unitário
de programação funcionalCada símbolo de programação funcional estrita é uma referência a uma quantidade direta ou resultado de expressão, e nenhuma função tem efeitos colaterais. Porque o valor nunca é modificado em algum lugar e nenhuma função modifica uma quantidade fora de seu escopo que é usada por outras funções (como membros de classe ou variáveis globais). Isso significa que o resultado da avaliação de uma função é apenas seu valor de retorno, e as únicas coisas que afetam seu valor de retorno são os parâmetros da função.
Este é o sonho molhado de um testador de unidade. Para cada função do programa em teste, você só precisa se preocupar com seus parâmetros, sem ter que considerar a ordem das chamadas de função ou definir cuidadosamente o estado externo. Tudo o que você precisa fazer é passar parâmetros que representem casos extremos. Se todas as funções do programa passarem no teste de unidade, você terá uma confiança considerável na qualidade do software. Mas a programação imperativa não pode ser tão otimista. Em Java ou C++, não basta apenas verificar o valor de retorno de uma função - devemos também verificar o estado externo que a função pode ter modificado.
Depuração
Se um programa funcional não se comporta da maneira esperada, a depuração é moleza. Como os bugs em programas funcionais não dependem de caminhos de código não relacionados a eles antes da execução, os problemas encontrados sempre podem ser reproduzidos. Em programas imperativos, bugs aparecem e desaparecem, pois o funcionamento da função depende dos efeitos colaterais de outras funções, e você pode pesquisar por muito tempo em direções não relacionadas à ocorrência do bug, mas sem nenhum resultado. Este não é o caso dos programas funcionais - se o resultado de uma função estiver errado, não importa o que você execute antes, a função sempre retornará o mesmo resultado errado.
Depois de recriar o problema, encontrar sua causa raiz será fácil e poderá até deixá-lo feliz. Interrompa a execução desse programa e examine a pilha. Assim como na programação imperativa, os parâmetros de cada chamada de função na pilha são apresentados a você. Mas em programas imperativos estes parâmetros não são suficientes. As funções também dependem de variáveis de membro, variáveis globais e do estado da classe (que por sua vez depende de muitas delas). Na programação funcional, uma função depende apenas dos seus parâmetros, e essa informação está bem debaixo dos seus olhos! Além disso, em um programa imperativo, apenas verificar o valor de retorno de uma função não pode garantir que a função esteja funcionando corretamente. Você precisa verificar o status de dezenas de objetos fora do escopo dessa função para confirmar. Com um programa funcional, tudo que você precisa fazer é observar seu valor de retorno!
Verifique os parâmetros e os valores de retorno da função ao longo da pilha. Assim que encontrar um resultado irracional, entre nessa função e siga passo a passo. Repita esse processo até encontrar o ponto onde o bug foi gerado.
Programas funcionais paralelos podem ser executados em paralelo sem qualquer modificação. Não se preocupe com impasses e seções críticas porque você nunca usa bloqueios! Nenhum dado em um programa funcional é modificado duas vezes pelo mesmo thread, muito menos por dois threads diferentes. Isso significa que threads podem ser simplesmente adicionados sem pensar duas vezes, sem causar os problemas tradicionais que afetam os aplicativos paralelos.
Se for esse o caso, por que nem todo mundo usa programação funcional em aplicações que exigem operações altamente paralelas? Bem, eles estão fazendo isso. A Ericsson projetou uma linguagem funcional chamada Erlang e a utilizou em switches de telecomunicações que exigem tolerância a falhas e escalabilidade extremamente altas. Muitas pessoas também descobriram as vantagens do Erlang e começaram a usá-lo. Estamos falando de sistemas de controle de telecomunicações, que exigem muito mais confiabilidade e escalabilidade do que um sistema típico projetado para Wall Street. Na verdade, o sistema Erlang não é confiável e extensível, mas o JavaScript é. Os sistemas Erlang são sólidos como uma rocha.
A história sobre paralelismo não para por aí. Mesmo que seu programa seja de thread único, um compilador de programa funcional ainda pode otimizá-lo para execução em múltiplas CPUs. Por favor, observe o seguinte código:
String s1 = someLongOperation1();
String s2 = someLongOperation2();
String s3 = concatenate(s1, s2);
Em uma linguagem de programação funcional, o compilador analisa o código para identificar funções potencialmente demoradas que criam as strings s1 e s2 e depois as executa em paralelo. Isto não é possível em linguagens imperativas, onde cada função pode modificar o estado fora do escopo da função e as funções subsequentes podem depender dessas modificações. Em linguagens funcionais, analisar funções automaticamente e identificar candidatos adequados para execução paralela é tão simples quanto inlining automático de funções! Nesse sentido, a programação de estilo funcional é "à prova de futuro" (mesmo que eu não goste de usar termos da indústria, desta vez abrirei uma exceção). Os fabricantes de hardware não conseguiam mais fazer as CPUs rodarem mais rápido, então aumentaram a velocidade dos núcleos do processador e alcançaram um aumento de quatro vezes na velocidade devido ao paralelismo. Claro, eles também esqueceram de mencionar que o dinheiro extra que gastamos foi usado apenas em software para resolver problemas paralelos. Uma pequena proporção de software imperativo e software 100% funcional pode ser executada diretamente em paralelo nessas máquinas.
A implantação a quente de código
costumava exigir a instalação de atualizações no Windows e a reinicialização do computador era inevitável, e mais de uma vez, mesmo se uma nova versão do media player fosse instalada. O Windows XP melhorou muito essa situação, mas ainda não é o ideal (hoje executei o Windows Update no trabalho e agora um ícone irritante sempre aparece na bandeja, a menos que eu reinicie a máquina). Os sistemas Unix sempre funcionaram melhor. Ao instalar atualizações, apenas os componentes relacionados ao sistema precisam ser interrompidos, e não todo o sistema operacional. Mesmo assim, isso ainda é insatisfatório para uma aplicação de servidor de grande escala. Os sistemas de telecomunicações devem estar operacionais 100% do tempo porque se a discagem de emergência falhar enquanto o sistema estiver sendo atualizado, poderá ocorrer perda de vidas. Não há razão para que as empresas de Wall Street fechem os serviços no fim de semana para instalar atualizações.
A situação ideal é atualizar o código relevante sem interromper nenhum componente do sistema. Isso é impossível em um mundo imperativo. Considere que quando o tempo de execução fizer upload de uma classe Java e substituir uma nova definição, todas as instâncias dessa classe ficarão indisponíveis porque seu estado salvo será perdido. Poderíamos começar a escrever algum código de controle de versão tedioso para resolver esse problema, depois serializar todas as instâncias dessa classe, destruir essas instâncias, recriar essas instâncias com a nova definição dessa classe e, em seguida, carregar os dados serializados anteriormente e esperar que o carregamento o código portará corretamente esses dados para a nova instância. Além disso, o código de portabilidade deve ser reescrito manualmente a cada atualização, e muito cuidado deve ser tomado para evitar a quebra dos inter-relacionamentos entre os objetos. A teoria é simples, mas a prática não é fácil.
Para programas funcionais, todos os estados, ou seja, os parâmetros passados para a função, são salvos na pilha, facilitando a implantação a quente! Na verdade, tudo o que precisamos fazer é comparar o código funcional e a nova versão e, em seguida, implantar o novo código. O resto será feito automaticamente por uma ferramenta de linguagem! Se você acha que esta é uma história de ficção científica, pense novamente. Durante anos, os engenheiros de Erlang atualizaram seus sistemas em execução sem interrompê-los.
Raciocínio e otimização assistidos por máquina
Uma propriedade interessante das linguagens funcionais é que elas podem ser raciocinadas matematicamente. Como uma linguagem funcional é apenas uma implementação de um sistema formal, todas as operações feitas no papel podem ser aplicadas a programas escritos nesta linguagem. Os compiladores podem usar a teoria matemática para transformar um trecho de código em código equivalente, porém mais eficiente [7]. Os bancos de dados relacionais vêm passando por esse tipo de otimização há anos. Não há razão para que esta técnica não possa ser aplicada a software normal.
Além disso, você pode usar essas técnicas para provar que partes do seu programa estão corretas e talvez até criar ferramentas para analisar seu código e gerar automaticamente casos extremos para testes de unidade! Esta funcionalidade não tem valor para um sistema robusto, mas se você estiver projetando um marca-passo ou um sistema de controle de tráfego aéreo, esta ferramenta é indispensável. Se os aplicativos que você escreve não são tarefas essenciais do setor, esse tipo de ferramenta também pode ser um trunfo sobre seus concorrentes.
23.4.3 Desvantagens da programação funcional
Efeitos colaterais dos encerramentos
Na programação funcional não estrita, os encerramentos podem substituir o ambiente externo (já vimos isso no capítulo anterior), o que traz efeitos colaterais, e quando tais efeitos colaterais ocorrem com frequência E quando o ambiente em que o programa é executado é alterado com frequência, os erros tornam-se difíceis de rastrear.
//TODO:
forma recursiva
Embora a recursão seja frequentemente a forma de expressão mais concisa, ela não é tão intuitiva quanto os loops não recursivos.
//TODO:
A fraqueza do valor atrasado
//TODO: