Puro funcional vs dizer, não pergunte?


14

"O número ideal de argumentos para uma função é zero" está totalmente errado. O número ideal de argumentos é exatamente o número necessário para permitir que sua função seja livre de efeitos colaterais. Menos do que isso e você desnecessariamente faz com que suas funções sejam impuras, forçando-o a se afastar do poço do sucesso e a subir o gradiente de dor. Às vezes, "Tio Bob" fica no local com seus conselhos. Às vezes, ele está espetacularmente errado. Seu conselho de zero argumentos é um exemplo deste último

( Fonte: comentário de @David Arno sob outra pergunta neste site )

O comentário ganhou uma quantidade espetacular de 133 votos positivos, e é por isso que eu gostaria de prestar mais atenção ao seu mérito.

Tanto quanto sei, existem duas maneiras distintas de programação: programação funcional pura (o que esse comentário é encorajador) e diga, não pergunte (o que de vez em quando também é recomendado neste site). AFAIK, esses dois princípios são fundamentalmente incompatíveis, próximos de serem opostos: o funcional puro pode ser resumido como "apenas retornar valores, não tem efeitos colaterais", enquanto dizer, não perguntar pode ser resumido como "não retornar nada, só tem efeitos colaterais ". Além disso, fico meio perplexo porque pensei que dizer, não perguntar era considerado o núcleo do paradigma OO, enquanto funções puras eram consideradas o núcleo do paradigma funcional - agora vejo funções puras recomendadas no OO!

Suponho que os desenvolvedores provavelmente devam escolher um desses paradigmas e cumpri-lo? Bem, devo admitir que nunca poderia me seguir. Muitas vezes, parece conveniente que eu retorne um valor e não consigo ver como conseguir o que quero alcançar apenas com efeitos colaterais. Muitas vezes, parece conveniente para mim ter efeitos colaterais e não consigo realmente ver como posso alcançar o que quero alcançar apenas retornando valores. Além disso, muitas vezes (acho que isso é horrível), tenho métodos que fazem as duas coisas.

No entanto, dessas 133 votações anteriores, estou raciocinando que atualmente a programação funcional pura está "ganhando", pois se torna um consenso que é superior dizer, não pergunte. Isso está correto?

Portanto, no exemplo deste jogo antipadrão, estou tentando fazer : Se eu quisesse fazê-lo estar em conformidade com o paradigma funcional puro - COMO ?!

Parece-me razoável ter um estado de batalha. Como este é um jogo baseado em turnos, mantenho os estados de batalha em um dicionário (multiplayer - pode haver muitas batalhas disputadas por muitos jogadores ao mesmo tempo). Sempre que um jogador faz a sua vez, eu chamo um método apropriado no estado de batalha que (a) modifica o estado de acordo e (b) retorna atualizações para os jogadores, que são serializados no JSON e basicamente apenas lhes diz o que aconteceu no jogo. borda. Suponho que isso viole flagrantemente os dois princípios e, ao mesmo tempo.

OK - eu poderia tornar um método RETURN um estado de batalha em vez de modificá-lo no lugar, se realmente quisesse. Mas! Terei de copiar desnecessariamente tudo o que está no estado de batalha para retornar um estado totalmente novo em vez de modificá-lo?

Agora, talvez, se o movimento é um ataque, eu poderia apenas retornar um personagem atualizado HP? O problema é que não é tão simples: regras de jogo, uma jogada pode e muitas vezes terá muito mais efeitos do que apenas remover uma parte do HP de um jogador. Por exemplo, pode aumentar a distância entre caracteres, aplicar efeitos especiais, etc.

Parece muito mais simples modificar o estado em vigor e retornar atualizações ...

Mas como um engenheiro experiente lidaria com isso?


9
Seguir qualquer paradigma é um caminho certo para o fracasso. A política nunca deve superar a inteligência. A solução para um problema deve depender do problema, não de suas crenças religiosas sobre a solução de problemas.
John Douma

1
Eu nunca tive uma pergunta aqui sobre algo que eu disse antes. Estou honrado. :)
David Arno

Respostas:


14

Como a maioria dos aforismos de programação, "diga, não peça" sacrifica a clareza para ganhar brevidade. Não é de todo recomendável recomendar a não solicitação dos resultados de um cálculo, é recomendável não solicitar as entradas de um cálculo. "Não obtenha, calcule e defina, mas não há problema em retornar um valor de um cálculo", não é tão conciso.

Costumava ser bastante comum as pessoas chamarem um getter, fazer alguns cálculos e depois chamar um setter com o resultado. Esse é um sinal claro de que seu cálculo realmente pertence à classe que você chamou de getter. "Diga, não pergunte" foi cunhado para lembrar as pessoas de estarem atentas a esse antipadrão, e funcionou tão bem que agora algumas pessoas pensam que essa parte é óbvia e procuram outros tipos de "pedidos" para eliminar. No entanto, o aforismo é apenas útil para essa situação.

Os programas funcionais puros nunca sofreram com esse antipadrão exato, pela simples razão de que não há criadores nesse estilo. No entanto, o problema mais geral (e mais difícil de ver) de não misturar diferentes níveis de abstração semântica na mesma função se aplica a todo paradigma.


Obrigado por explicar corretamente "Diga, não pergunte".
user949300

13

Tanto o tio Bob quanto David Arno (o autor da citação que você teve) têm lições importantes que podemos extrair do que escreveram. Acho que vale a pena aprender a lição e depois extrapolar o que realmente significa para você e seu projeto.

Primeiro: Lição do Tio Bob

O tio Bob está argumentando que quanto mais argumentos você tiver em sua função / método, mais desenvolvedores que o usarão terão que entender. Essa carga cognitiva não é gratuita e, se você não for consistente com a ordem dos argumentos, etc., a carga cognitiva apenas aumentará.

Isso é um fato de ser humano. Penso que o principal erro no livro de código limpo do tio Bob é a afirmação "O número ideal de argumentos para uma função é zero" . O minimalismo é ótimo até que não seja. Assim como você nunca alcança seus limites no Cálculo, nunca alcançará o código "ideal" - nem deveria.

Como Albert Einstein disse: "Tudo deve ser o mais simples possível, mas não mais simples".

Segundo: Lição de David Arno

A maneira de desenvolver David Arno descrito é um desenvolvimento de estilo mais funcional do que orientado a objetos . No entanto, o código funcional é muito melhor que a programação tradicional orientada a objetos. Por quê? Por causa do bloqueio. Sempre que um estado é mutável em um objeto, você corre o risco de condições de corrida ou contenção de bloqueio.

Tendo escrito sistemas altamente concorrentes usados ​​em simulações e outras aplicações do lado do servidor, o modelo funcional faz maravilhas. Eu posso atestar as melhorias que a abordagem fez. No entanto, é um estilo de desenvolvimento muito diferente, com diferentes requisitos e expressões idiomáticas.

Desenvolvimento é uma série de trade-offs

Você conhece seu aplicativo melhor do que qualquer um de nós. Você pode não precisar da escalabilidade fornecida com a programação de estilos funcionais. Existe um mundo entre os dois ideais listados acima. Aqueles de nós que lidam com sistemas que precisam lidar com alto rendimento e paralelismo ridículo tenderão ao ideal da programação funcional.

Dito isto, você pode usar objetos de dados para armazenar o conjunto de informações que você precisa transmitir a um método. Isso ajuda no problema de carga cognitiva que o tio Bob estava abordando, enquanto ainda sustentava o ideal funcional que David Arno estava abordando.

Eu trabalhei em ambos os sistemas de desktop com paralelismo limitado necessário e software de simulação de alto rendimento. Eles têm necessidades muito diferentes. Eu posso apreciar um código orientado a objetos bem escrito, projetado com base no conceito de ocultação de dados que você conhece. Funciona para várias aplicações. No entanto, isso não funciona para todos eles.

Quem está certo? Bem, David está mais certo que o tio Bob neste caso. No entanto, o ponto subjacente que quero destacar aqui é que um método deve ter tantos argumentos quanto faça sentido.


Há paralelismo. Diferentes batalhas podem ser processadas em paralelo. No entanto, sim: uma única batalha, enquanto está sendo processada, precisa ser travada.
precisa saber é

Sim, eu quis dizer que os leitores (ceifadores na sua analogia) recolhem seus escritos (o semeador). Dito isto, voltei a olhar para algumas coisas que escrevi no passado e reaprendi alguma coisa ou discordei de meu antigo eu. Estamos todos aprendendo e evoluindo, e essa é a razão número um pela qual você deve sempre raciocinar sobre como e se aplica algo que aprendeu.
Berin Loritsch 7/03/19

8

OK - eu poderia tornar um método RETURN um estado de batalha em vez de modificá-lo no lugar, se realmente quisesse.

Sim, essa é a ideia.

Terei então que copiar tudo no estado de batalha para retornar um estado totalmente novo em vez de modificá-lo no lugar?

Não. Seu "estado de batalha" pode ser modelado como uma estrutura de dados imutável, que contém outras estruturas de dados imutáveis ​​como blocos de construção, talvez aninhadas em algumas hierarquias de estruturas de dados imutáveis.

Portanto, pode haver partes do estado de batalha que não precisam ser alteradas durante um turno, e outras que precisam ser alteradas. As partes que não mudam não precisam ser copiadas, uma vez que são imutáveis, basta copiar uma referência a essas partes, sem risco de causar efeitos colaterais. Isso funciona melhor em ambientes de idiomas coletados pelo lixo.

Pesquise no Google por "Estruturas de dados imutáveis ​​eficientes" e você certamente encontrará algumas referências sobre como isso funciona em geral.

Parece muito mais simples modificar o estado em vigor e retornar as atualizações.

Para certos problemas, isso pode ser realmente mais simples. Jogos e simulações baseadas em rodadas podem se enquadrar nessa categoria, pois grande parte do estado do jogo muda de uma rodada para outra. No entanto, a percepção do que é realmente "mais simples" é até certo ponto subjetiva e depende muito do que as pessoas estão acostumadas.


8

Como autor do comentário, acho que devo esclarecer aqui, pois é claro que há mais do que a versão simplificada que meu comentário oferece.

AFAIK, esses dois princípios são fundamentalmente incompatíveis, próximos de serem opostos: o funcional puro pode ser resumido como "apenas retornar valores, não tem efeitos colaterais", enquanto dizer, não perguntar pode ser resumido como "não retornar nada, só tem efeitos colaterais ".

Para ser sincero, acho isso um uso muito estranho do termo "diga, não pergunte". Então, eu li o que Martin Fowler disse sobre o assunto há alguns anos, o que foi esclarecedor . A razão pela qual eu achei estranho é porque "dizer não perguntar" é sinônimo de injeção de dependência na minha cabeça e a forma mais pura de injeção de dependência é passar tudo o que uma função precisa através de seus parâmetros.

Mas parece que o significado que aplico a "dizer não perguntar" vem de pegar a definição focada em OO de Fowler e torná-la mais independente de paradigma. No processo, acredito que leva o conceito a suas conclusões lógicas.

Vamos voltar ao começo simples. Temos "pedaços de lógica" (procedimentos) e temos dados globais. Os procedimentos leem esses dados diretamente para acessá-los. Temos um cenário simples de "perguntar".

Vento para a frente um pouco. Agora temos objetos e métodos. Esses dados não precisam mais ser globais, podem ser transmitidos pelo construtor e contidos no objeto. E então temos métodos que atuam sobre esses dados. Então agora temos "diga, não pergunte" como Fowler descreve. O objeto recebe seus dados. Esses métodos não precisam mais solicitar ao escopo global seus dados. Mas aqui está o problema: isso ainda não é verdade "diga, não pergunte" na minha opinião, pois esses métodos ainda precisam perguntar ao escopo do objeto. Este é mais um cenário "diga, pergunte" que eu sinto.

Então, avance para os dias modernos, despeje a abordagem "é OO todo o caminho" e pegue emprestados alguns princípios da programação funcional. Agora, quando um método é chamado, todos os dados são fornecidos a ele por meio de seus parâmetros. Pode (e tem) sido argumentado: "qual é o sentido, isso está apenas complicando o código?" E sim, a passagem por parâmetros, dados acessíveis pelo escopo do objeto, acrescenta complexidade ao código. Mas armazenar esses dados em um objeto, em vez de torná-lo acessível globalmente, também aumenta a complexidade. No entanto, poucos argumentariam que as variáveis ​​globais são sempre melhores porque são mais simples. A questão é que os benefícios que "contar, não perguntar" trazem mais do que a complexidade de reduzir o escopo. Isso se aplica mais à passagem via parâmetros do que à restrição do escopo ao objeto.private statice passe tudo o que precisa através dos parâmetros e agora esse método pode ser confiável para não acessar sorrateiramente coisas que não deveria. Além disso, incentiva a manter o método pequeno, caso contrário, a lista de parâmetros fica fora de controle. E incentiva métodos de escrita que se encaixam nos critérios de "função pura".

Portanto, não vejo "puro funcional" e "diga, não pergunte" como opostos um ao outro. O primeiro é a única implementação completa do último, no que me diz respeito. A abordagem de Fowler não é completa "diga, não pergunte".

Mas é importante lembrar que essa "implementação completa do dizer não perguntar" é realmente um ideal, ou seja, o pragmatismo deve entrar em ação a menos que nos tornemos idealistas e, assim, o tratemos de maneira incorreta como a única abordagem possivelmente correta de todos os tempos. Pouquíssimos aplicativos podem chegar perto de serem 100% livres de efeitos colaterais pelo simples motivo de que eles não fariam nada de útil se fossem realmente livres de efeitos colaterais. Precisamos mudar de estado, precisamos de IO etc para que o aplicativo seja útil. E, nesses casos, os métodos devem causar efeitos colaterais e, portanto, não podem ser puros. Mas a regra geral aqui é manter esses métodos "impuros" no mínimo; eles só têm efeitos colaterais porque precisam, e não como norma.

Parece-me razoável ter um estado de batalha. Como este é um jogo baseado em turnos, mantenho os estados de batalha em um dicionário (multiplayer - pode haver muitas batalhas disputadas por muitos jogadores ao mesmo tempo). Sempre que um jogador faz a sua vez, eu chamo um método apropriado no estado de batalha que (a) modifica o estado de acordo e (b) retorna atualizações para os jogadores, que são serializados no JSON e basicamente apenas lhes diz o que aconteceu no jogo. borda.

Parece mais do que razoável ter um estado de batalha para mim; parece essencial. Todo o objetivo desse código é manipular solicitações para alterar o estado, gerenciar essas alterações de estado e relatá-las novamente. Você pode lidar com esse estado globalmente, pode mantê-lo dentro de objetos individuais do jogador ou pode distribuí-lo por um conjunto de funções puras. Qual você escolhe se resume ao que funciona melhor para o seu cenário específico. O estado global simplifica o design do código e é rápido, o que é um requisito essencial para a maioria dos jogos. Mas torna o código muito mais difícil de manter, testar e depurar. Um conjunto de funções puras tornará o código mais complexo para implementar e corre o risco de ser muito lento devido à cópia excessiva de dados. Mas será o mais simples de testar e manter. A "abordagem OO" fica no meio do caminho.

A chave é: não existe uma solução perfeita que funcione o tempo todo. O objetivo das funções puras é ajudá-lo a "cair no poço do sucesso". Mas se esse poço é tão raso, devido à complexidade que pode trazer para o código, que você não cai nele tanto quanto tropeça nele, então não é a abordagem certa para você. Apontar para o ideal, mas seja pragmático e pare quando esse ideal não é um bom lugar para ir desta vez.

E, como ponto final, apenas para reiterar: funções puras e "diga, não pergunte" não são opostas.


5

Para qualquer coisa, já dito, existe um contexto, no qual você pode colocar essa afirmação, que a tornará absurda.

insira a descrição da imagem aqui

Tio Bob está completamente errado se você seguir o conselho do argumento zero como requisito. Ele está completamente certo se você entender que todos os argumentos adicionais dificultam a leitura do código. Tem um custo. Você não adiciona argumentos às funções porque as torna mais fáceis de ler. Você adiciona argumentos às funções porque não consegue pensar em um bom nome que torne óbvia a dependência desse argumento.

Por exemplo, pi()é uma função perfeitamente bem como é. Por quê? Porque eu não me importo como, ou mesmo se, foi calculado. Ou se usou e, ou sin (), para chegar ao número que retorna. Eu estou bem com isso, porque o nome me diz tudo o que eu preciso saber.

No entanto, nem todo nome me diz tudo o que preciso saber. Alguns nomes não revelam importante para entender informações que controlam o comportamento da função, assim como os argumentos expostos. É isso que facilita o raciocínio sobre o estilo funcional da programação.

Eu posso manter as coisas imutáveis ​​e livres de efeitos colaterais em um estilo completamente OOP. Return é simplesmente um mecânico usado para deixar valores na pilha para o próximo procedimento. Você pode permanecer tão imutável usando as portas de saída para comunicar valores a outras coisas imutáveis ​​até atingir a última porta de saída que finalmente precisa mudar alguma coisa, se quiser que as pessoas possam lê-la. Isso vale para todas as línguas, funcionais ou não.

Portanto, não afirme que Programação Funcional e Programação Orientada a Objetos são "fundamentalmente incompatíveis". Posso usar objetos em meus programas funcionais e funções puras em meus programas OO.

No entanto, há um custo para misturá-los: Expectativas. Você pode seguir fielmente a mecânica de ambos os paradigmas e ainda causar confusão. Um dos benefícios do uso de uma linguagem funcional é que os efeitos colaterais, embora devam existir para obter qualquer resultado, são colocados em um local previsível. A menos, é claro, que um objeto mutável seja acessado de maneira indisciplinada. Então, o que você tomou como dado naquele idioma se desfaz.

Da mesma forma, você pode oferecer suporte a objetos com funções puras, criar objetos imutáveis. O problema é que, se você não sinalizar que as funções são puras ou que os objetos são imutáveis, as pessoas não se beneficiam com esses recursos até que gastem muito tempo lendo o código.

Este não é um problema novo. Durante anos, as pessoas codificaram proceduralmente em "linguagens OO" pensando que estão fazendo OO porque usam uma "linguagem OO". Poucas línguas são boas para impedir que você se atire no pé. Para que essas idéias funcionem, elas precisam viver em você.

Ambos oferecem bons recursos. Você pode fazer as duas coisas. Se você for corajoso o suficiente para misturá-los, identifique-os claramente.


0

Às vezes, luto para entender todas as regras de vários paradigmas. Às vezes, eles estão em conflito um com o outro, pois estão nessa situação.

OOP é um paradigma imperativo que consiste em correr com uma tesoura no mundo onde coisas perigosas acontecem.

FP é um paradigma funcional em que se encontra segurança absoluta em computação pura. Nada acontece aqui.

No entanto, todos os programas devem entrar no mundo imperativo para serem úteis. Assim, núcleo funcional, shell imperativo .

As coisas ficam confusas quando você começa a definir objetos imutáveis ​​(aqueles cujos comandos retornam uma cópia modificada em vez de realmente sofrer mutação). Você diz para si mesmo: "Isso é POO" e "Estou definindo o comportamento do objeto". Você pensa no princípio já testado e testado do Tell, Don't Ask. O problema é que você está aplicando-o ao domínio errado.

Os reinos são inteiramente diferentes e cumprem regras diferentes. O domínio funcional cresce até o ponto em que deseja liberar efeitos colaterais no mundo. Para que esses efeitos sejam liberados, todos os dados que teriam sido encapsulados em um objeto imperativo (se isso tivesse sido escrito dessa maneira!) Precisam estar à disposição do shell imperativo. Sem acesso a esses dados que em um mundo diferente teriam sido ocultados via encapsulamento, ele não pode fazer o trabalho. É computacionalmente impossível.

Assim, ao escrever objetos imutáveis ​​(o que Clojure chama de estruturas de dados persistentes) lembre-se de que você está no domínio funcional. Jogue Tell, Don't Ask pela janela e deixe-a entrar em casa somente quando você entrar novamente no reino imperativo.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.