Como posso _read_ código JavaScript funcional?

9

Acredito que aprendi alguns / muitos / a maioria dos conceitos básicos subjacentes à programação funcional em JavaScript. No entanto, tenho problemas para ler especificamente o código funcional, mesmo o código que escrevi, e me pergunto se alguém pode me dar sugestões, dicas, práticas recomendadas, terminologia etc. que possam ajudar.

Pegue o código abaixo. Eu escrevi esse código. O objetivo é atribuir uma porcentagem de semelhança entre dois objetos, entre dizer {a:1, b:2, c:3, d:3}e {a:1, b:1, e:2, f:2, g:3, h:5}. Eu produzi o código em resposta a esta pergunta no Stack Overflow . Como não tinha certeza exatamente de que tipo de similaridade percentual o cartaz estava perguntando, forneço quatro tipos diferentes:

  • a porcentagem de chaves no primeiro objeto que pode ser encontrada no segundo,
  • a porcentagem dos valores no primeiro objeto que podem ser encontrados no segundo, incluindo duplicatas,
  • a porcentagem dos valores no 1º objeto que podem ser encontrados no 2º, sem duplicatas permitidas, e
  • a porcentagem de {key: value} emparelha no primeiro objeto que pode ser encontrado no segundo objeto.

Comecei com um código razoavelmente imperativo, mas rapidamente percebi que esse era um problema adequado para a programação funcional. Em particular, percebi que, se eu pudesse extrair uma função ou três para cada uma das quatro estratégias acima, que definiam o tipo de recurso que eu estava procurando comparar (por exemplo, as chaves, os valores etc.), então eu poderia estar capaz de reduzir (perdoar o jogo de palavras) o restante do código em unidades repetíveis. Você sabe, mantendo-o SECO. Então mudei para a programação funcional. Estou muito orgulhoso do resultado, acho que é razoavelmente elegante e acho que entendo o que fiz muito bem.

No entanto, mesmo tendo escrito o código e compreendendo cada parte dele durante a construção, quando agora olho para trás, continuo um pouco confuso com o modo de ler qualquer meia-linha em particular e com o modo de ler. "grok" o que qualquer meia-linha específica de código está realmente fazendo. Eu me pego fazendo flechas mentais para conectar diferentes partes que rapidamente se degradam em uma bagunça de espaguete.

Então, alguém pode me dizer como "ler" alguns dos pedaços de código mais complicados de uma maneira que seja concisa e que contribua para minha compreensão do que estou lendo? Eu acho que as partes que mais me atraem são aquelas que têm várias flechas grossas seguidas e / ou partes que têm vários parênteses seguidos. Mais uma vez, no âmago, posso finalmente descobrir a lógica, mas (espero), há uma maneira melhor de agir rápida e clara e diretamente "incorporando" uma linha de programação JavaScript funcional.

Sinta-se livre para usar qualquer linha de código abaixo ou até outros exemplos. No entanto, se você quiser algumas sugestões iniciais, aqui estão algumas. Comece com um razoavelmente simples. Perto do final do código, não há esse que é passado como um parâmetro para uma função: obj => key => obj[key]. Como alguém lê e entende isso? Um exemplo é mais uma função completa de perto do início: const getXs = (obj, getX) => Object.keys(obj).map(key => getX(obj)(key));. A última mapparte me deixa em particular.

Por favor nota, neste momento eu estou não procurando referências a Haskell ou notação abstrata simbólica ou os fundamentos do currying, etc. O que eu estou procurando é frases em inglês que eu possa silenciosamente boca enquanto olha para uma linha de código. Se você tem referências que abordam exatamente exatamente isso, ótimo, mas também não estou procurando respostas que digam que eu deveria ler alguns livros básicos. Eu fiz isso e recebo (pelo menos uma quantidade significativa) da lógica. Observe também que não preciso de respostas exaustivas (embora essas tentativas sejam bem-vindas): Mesmo respostas curtas que fornecem uma maneira elegante de ler uma única linha específica de código problemático seriam apreciadas.

Suponho que uma parte dessa pergunta seja: Posso ler o código funcional linearmente, da esquerda para a direita e de cima para baixo? Ou alguém é forçado a criar uma imagem mental da fiação semelhante a espaguete na página de código que decididamente não é linear? E se é preciso fazer isso, ainda precisamos ler o código, então como pegar texto linear e conectar o espaguete?

Qualquer dica seria apreciada.

const obj1 = { a:1, b:2, c:3, d:3 };
const obj2 = { a:1, b:1, e:2, f:2, g:3, h:5 };

// x or X is key or value or key/value pair

const getXs = (obj, getX) =>
  Object.keys(obj).map(key => getX(obj)(key));

const getPctSameXs = (getX, filter = vals => vals) =>
  (objA, objB) =>
    filter(getXs(objB, getX))
      .reduce(
        (numSame, x) =>
          getXs(objA, getX).indexOf(x) > -1 ? numSame + 1 : numSame,
        0
      ) / Object.keys(objA).length * 100;

const pctSameKeys       = getPctSameXs(obj => key => key);
const pctSameValsDups   = getPctSameXs(obj => key => obj[key]);
const pctSameValsNoDups = getPctSameXs(obj => key => obj[key], vals => [...new Set(vals)]);
const pctSameProps      = getPctSameXs(obj => key => JSON.stringify( {[key]: obj[key]} ));

console.log('obj1:', JSON.stringify(obj1));
console.log('obj2:', JSON.stringify(obj2));
console.log('% same keys:                   ', pctSameKeys      (obj1, obj2));
console.log('% same values, incl duplicates:', pctSameValsDups  (obj1, obj2));
console.log('% same values, no duplicates:  ', pctSameValsNoDups(obj1, obj2));
console.log('% same properties (k/v pairs): ', pctSameProps     (obj1, obj2));

// output:
// obj1: {"a":1,"b":2,"c":3,"d":3}
// obj2: {"a":1,"b":1,"e":2,"f":2,"g":3,"h":5}
// % same keys:                    50
// % same values, incl duplicates: 125
// % same values, no duplicates:   75
// % same properties (k/v pairs):  25
Andrew Willems
fonte

Respostas:

18

Você está tendo dificuldades para lê-lo porque esse exemplo em particular não é muito legível. Sem querer ofender, uma proporção desanimadora de amostras que você encontra na Internet também não é. Muitas pessoas brincam apenas com programação funcional nos fins de semana e nunca precisam lidar com a manutenção do código funcional de produção a longo prazo. Eu escreveria mais assim:

function mapObj(obj, f) {
  return Object.keys(obj).map(key => f(obj, key));
}

function getPctSameXs(obj1, obj2, f) {
  const mapped1 = mapObj(obj1, f);
  const mapped2 = mapObj(obj2, f);
  const same = mapped1.filter(x => mapped2.indexOf(x) != -1);
  const percent = same.length / mapped1.length * 100;
  return percent;
}

const getValues = (obj, key) => obj[key];
const valuesWithDupsPercent = getPctSameXs(obj1, obj2, getValues);

Por alguma razão, muitas pessoas têm essa ideia de que o código funcional deve ter uma certa "aparência" estética de uma grande expressão aninhada. Observe que, apesar de minha versão parecer um código imperativo com todos os pontos e vírgulas, tudo é imutável; portanto, você pode substituir todas as variáveis ​​e obter uma grande expressão, se quiser. É realmente tão "funcional" quanto a versão espaguete, mas com mais legibilidade.

Aqui, as expressões são divididas em pedaços muito pequenos e recebem nomes significativos para o domínio. O aninhamento é evitado puxando funcionalidades comuns, como mapObjem uma função nomeada. Lambdas são reservadas para funções muito curtas com um propósito claro no contexto.

Se você encontrar um código difícil de ler, refatore-o até ficar mais fácil. É preciso alguma prática, mas vale a pena. O código funcional pode ser tão legível quanto imperativo. De fato, muitas vezes mais, porque geralmente é mais conciso.

Karl Bielefeldt
fonte
Definitivamente nenhuma ofensa tomada! Embora eu ainda afirme que sei algumas coisas sobre programação funcional, talvez minhas afirmações na pergunta sobre o quanto eu sei tenham sido um pouco exageradas. Eu sou realmente um iniciante em relação. Portanto, ver como essa tentativa em particular pode ser reescrita de uma maneira tão clara e concisa, mas ainda funcional, parece ouro ... obrigado. Estarei estudando sua reescrita com cuidado.
Andrew Willems
11
Ouvi dizer que ter longas cadeias e / ou aninhamento de métodos elimina variáveis ​​intermediárias desnecessárias. Por outro lado, sua resposta divide minhas cadeias / aninhamentos em instruções independentes intermediárias usando variáveis ​​intermediárias bem nomeadas. Acho o seu código mais legível nesse caso, mas me pergunto o quão geral você está tentando ser. Você está dizendo que longas cadeias de métodos e / ou aninhamento profundo são frequentemente ou mesmo sempre um anti-padrão a ser evitado, ou há momentos em que trazem benefícios significativos? E a resposta a essa pergunta é diferente para codificação funcional versus imperativa?
Andrew Willems
3
Existem certas situações em que a eliminação de variáveis ​​intermediárias pode adicionar clareza. Por exemplo, no FP, você quase nunca deseja um índice em uma matriz. Às vezes, também não há um ótimo nome para o resultado intermediário. Na minha experiência, porém, a maioria das pessoas tende a errar demais para o outro lado.
Karl Bielefeldt
6

Eu não fiz muito trabalho altamente funcional em Javascript (o que eu diria que é isso - a maioria das pessoas falando sobre Javascript funcional pode estar usando mapas, filtros e reduções, mas seu código define suas próprias funções de nível superior , que é um pouco mais avançado que isso), mas já fiz isso em Haskell e acho que pelo menos parte da experiência se traduz. Vou dar algumas dicas para as coisas que aprendi:

Especificar os tipos de funções é realmente importante. Haskell não exige que você especifique qual é o tipo de uma função, mas incluir o tipo na definição facilita a leitura. Embora o Javascript não suporte a digitação explícita da mesma maneira, não há motivo para não incluir a definição de tipo em um comentário, por exemplo:

// getXs :: forall O, F . O -> (O -> String -> F) -> [F]
const getXs = (obj, getX) =>
    Object.keys(obj).map(key => getX(obj)(key));

Com um pouco de prática no trabalho com definições de tipo como essa, elas tornam o significado de uma função muito mais claro.

A nomeação é importante, talvez até mais do que na programação processual. Muitos programas funcionais são escritos em um estilo muito conciso que é pesado em convenções (por exemplo, a convenção de que 'xs' é uma lista / matriz e que 'x' é um item é muito difundida), mas a menos que você entenda esse estilo facilmente eu sugeriria nomes mais detalhados. Observando nomes específicos que você usou, "getX" é meio opaco e, portanto, "getXs" também não ajuda muito. Eu chamaria "getXs" algo como "applyToProperties" e "getX" provavelmente seria "propertyMapper". "getPctSameXs" seria "percentPropertiesSameWith" ("with")

Outra coisa importante é escrever código idiomático . Percebo que você está usando uma sintaxe a => b => some-expression-involving-a-and-bpara produzir funções com caril. Isso é interessante e pode ser útil em algumas situações, mas você não está fazendo nada aqui que se beneficie de funções com curry e seria mais Javascript idiomático usar funções tradicionais de vários argumentos. Isso pode facilitar a visualização rápida do que está acontecendo. Você também está usando const name = lambda-expressionpara definir funções, onde seria mais idiomático usar function name (args) { ... }. Eu sei que eles são semanticamente ligeiramente diferentes, mas, a menos que você esteja contando com essas diferenças, sugiro usar a variante mais comum quando possível.

Jules
fonte
5
+1 para tipos! Só porque o idioma não os possui, não significa que você não precise pensar neles . Vários sistemas de documentação para ECMAScript possuem uma linguagem de tipos para registrar os tipos de funções. Vários IDEs do ECMAScript também possuem uma linguagem de tipos (e, geralmente, eles também entendem as linguagens de tipos dos principais sistemas de documentação) e podem até executar verificação rudimentar de tipos e dicas heurísticas usando essas anotações de tipo .
Jörg W Mittag
Você me deu muito o que pensar: definições de tipo, nomes significativos, usando expressões idiomáticas ... obrigado! Apenas alguns dos muitos comentários possíveis: eu não pretendia necessariamente escrever certas partes como funções de caril; eles apenas evoluíram dessa maneira quando refatorei meu código durante a escrita. Agora posso ver como isso não era necessário, e até mesmo mesclar os parâmetros dessas duas funções em dois parâmetros para uma única função não apenas faz mais sentido, mas instantaneamente torna esse pequeno pedaço pelo menos mais legível.
Andrew Willems
@ JörgWMittag, obrigado por seus comentários sobre a importância dos tipos e pelo link para a outra resposta que você escreveu. Uso o WebStorm e não percebi que, de acordo com a outra resposta, o WebStorm sabe como interpretar anotações do tipo jsdoc. Estou assumindo pelo seu comentário que o jsdoc e o WebStorm podem ser usados ​​juntos para anotar códigos funcionais, e não apenas imperativos, mas eu teria que me aprofundar mais para realmente saber disso. Eu já joguei com o jsdoc antes e agora que sei que o WebStorm e eu podemos cooperar lá, espero usar mais esse recurso / abordagem.
Andrew Willems
@ Jules, apenas para esclarecer a qual função ao curry eu estava me referindo no meu comentário acima: Como você sugeriu, cada instância do obj => key => ...pode ser simplificada, (obj, key) => ...porque mais tarde getX(obj)(key)também pode ser simplificada get(obj, key). Por outro lado, outra função com curry,, (getX, filter = vals => vals) => (objA, objB) => ...não pode ser facilmente simplificada, pelo menos no contexto do restante do código, conforme escrito.
Andrew Willems