OK, então o título é um pouco clickbaity, mas sério, eu já falei, não peça chute por um tempo. Gosto de como incentiva os métodos a serem usados como mensagens de maneira realmente orientada a objetos. Mas isso tem um problema incômodo que tem sido perturbador na minha cabeça.
Cheguei a suspeitar que código bem escrito pode seguir os princípios OO e princípios funcionais ao mesmo tempo. Estou tentando conciliar essas idéias e o grande ponto de discórdia em que aterrei é return
.
Uma função pura tem duas qualidades:
Chamá-lo repetidamente com as mesmas entradas sempre dá o mesmo resultado. Isso implica que é imutável. Seu estado é definido apenas uma vez.
Não produz efeitos colaterais. A única alteração causada pela chamada está produzindo o resultado.
Então, como alguém pode ser puramente funcional se você jurou usar return
como forma de comunicar resultados?
O dizer, não pergunte que a ideia funciona usando o que alguns considerariam um efeito colateral. Quando trato de um objeto, não o pergunto sobre seu estado interno. Eu digo o que preciso fazer e ele usa seu estado interno para descobrir o que fazer com o que eu disse para fazer. Depois que digo, não pergunto o que fez. Só espero que tenha feito algo sobre o que foi dito para fazer.
Penso em Tell, Don't Ask como mais do que apenas um nome diferente para encapsulamento. Quando uso return
, não faço ideia do que me chamou. Não posso falar do protocolo, tenho que forçá-lo a lidar com o meu protocolo. Que em muitos casos é expresso como o estado interno. Mesmo que o que está exposto não seja exatamente o estado, geralmente são apenas alguns cálculos realizados nos argumentos de estado e entrada. Ter uma interface para responder oferece a chance de massagear os resultados em algo mais significativo que o estado interno ou os cálculos. Isso é passagem de mensagem . Veja este exemplo .
No passado, quando unidades de disco tinham discos e um pen drive era o que você fazia no carro quando o volante estava frio demais para tocar com os dedos, fui ensinado como as pessoas irritantes consideram funções que possuem parâmetros fora dos parâmetros. void swap(int *first, int *second)
parecia tão útil, mas fomos encorajados a escrever funções que retornassem os resultados. Então, levei isso a sério pela fé e comecei a segui-la.
Mas agora vejo pessoas construindo arquiteturas onde objetos permitem que como eles foram construídos controlem para onde enviam seus resultados. Aqui está um exemplo de implementação . Injetar o objeto da porta de saída parece um pouco com a idéia do parâmetro out novamente. Mas é assim que os objetos dizer e não perguntar dizem a outros objetos o que fizeram.
Quando soube dos efeitos colaterais, pensei nele como o parâmetro de saída. Estávamos nos dizendo para não surpreender as pessoas, fazendo com que parte do trabalho acontecesse de maneira surpreendente, ou seja, não seguindo a return result
convenção. Agora, claro, eu sei que há uma pilha de problemas de encadeamento assíncrono paralelo com os quais os efeitos colaterais se complicam, mas o retorno é realmente apenas uma convenção que faz com que você deixe o resultado pressionado na pilha para que, como quer que seja chamado, você poderá soltá-lo mais tarde. Isso é tudo o que realmente é.
O que estou realmente tentando perguntar:
É return
a única maneira de evitar todo esse efeito colateral da miséria e obter segurança de linha sem travas, etc. Ou posso seguir dizer, não pergunte de uma maneira puramente funcional?
fonte
Respostas:
Se uma função não tem efeitos colaterais e não retorna nada, a função é inútil. É tão simples quanto isso.
Mas acho que você pode usar alguns truques se quiser seguir a letra das regras e ignorar o raciocínio subjacente. Por exemplo, o uso de um parâmetro out é, estritamente falando, não o retorno. Mas ainda faz exatamente o mesmo que um retorno, apenas de uma maneira mais complicada. Portanto, se você acredita que o retorno é ruim por um motivo , o uso de um parâmetro out é claramente ruim pelos mesmos motivos subjacentes.
Você pode usar truques mais complicados. Por exemplo, Haskell é famoso pelo truque de IO da mônada, onde você pode ter efeitos colaterais na prática, mas ainda não estritamente falando, tem efeitos colaterais do ponto de vista teórico. O estilo de passagem de continuação é outro truque, que permite evitar retornos ao preço de transformar seu código em espaguete.
A conclusão é que, na ausência de truques bobos, os dois princípios das funções livres de efeitos colaterais e "sem retorno" simplesmente não são compatíveis. Além disso, vou apontar que os dois princípios são realmente ruins (dogmas realmente) em primeiro lugar, mas essa é uma discussão diferente.
Regras como "diga, não pergunte" ou "sem efeitos colaterais" não podem ser aplicadas universalmente. Você sempre tem que considerar o contexto. Um programa sem efeitos colaterais é literalmente inútil. Mesmo linguagens funcionais puras reconhecem isso. Em vez disso, eles se esforçam para separar as partes puras do código daquelas com efeitos colaterais. O ponto das mônadas do Estado ou da IO em Haskell não é que você evite efeitos colaterais - porque você não pode -, mas que a presença de efeitos colaterais é explicitamente indicada pela assinatura da função.
A regra de dizer não perguntar se aplica a um tipo diferente de arquitetura - o estilo em que os objetos no programa são "atores" independentes que se comunicam. Cada ator é basicamente autônomo e encapsulado. Você pode enviar uma mensagem e decide como reagir a ela, mas não pode examinar o estado interno do ator de fora. Isso significa que você não pode dizer se uma mensagem altera o estado interno do ator / objeto. Estado e efeitos colaterais são ocultos por design .
fonte
Tell, Don't Ask vem com algumas suposições fundamentais:
Nenhuma dessas coisas se aplica a funções puras.
Então, vamos revisar por que temos a regra "Diga, não pergunte". Esta regra é um aviso e um lembrete. Pode ser resumido assim:
Em outras palavras, as classes são as únicas responsáveis por manter seu próprio estado e agir sobre ele. É disso que se trata o encapsulamento.
Partida Fowler :
Para reiterar, nada disso tem a ver com funções puras, ou mesmo impuras, a menos que você esteja expondo o estado de uma classe ao mundo exterior. Exemplos:
Violação de TDA
Não é uma violação da TDA
ou
Nos dois exemplos anteriores, o semáforo mantém o controle de seu estado e de suas ações.
fonte
Você não apenas pede seu estado interno , mas também não tem um estado interno .
Diga também , não pergunte! que não implicam não conseguir um resultado na forma de um valor de retorno (fornecido por um
return
declaração dentro do método). Isso implica que eu não me importo como você faz isso, mas faça esse processamento! . E às vezes você quer imediatamente o resultado do processamento ...fonte
next()
método não deve retornar apenas o objeto atual, mas também alterar as iterators estado interno para que a próxima chamada retorna o próximo objeto ...Se você considera
return
"prejudicial" (para ficar na sua foto), em vez de fazer uma função comoconstrua-o de maneira a transmitir mensagens:
Contanto que
f
eg
sejam livres de efeitos colaterais, encadeá-los também será livre de efeitos colaterais. Eu acho que esse estilo é semelhante ao que também é chamado de estilo de passagem de continuação .Se isso realmente leva a programas "melhores", é discutível, pois quebra algumas convenções. O engenheiro de software alemão Ralf Westphal criou um modelo de programação completo em torno disso, que chamou de "Componentes Baseados em Eventos", com uma técnica de modelagem que ele chama de "Design de Fluxo".
Para ver alguns exemplos, comece na seção "Traduzindo para eventos" desta entrada do blog . Para uma abordagem completa, recomendo o seu e-book "Mensagens como modelo de programação - Fazendo POO como se você quisesse" .
fonte
If this really leads to "better" programs is debatable
Nós apenas temos que olhar para o código escrito em JavaScript durante a primeira década deste século. Jquery e seus plugins eram propensos a esse paradigma de retorno de chamada ... retornos de chamada em todos os lugares . Em um determinado momento, muitos retornos de chamada aninhados , tornaram a depuração um pesadelo. Código ainda tem que ser lido por seres humanos, independentemente das excentricidades da engenharia de software e seus "princípios"return
dados de funções e ainda aderir ao Tell Don't Ask, desde que não controle o estado de um objeto.A passagem de mensagens é inerentemente eficaz. Se você disser a um objeto que faça algo, espera que ele tenha efeito sobre algo. Se o manipulador de mensagens fosse puro, você não precisaria enviar uma mensagem.
Nos sistemas de atores distribuídos, o resultado de uma operação geralmente é enviado como uma mensagem de volta ao remetente da solicitação original. O remetente da mensagem é disponibilizado implicitamente pelo tempo de execução do ator ou é (por convenção) explicitamente passado como parte da mensagem. Na passagem de mensagens síncronas, uma única resposta é semelhante a uma
return
declaração. Na passagem assíncrona de mensagens, o uso de mensagens de resposta é particularmente útil, pois permite o processamento simultâneo em vários atores enquanto ainda fornece resultados.Passar o "remetente" para o qual o resultado deve ser entregue explicitamente basicamente modela o estilo de passagem de continuação ou os parâmetros temidos - exceto que ele passa mensagens para eles em vez de modificá-los diretamente.
fonte
Essa pergunta toda me parece uma 'violação de nível'.
Você possui (pelo menos) os seguintes níveis em um projeto importante:
E assim por diante, até tokens individuais.
Não há realmente nenhuma necessidade de uma entidade no nível de método / função não retornar (mesmo que apenas retorne
this
). E não há (na sua descrição) a necessidade de uma entidade no nível do ator retornar algo (dependendo do idioma que talvez nem seja possível). Eu acho que a confusão está em confluir esses dois níveis, e eu argumentaria que eles deveriam ser fundamentados de maneira distinta (mesmo que um objeto em particular abranja vários níveis).fonte
Você menciona que deseja se conformar tanto ao princípio da POO de "contar, não perguntar" quanto ao princípio funcional das funções puras, mas não vejo bem como isso o levou a evitar a declaração de retorno.
Uma maneira alternativa relativamente comum de seguir esses dois princípios é incluir as declarações de retorno e usar objetos imutáveis apenas com getters. A abordagem, então, é que alguns dos getters retornem um objeto semelhante com um novo estado, em vez de alterar o estado do objeto original.
Um exemplo dessa abordagem está nos tipos
tuple
efrozenset
dados internos do Python . Aqui está um uso típico de um frozenset:O que imprimirá o seguinte, demonstrando que o método de união cria um novo frozenset com seu próprio estado sem afetar os objetos antigos:
Outro exemplo extenso de estruturas de dados imutáveis semelhantes é a biblioteca Immutable.js do Facebook . Nos dois casos, você começa com esses blocos de construção e pode criar objetos de domínio de nível superior que seguem os mesmos princípios, obtendo uma abordagem OOP funcional, que ajuda a encapsular os dados e raciocinar sobre eles com mais facilidade. E a imutabilidade também permite que você colha o benefício de poder compartilhar esses objetos entre threads sem ter que se preocupar com bloqueios.
fonte
Eu tenho tentado o meu melhor para conciliar alguns dos benefícios de, mais especificamente, programação imperativa e funcional (naturalmente não obtendo todos os benefícios, mas tentando obter a maior parte de ambos), embora
return
seja realmente fundamental para fazer isso em de uma maneira direta para mim em muitos casos.Com relação a tentar evitar
return
declarações diretas, tentei ponderar sobre isso durante a última hora ou mais e, basicamente, a pilha transbordou meu cérebro várias vezes. Eu posso ver o apelo disso em termos de impor o nível mais forte de encapsulamento e informações ocultos em favor de objetos muito autônomos, que são instruídos apenas para o que fazer, e eu gosto de explorar as extremidades das idéias, apenas para tentar melhorar. compreensão de como eles funcionam.Se usarmos o exemplo do semáforo, imediatamente uma tentativa ingênua desejaria fornecer a esse semáforo o conhecimento de todo o mundo que o cerca e que certamente seria indesejável do ponto de vista do acoplamento. Portanto, se eu entendi corretamente, você abstrai isso e se dissocia em favor da generalização do conceito de portas de E / S que propagam ainda mais mensagens e solicitações, não dados, através do pipeline, e basicamente injeta esses objetos nas interações / solicitações desejadas entre si. enquanto alheios um ao outro.
O Pipeline Nodal
E esse diagrama é o mais longe que eu tentei esboçar isso (e, embora simples, eu tive que continuar mudando e repensando). Imediatamente, acho que um projeto com esse nível de dissociação e abstração se tornaria muito difícil de raciocinar em forma de código, porque o (s) orquestrador (es) que conectam todas essas coisas para um mundo complexo pode achar muito difícil acompanhe todas essas interações e solicitações para criar o pipeline desejado. Na forma visual, no entanto, pode ser razoavelmente simples desenhar essas coisas como um gráfico e vincular tudo e ver as coisas acontecendo de forma interativa.
Em termos de efeitos colaterais, eu pude ver isso livre de "efeitos colaterais", no sentido de que essas solicitações poderiam, na pilha de chamadas, levar a uma cadeia de comandos para cada thread executar, por exemplo (eu não conto isso como um "efeito colateral" em um sentido pragmático, pois não altera nenhum estado relevante para o mundo exterior até que esses comandos sejam realmente executados - o objetivo prático para mim na maioria dos softwares não é eliminar os efeitos colaterais, mas adiá-los e centralizá-los) . Além disso, a execução do comando pode gerar um novo mundo, em vez de alterar o existente. Meu cérebro está realmente sobrecarregado apenas tentando compreender tudo isso, no entanto, não há nenhuma tentativa de criar um protótipo dessas idéias. Eu também não
Como funciona
Então, para esclarecer, eu estava imaginando como você realmente programa isso. A maneira como eu a via trabalhando era, na verdade, o diagrama acima, capturando o fluxo de trabalho do usuário (programador). Você pode arrastar um semáforo para o mundo, arrastar um cronômetro, passar um período decorrido (após "construí-lo"). O cronômetro possui um
On Interval
evento (porta de saída), você pode conectá-lo ao semáforo para que, nesses eventos, esteja dizendo à luz para percorrer suas cores.O semáforo pode então, ao mudar para determinadas cores, emitir saídas (eventos) como,
On Red
nesse ponto, podemos arrastar um pedestre para o nosso mundo e fazer com que esse evento diga ao pedestre para começar a andar ... ou podemos arrastar os pássaros para dentro. nossa cena e fazemos isso quando a luz fica vermelha, dizemos aos pássaros que voem e batem as asas ... ou talvez, quando a luz fica vermelha, dizemos uma bomba para explodir - o que quisermos, e com os objetos sendo completamente alheios um ao outro e não fazendo nada além de dizer indiretamente um ao outro o que fazer com esse conceito abstrato de entrada / saída.E eles encapsulam completamente seu estado e não revelam nada sobre ele (a menos que esses "eventos" sejam considerados IMT, quando eu precisaria repensar bastante as coisas), eles dizem um ao outro para fazer indiretamente, não perguntam. E eles são super dissociados. Nada sabe sobre nada, exceto essa abstração de porta de entrada / saída generalizada.
Casos de uso prático?
Eu pude ver esse tipo de coisa sendo útil como uma linguagem incorporada específica de domínio de alto nível em certos domínios para orquestrar todos esses objetos autônomos que não sabem nada sobre o mundo circundante, não expõem nada de seu estado interno após a construção e basicamente propagam solicitações entre nós, que podemos mudar e ajustar o conteúdo de nossos corações. No momento, sinto que isso é muito específico do domínio, ou talvez não tenha pensado o suficiente, porque é muito difícil envolver meu cérebro com os tipos de coisas que desenvolvo regularmente (costumo trabalhar com código de nível médio baixo) se eu interpretasse Tell, Don't Ask a tais extremidades e desejasse o nível mais forte de encapsulamento imaginável. Mas se estivermos trabalhando com abstrações de alto nível em um domínio específico,
Sinais e Slots
Esse design me pareceu estranhamente familiar até que percebi que são basicamente sinais e slots, se não levarmos em conta as nuances de como ele é implementado. A principal questão para mim é a eficácia com que podemos programar esses nós individuais (objetos) no gráfico, seguindo rigorosamente o Tell, Don't Ask, levado ao grau de evitar
return
declarações e se podemos avaliar o referido gráfico sem mutações (em paralelo, por exemplo, bloqueio ausente). É aí que os benefícios mágicos não estão na maneira como ligamos essas coisas potencialmente, mas em como elas podem ser implementadas com esse grau de mutação ausente no encapsulamento. Ambos parecem viáveis para mim, mas não tenho certeza de quão amplamente aplicável seria, e é aí que estou um pouco perplexo tentando trabalhar com possíveis casos de uso.fonte
Eu vejo claramente vazamento de certeza aqui. Parece que "efeito colateral" é um termo conhecido e comumente entendido, mas, na realidade, não é. Dependendo das suas definições (que realmente estão faltando no OP), os efeitos colaterais podem ser totalmente necessários (como o @JacquesB conseguiu explicar) ou inaceitáveis e inaceitáveis. Ou, dando um passo em direção ao esclarecimento, é necessário distinguir entre os efeitos colaterais desejados que não se gosta de esconder (nesse ponto, o famoso IO de Haskell emerge: não é senão uma maneira de ser explícito) e efeitos colaterais indesejados como resultado de erros de código e esse tipo de coisa . Esses são problemas bem diferentes e, portanto, exigem um raciocínio diferente.
Então, sugiro começar a reformular a si mesmo: "Como definimos efeito colateral e o que as definições dadas dizem sobre sua inter-relação com a declaração" return "?".
fonte