Usando filas para desacoplar funções / evitar chamadas diretas?

8

Uma espécie de questão de novato em programação funcional aqui:

Eu tenho lido as transcrições de algumas das palestras de Rich Hickey e, em várias de suas mais conhecidas, ele recomenda o uso de filas como uma alternativa para que as funções se chamam. (Por exemplo, em design, composição e desempenho e no Simple Made Easy .)

Não entendo bem isso em vários aspectos:

  1. Ele está falando sobre colocar dados em uma fila e depois fazer com que cada função os use? Então, ao invés da função A que chama a função B para realizar seu próprio cálculo, apenas a função B coloca sua saída em uma fila e, em seguida, a função A agarra? Ou, alternativamente, estamos falando sobre colocar funções em uma fila e aplicá-las sucessivamente aos dados (certamente não, porque isso envolveria mutação maciça, certo? E também multiplicação de filas para funções de múltiplas aridades, como árvores ou algo assim? )

  2. Como isso torna as coisas mais simples? Minha intuição seria que essa estratégia criaria mais complexidade, porque a fila seria um tipo de estado, e então você precisa se preocupar "e se alguma outra função aparecer e colocar alguns dados no topo da fila?"

Uma resposta para uma pergunta de implementação no SO sugere que a idéia está criando várias filas diferentes. Portanto, cada função coloca sua saída em sua própria fila (??). Mas isso também me confunde, porque se você estiver executando uma função uma vez, por que ela precisa de uma fila para sua saída, quando você pode apenas pegar essa saída e colocar um nome nela como uma (var, atom, entrada em uma grande tabela de hash, o que for). Por outro lado, se uma função estiver sendo executada várias vezes e você colocar sua saída em uma fila, infligirá um estado a si mesmo novamente e precisará se preocupar com a ordem em que tudo é chamado, as funções downstream ficam menos puras, etc.

Claramente, não estou entendendo o ponto aqui. Alguém pode explicar um pouco?

Paul Gowder
fonte
Eu não vi uma única referência a uma fila no seu primeiro link, embora eu garanto que a postagem é incrivelmente longa e eu posso ter perdido. Parece que é mais uma conversa sobre arte do que sobre programação.
Robert Harvey
As filas são brevemente mencionadas duas vezes no segundo artigo, mas não são expostas. De qualquer forma, a idéia de usar uma fila de mensagens para se comunicar entre aplicativos ou módulos existe há algum tempo. Parece improvável que você faça isso em um único aplicativo, a menos que esteja criando um pipeline de processamento ou mecanismo de estado.
21716 Robert Harvey
É no parágrafo sob o slide com o título "Leve para além do tempo / ordem / fluxo" ( "Você pode quebrar sistemas separados, então não há vocação menos direta Você pode usar filas para fazer isso.".)
Paul Gowder
3
Não vale a pena fazer uma resposta completa, mas ele está apenas dando uma descrição difusa para o conceito de pools de tarefas e programação orientada a eventos. Então, você empacota uma chamada de função em um Jobobjeto genérico , envia-o para uma fila e faz com que um ou mais threads de trabalho funcionem nessa fila. Em Jobseguida, ele envia mais Jobs para a fila após a conclusão. Os valores retornados são substituídos por retornos de chamada nesse conceito. É um pesadelo depurar e verificar se você não possui uma pilha de chamadas, eficiente e flexível pelo mesmo motivo.
Ext3h
Obrigado. Talvez o meu verdadeiro problema seja que eu não entendo as mensagens! (Heh, hora de ir para aprender Smalltalk :-)?)
Paul Gowder

Respostas:

6

É mais um exercício de design do que uma recomendação geral. Você não costuma colocar uma fila entre todas as suas chamadas de função diretas. Isso seria ridículo. No entanto, se você não projetar suas funções como se uma fila pudesse ser inserida entre qualquer uma das chamadas diretas de função, não poderá justificadamente afirmar que escreveu código reutilizável e composível. Esse é o argumento de Rich Hickey.

Esta é uma das principais razões por trás do sucesso do Apache Spark , por exemplo. Você escreve um código que parece estar fazendo chamadas diretas de função em coleções locais, e a estrutura converte esse código em passar mensagens nas filas entre nós do cluster. O tipo de estilo de codificação simples, compostável e reutilizável que Rich Hickey defende torna isso possível.

Karl Bielefeldt
fonte
Mas isso não é apenas uma mudança no processo de ligação do método? No final do dia, uma chamada de função é apenas uma chamada de função, certo? O que acontece depois disso depende do que a função faz. Portanto, parece menos fazer chamadas de função do que como a infraestrutura subjacente é projetada.
21816 Robert Harvey
1
Em outras palavras, que alteração você faria nas chamadas de função para torná-las "amigáveis ​​à fila"?
21816 Robert Harvey
5
Lembre-se de que o código do outro lado da fila não tem necessariamente acesso à mesma memória e IO. As funções amigáveis ​​à fila estão livres de efeitos colaterais e esperam entradas e produzem saídas imutáveis ​​e facilmente serializáveis. Não é um teste tão fácil de encontrar em muitas bases de código.
Karl Bielefeldt
3
Ah, então "programação funcional amigável" então. Meio que faz sentido, já que Rich Hickey está discutindo isso.
21716 Robert Harvey
0

Uma coisa a observar é que a programação funcional permite conectar funções entre si indiretamente por meio de objetos mediadores que cuidam da aquisição de argumentos para alimentar as funções e rotear de maneira flexível seus resultados para os destinatários que desejam seus resultados. Então, suponha que você tenha algum código de chamada direta simples que se parece com este exemplo, em Haskell:

myThing :: A -> B -> C
myThing a b = f a (g a b)

Bem, usando de Haskell Applicativeclasse e seus <$>e <*>operadores podemos mecanicamente reescrever o código para isso:

myThing :: Applicative f => f A -> f B -> f C
myThing a b = f <$> a <*> (g <$> a <*> b)

... onde agora myThingnão está mais chamando diretamente fe g, mas conectando-os através de alguns mediadores do tipo f. Assim, por exemplo, fpoderia ser algum Streamtipo fornecido por uma biblioteca que fornece uma interface para um sistema de filas; nesse caso, teríamos esse tipo:

myThing :: Stream A -> Stream B -> Stream C
myThing a b = f <$> a <*> (g <$> a <*> b)

Sistemas como este existem. De fato, você pode ver os fluxos do Java 8 como uma versão desse paradigma. Você obtém código como este:

List<Integer> transactionsIds = 
    transactions.parallelStream()
                .filter(t -> t.getType() == Transaction.GROCERY)
                .sorted(comparing(Transaction::getValue).reversed())
                .map(Transaction::getId)
                .collect(toList());

Aqui você está usando as seguintes funções:

  • t -> t.getType() == Transaction.GROCERY
  • comparing(Transaction::getValue).reversed()
  • Transaction::getId
  • toList()

... e, em vez de fazer com que eles se liguem diretamente, você está usando o Streamsistema para mediar entre eles. Este exemplo de código não está chamando a Transaction::getIdfunção diretamente - Streamestá chamando-a com as transações que sobreviveram à anterior filter. Você pode pensar Streamnisso como um tipo muito mínimo de fila que une funções indiretamente e roteia valores entre elas.

sacundim
fonte