Tendo sido autor de vários projetos no estilo funcional JavaScript, permita-me compartilhar minha experiência. Vou me concentrar em considerações de alto nível, não em preocupações específicas de implementação. Lembre-se, não há balas de prata, faça o que funciona melhor para sua situação específica.
Estilo funcional versus funcional
Considere o que você deseja obter com essa refatoração funcional. Deseja realmente uma implementação funcional pura ou apenas alguns dos benefícios que um estilo de programação funcional pode trazer, como transparência referencial.
Eu descobri que a última abordagem, o que chamo de estilo funcional, combina bem com JavaScript e geralmente é o que eu quero. Também permite que conceitos seletivos não funcionais sejam usados com cuidado no código onde eles fazem sentido.
Escrever código em um estilo mais puramente funcional também é possível em JavaScript, mas nem sempre é a solução mais apropriada. E o Javascript nunca pode ser puramente funcional.
Interfaces de estilo funcional
No estilo funcional Javascript, a consideração mais importante é a fronteira entre o código funcional e o imperativo. Claramente entender e definir esse limite é essencial.
Organizo meu código em interfaces de estilo funcional. Essas são coleções de lógica que se comportam em um estilo funcional, variando de funções individuais a módulos inteiros. A separação conceitual de interface e implementação é importante, uma interface de estilo funcional pode ser implementada imperativamente, desde que a implementação imperativa não vaze através do limite.
Infelizmente em Javascript, o ônus de impor interfaces de estilo funcional recai inteiramente no desenvolvedor. Além disso, recursos de idioma, como exceções, tornam impossível uma separação realmente limpa.
Como você refatorará uma base de código existente, comece identificando as interfaces lógicas existentes no seu código que podem ser movidas corretamente para um estilo funcional. Uma quantidade surpreendente de código já pode ser de estilo funcional. Bons pontos de partida são pequenas interfaces com poucas dependências imperativas. Sempre que você move o código, as dependências imperativas existentes são eliminadas e mais códigos podem ser movidos.
Continue iterando, trabalhando gradualmente para o exterior para empurrar grandes partes do seu projeto para o lado funcional do limite, até ficar satisfeito com os resultados. Essa abordagem incremental permite testar alterações individuais e facilita a identificação e a aplicação do limite.
Repensar algoritmos, estruturas de dados e design
Soluções imperativas e padrões de design geralmente não fazem sentido no estilo funcional. Especialmente para estruturas de dados, as portas diretas do código imperativo para o estilo funcional podem ser feias e lentas. Um exemplo simples: você prefere copiar todos os elementos de uma lista para adicionar um prefixo ou incluir o elemento na lista existente?
Pesquise e avalie as abordagens funcionais existentes para os problemas. A programação funcional é mais do que apenas uma técnica de programação, é uma maneira diferente de pensar e resolver problemas. Projetos escritos nas linguagens de programação funcional mais tradicionais, como lisp ou haskell, são ótimos recursos nesse sentido.
Compreender o idioma e os recursos internos
Esta é uma parte importante da compreensão do limite imperativo funcional e afetará o design da interface. Entenda quais recursos podem ser usados com segurança no código de estilo funcional e quais não. Tudo a partir do qual os built-in podem gerar exceções e quais, como [].push
, modificar objetos, a objetos sendo passados por referência e como as cadeias de protótipos funcionam. Também é necessário um entendimento semelhante de qualquer outra biblioteca que você consome no seu projeto.
Esse conhecimento permite tomar decisões de design informadas, como lidar com entradas e exceções ruins ou o que acontece se uma função de retorno de chamada fizer algo inesperado? Parte disso pode ser aplicada no código, mas outras apenas requerem documentação sobre o uso adequado.
Outro ponto é quão rigorosa sua implementação deve ser. Deseja realmente tentar aplicar o comportamento correto aos erros máximos de pilha de chamadas ou quando o usuário redefine alguma função interna?
Usando conceitos não funcionais, quando apropriado
As abordagens funcionais nem sempre são apropriadas, especialmente em uma linguagem como JavaScript. A solução recursiva pode ser elegante até você começar a obter o máximo de exceções da pilha de chamadas para entradas maiores, portanto, talvez uma abordagem iterativa seja melhor. Da mesma forma, uso herança para organização de código, atribuição em construtores e objetos imutáveis onde eles fazem sentido.
Desde que as interfaces funcionais não sejam violadas, faça o que faz sentido e o que será mais fácil de testar e manter.
Exemplo
Aqui está um exemplo de conversão de código real muito simples em estilo funcional. Não tenho um exemplo grande e muito bom do processo, mas este pequeno aborda vários pontos interessantes.
Esse código cria um teste a partir de uma seqüência de caracteres. Começarei com algum código baseado na seção superior do módulo trie-js build-trie de John Resig . Muitos simplificações / formatação alterações foram feitas e minha intenção não é comentário sobre a qualidade do código original (Há maneiras muito mais claras e mais limpas para construir uma trie, então por que este código c estilo aparece em primeiro lugar no Google é interessante. Aqui está um implementação rápida que usa reduzir ).
Gosto deste exemplo porque não preciso levá-lo a uma verdadeira implementação funcional, apenas a interfaces funcionais. Aqui está o ponto de partida:
var txt = SOME_USER_STRING,
words = txt.replace(/\n/g, "").split(" "),
trie = {};
for (var i = 0, l = words.length; i < l; i++) {
var word = words[i], letters = word.split(""), cur = trie;
for (var j = 0; j < letters.length; j++) {
var letter = letters[j];
if (!cur.hasOwnProperty(letter))
cur[letter] = {};
cur = cur[ letter ];
}
cur['$'] = true;
}
// Output for text = 'a ab f abc abf'
trie = {
"a": {
"$":true,
"b":{
"$":true,
"c":{"$":true}
"f":{"$":true}}},
"f":{"$":true}};
Vamos fingir que este é o programa inteiro. Este programa faz duas coisas: converter uma string em uma lista de palavras e criar um trie a partir da lista de palavras. Essas são as interfaces existentes no programa. Aqui está o nosso programa com as interfaces mais claramente expressas:
var _get_words = function(txt) {
return txt.replace(/\n/g, "").split(" ");
};
var _build_trie_from_list = function(words, trie) {
for (var i = 0, l = words.length; i < l; i++) {
var word = words[i], letters = word.split(""), cur = trie;
for (var j = 0; j < letters.length; j++) {
var letter = letters[j];
if (!cur.hasOwnProperty(letter))
cur[letter] = {};
cur = cur[ letter ];
}
cur['$'] = true;
}
};
// The 'public' interface
var build_trie = function(txt) {
var words = _get_words(txt), trie = {};
_build_trie_from_list(words, trie);
return trie;
};
Apenas definindo nossas interfaces, podemos passar _get_words
para o lado do estilo funcional do limite. Nem replace
ou split
modifica a string original. E nossa interface pública também build_trie
é estilo funcional, mesmo que interaja com algum código muito imperativo. De fato, esse é um bom ponto de parada na maioria dos casos. Mais código ficaria avassalador, então deixe-me apresentar algumas outras alterações.
Primeiro, torne o estilo funcional de todas as interfaces. Isso é trivial neste caso, basta _build_trie_from_list
retornar um objeto em vez de mutá-lo.
Manipulação de entrada incorreta
Considere o que acontece se chamarmos build_trie
com uma matriz de caracteres build_trie(['a', ' ', 'a', 'b', 'c', ' ', 'f'])
,. O chamador assumiu que isso se comportaria em um estilo funcional, mas, em vez disso, gera uma exceção quando .replace
é chamado na matriz. Este pode ser o comportamento pretendido. Ou podemos fazer verificações explícitas de tipo e garantir que as entradas sejam as esperadas. Mas eu prefiro digitar pato.
Strings são apenas matrizes de caracteres e matrizes são apenas objetos com uma length
propriedade e números inteiros não negativos como chaves. Então, se nós reformulado e escreveu novas replace
e split
métodos que opera na matriz como objetos de cordas, eles não tem sequer a ser strings, o nosso código poderia apenas fazer a coisa certa. ( String.prototype.*
não funcionará aqui, converte a saída em uma string). A digitação com patos é totalmente separada da programação funcional, mas o ponto mais importante é que as entradas ruins devem sempre ser consideradas.
Redesenho fundamental
Há também considerações mais fundamentais. Suponha que também queremos criar o teste em estilo funcional. No código imperativo, a abordagem era construir a palavra trie de cada vez. Uma porta direta nos faria copiar o trie inteiro toda vez que precisamos fazer uma inserção para evitar a mutação de objetos. Claramente, isso não vai funcionar. Em vez disso, o trie pode ser construído nó por nó, de baixo para cima, para que, uma vez que um nó seja concluído, ele nunca precise ser tocado novamente. Ou outra abordagem inteiramente pode ser melhor, uma pesquisa rápida mostra muitas implementações funcionais existentes de tentativas.
Espero que o exemplo tenha esclarecido um pouco as coisas.
No geral, acho que escrever código de estilo funcional em Javascript é um esforço interessante e interessante. Javascript é uma linguagem funcional surpreendentemente capaz, embora muito detalhada em seu estado padrão.
Boa sorte com seu projeto.
Alguns projetos pessoais escritos em estilo funcional Javascript:
- parse.js - Combinadores de analisadores
- Nu - córregos preguiçosos
- Atum - intérprete Javascript escrito em estilo Javascript funcional.
- Khepri - Linguagem de programação de protótipo que eu uso para o desenvolvimento funcional de Javascript. Implementado em estilo funcional Javascript.