Eu li o artigo da Wikipedia sobre programação reativa . Também li o pequeno artigo sobre programação reativa funcional . As descrições são bastante abstratas.
- O que significa programação reativa funcional (FRP) na prática?
- Em que consiste a programação reativa (em oposição à programação não reativa?)?
Minha formação é em linguagens imperativas / OO, então uma explicação relacionada a esse paradigma seria apreciada.
Respostas:
Se quiser ter uma idéia do FRP, comece com o antigo tutorial de Fran de 1998, que tem ilustrações animadas. Para artigos, comece com Animação Reativa Funcional e siga os links no link de publicações na minha home page e o link FRP no wiki da Haskell .
Pessoalmente, gosto de pensar no que significa FRP antes de abordar como ele pode ser implementado. (Código sem especificação é uma resposta sem uma pergunta e, portanto, "nem errado".) Portanto, não descrevo o FRP em termos de representação / implementação, como Thomas K em outra resposta (gráficos, nós, arestas, disparo, execução, etc). Existem muitos estilos de implementação possíveis, mas nenhuma implementação diz o que é FRP .
Eu ressoo com a descrição simples de Laurence G de que o FRP é sobre "tipos de dados que representam um valor 'ao longo do tempo'". A programação imperativa convencional captura esses valores dinâmicos apenas indiretamente, através de estado e mutações. A história completa (passado, presente, futuro) não tem representação de primeira classe. Além disso, apenas valores em evolução discreta podem ser (indiretamente) capturados, uma vez que o paradigma imperativo é temporalmente discreto. Por outro lado, o FRP captura esses valores em evolução diretamente e não tem dificuldade em evoluir continuamente .
O FRP também é incomum, pois é concorrente sem colidir com o ninho de ratos teórico e pragmático que assola a concorrência imperativa. Semanticamente, a simultaneidade do FRP é refinada , determinada e contínua . (Estou falando de significado, não de implementação. Uma implementação pode ou não envolver simultaneidade ou paralelismo.) A determinação semântica é muito importante para o raciocínio, rigoroso e informal. Embora a concorrência acrescente uma complexidade enorme à programação imperativa (devido à intercalação não determinística), ela é fácil no FRP.
Então, o que é FRP? Você poderia ter inventado você mesmo. Comece com estas idéias:
Valores dinâmicos / em evolução (ou seja, valores "ao longo do tempo") são valores de primeira classe em si mesmos. Você pode defini-los e combiná-los, passá-los para dentro e fora das funções. Eu chamei essas coisas de "comportamentos".
Os comportamentos são criados a partir de algumas primitivas, como comportamentos constantes (estáticos) e tempo (como um relógio), e depois com combinações sequenciais e paralelas. Os comportamentos n são combinados aplicando uma função n-ária (em valores estáticos), "ponto a ponto", ou seja, continuamente ao longo do tempo.
Para explicar fenômenos discretos, tenha outro tipo (família) de "eventos", cada um dos quais com um fluxo (finito ou infinito) de ocorrências. Cada ocorrência tem um tempo e valor associados.
Para criar o vocabulário de composição a partir do qual todos os comportamentos e eventos podem ser construídos, brinque com alguns exemplos. Continue desconstruindo em peças mais gerais / simples.
Para que você saiba que está em uma base sólida, forneça a todo o modelo uma base de composição, usando a técnica da semântica denotacional, o que significa apenas que (a) cada tipo tem um tipo matemático simples e preciso correspondente de "significados" e ( b) cada primitivo e operador tem um significado simples e preciso em função dos significados dos constituintes. Nunca, nunca misture considerações de implementação em seu processo de exploração. Se essa descrição lhe parecer absurda, consulte (a) design denotacional com morfismos de classe de tipo , (b) programação reativa funcional push-pull (ignorando os bits de implementação) e (c) a página de wikilivros de Denotational Semantics Haskell. Observe que a semântica denotacional tem duas partes, de seus dois fundadores, Christopher Strachey e Dana Scott: a parte Strachey, mais fácil e útil, e a parte Scott, mais difícil e menos útil (para o design de software).
Se você seguir esses princípios, espero que consiga algo mais ou menos no espírito do FRP.
Onde consegui esses princípios? No design de software, eu sempre faço a mesma pergunta: "o que isso significa?". A semântica denotacional me deu uma estrutura precisa para essa questão e que se encaixa na minha estética (diferente da semântica operacional ou axiomática, ambas as quais me deixam insatisfeito). Então eu me perguntei o que é comportamento? Logo percebi que a natureza temporalmente discreta da computação imperativa é uma acomodação a um estilo particular de máquina , em vez de uma descrição natural do próprio comportamento. A descrição mais simples e precisa do comportamento em que consigo pensar é simplesmente "função do tempo (contínuo)", então esse é o meu modelo. Deliciosamente, esse modelo lida com simultaneidade determinística contínua com facilidade e graça.
Tem sido um grande desafio implementar esse modelo de maneira correta e eficiente, mas isso é outra história.
fonte
Na programação funcional pura, não há efeitos colaterais. Para muitos tipos de software (por exemplo, qualquer coisa com interação do usuário), os efeitos colaterais são necessários em algum nível.
Uma maneira de obter um efeito colateral como um efeito coletivo e ainda manter um estilo funcional é usar a programação reativa funcional. Esta é a combinação de programação funcional e programação reativa. (O artigo da Wikipedia ao qual você vinculou é sobre o último.)
A idéia básica por trás da programação reativa é que existem certos tipos de dados que representam um valor "ao longo do tempo". Os cálculos que envolvem esses valores de mudança ao longo do tempo terão eles mesmos valores que mudam ao longo do tempo.
Por exemplo, você pode representar as coordenadas do mouse como um par de valores de número inteiro ao longo do tempo. Digamos que tivemos algo como (isto é pseudo-código):
A qualquer momento, x e y teriam as coordenadas do mouse. Diferentemente da programação não reativa, precisamos fazer essa atribuição apenas uma vez, e as variáveis x e y permanecerão "atualizadas" automaticamente. É por isso que a programação reativa e a programação funcional funcionam tão bem juntas: a programação reativa elimina a necessidade de alterar variáveis enquanto ainda permite fazer muito do que você poderia realizar com mutações variáveis.
Se fizermos alguns cálculos com base nisso, os valores resultantes também serão aqueles que mudam ao longo do tempo. Por exemplo:
Neste exemplo,
minX
sempre será 16 menor que a coordenada x do ponteiro do mouse. Com bibliotecas reativas, você poderia dizer algo como:E uma caixa de 32 x 32 será desenhada ao redor do ponteiro do mouse e o rastreará onde quer que ele se mova.
Aqui está um artigo muito bom sobre programação reativa funcional .
fonte
sqrt(x)
C com sua macro, isso apenas calculasqrt(mouse_x())
e me devolve um duplo. Em um verdadeiro sistema reativo funcional,sqrt(x)
retornaria um novo "dobro ao longo do tempo". Se você tentasse simular um sistema de FR,#define
teria que jurar variáveis em favor de macros. Os sistemas FR também normalmente recalculam as coisas quando precisam ser recalculadas, enquanto o uso de macros significa que você está constantemente reavaliando tudo, até as subexpressões.Uma maneira fácil de alcançar uma primeira intuição sobre como é imaginar o seu programa é uma planilha e todas as suas variáveis são células. Se alguma das células de uma planilha for alterada, as células que se referem a essa célula também serão alteradas. É o mesmo com o FRP. Agora imagine que algumas células mudam por conta própria (ou melhor, são retiradas do mundo exterior): em uma situação de GUI, a posição do mouse seria um bom exemplo.
Isso necessariamente perde muito. A metáfora se decompõe muito rapidamente quando você realmente usa um sistema FRP. Por um lado, geralmente existem tentativas de modelar eventos discretos também (por exemplo, o mouse sendo clicado). Só estou colocando isso aqui para ter uma idéia de como é.
fonte
Para mim, são cerca de 2 significados diferentes de símbolos
=
:x = sin(t)
meios, quex
é nome diferente parasin(t)
. Então escreverx + y
é a mesma coisa quesin(t) + y
. A programação reativa funcional é como a matemática a esse respeito: se você escrevex + y
, é calculada com qualquer valor quet
seja no momento em que é usado.x = sin(t)
existe uma atribuição: significa quex
armazena o valor dasin(t)
tomada no momento da atribuição.fonte
x = sin(t)
significax
o valor desin(t)
para o dadot
. É não um nome diferente parasin(t)
a função. Caso contrário, seriax(t) = sin(t)
.2 + 3 = 5
oua**2 + b**2 = c**2
.OK, a partir do conhecimento de base e da leitura da página da Wikipedia para a qual você apontou, parece que a programação reativa é algo como computação de fluxo de dados, mas com "estímulos" externos específicos acionando um conjunto de nós para disparar e executar seus cálculos.
Isso é bastante adequado ao design da interface do usuário, por exemplo, no qual tocar em um controle da interface do usuário (por exemplo, o controle de volume em um aplicativo de reprodução de música) pode precisar atualizar vários itens de exibição e o volume real da saída de áudio. Quando você modifica o volume (um controle deslizante, digamos) que corresponda à modificação do valor associado a um nó em um gráfico direcionado.
Vários nós com bordas desse nó "valor do volume" seriam acionados automaticamente e quaisquer cálculos e atualizações necessários naturalmente se propagariam pelo aplicativo. A aplicação "reage" ao estímulo do usuário. A programação reativa funcional seria apenas a implementação dessa idéia em uma linguagem funcional, ou geralmente dentro de um paradigma de programação funcional.
Para saber mais sobre "computação de fluxo de dados", pesquise essas duas palavras na Wikipedia ou use seu mecanismo de pesquisa favorito. A idéia geral é a seguinte: o programa é um gráfico direcionado de nós, cada um executando um cálculo simples. Esses nós são conectados entre si por links gráficos que fornecem as saídas de alguns nós para as entradas de outros.
Quando um nó aciona ou executa seu cálculo, os nós conectados às suas saídas têm suas entradas correspondentes "acionadas" ou "marcadas". Qualquer nó com todas as entradas acionadas / marcadas / disponíveis é acionado automaticamente. O gráfico pode estar implícito ou explícito, dependendo exatamente de como a programação reativa é implementada.
Os nós podem ser vistos como disparados em paralelo, mas geralmente são executados em série ou com paralelismo limitado (por exemplo, pode haver alguns threads em execução). Um exemplo famoso foi o Manchester Dataflow Machine , que (IIRC) usou uma arquitetura de dados com tags para agendar a execução de nós no gráfico por meio de uma ou mais unidades de execução. A computação de fluxo de dados é bastante adequada para situações nas quais disparar cálculos de forma assíncrona, dando origem a cascatas de cálculos, funciona melhor do que tentar que a execução seja governada por um relógio (ou relógios).
A programação reativa importa essa idéia de "cascata de execução" e parece pensar no programa de maneira semelhante ao fluxo de dados, mas com a condição de que alguns dos nós estejam conectados ao "mundo externo" e as cascatas de execução são acionadas quando essas informações sensoriais como nós mudam. A execução do programa pareceria algo análogo a um arco reflexo complexo. O programa pode ou não ser basicamente séssil entre estímulos ou pode se estabelecer em um estado basicamente séssil entre estímulos.
A programação "não reativa" seria uma programação com uma visão muito diferente do fluxo de execução e relacionamento com entradas externas. É provável que seja um pouco subjetivo, pois as pessoas provavelmente serão tentadas a dizer qualquer coisa que responda a insumos externos "reaja" a elas. Mas, olhando para o espírito da coisa, um programa que pesquisa uma fila de eventos em um intervalo fixo e despacha todos os eventos encontrados para funções (ou threads) é menos reativo (porque apenas atende à entrada do usuário em um intervalo fixo). Novamente, é o espírito da coisa aqui: pode-se imaginar colocar uma implementação de pesquisa com um rápido intervalo de pesquisa em um sistema em um nível muito baixo e programar de forma reativa sobre ele.
fonte
Depois de ler muitas páginas sobre o FRP, finalmente me deparei com este texto esclarecedor sobre o FRP, que finalmente me fez entender o que realmente é o FRP.
Cito abaixo Heinrich Apfelmus (autor de banana reativa).
Então, no meu entendimento, um programa de FRP é um conjunto de equações:
j
é discreto: 1,2,3,4 ...f
depende,t
portanto, isso incorpora a possibilidade de modelar estímulos externostodo o estado do programa é encapsulado em variáveis
x_i
A biblioteca FRP cuida do progresso do tempo, em outras palavras, do
j
paraj+1
.Eu explico essas equações com muito mais detalhes neste vídeo.
EDITAR:
Cerca de 2 anos após a resposta original, cheguei recentemente à conclusão de que as implementações de FRP têm outro aspecto importante. Eles precisam (e geralmente resolvem) resolver um problema prático importante: invalidação de cache .
As equações para
x_i
-s descrevem um gráfico de dependência. Quando algumasx_i
alterações são alteradas no momentoj
, nem todos os outrosx_i'
valoresj+1
precisam ser atualizados, portanto nem todas as dependências precisam ser recalculadas, porque algumasx_i'
podem ser independentesx_i
.Além disso,
x_i
-s que mudam podem ser atualizados incrementalmente. Por exemplo, vamos considerar uma operação de mapaf=g.map(_+1)
no Scala, ondef
eg
sãoList
deInts
. Aquif
corresponde ax_i(t_j)
eg
éx_j(t_j)
. Agora, se eu acrescentar um elemento ag
ele, seria um desperdício realizar amap
operação para todos os elementosg
. Algumas implementações de FRP (por exemplo, reflex-frp ) visam solucionar esse problema. Esse problema também é conhecido como computação incremental.Em outras palavras, comportamentos (
x_i
-s) no FRP podem ser pensados como cálculos em cache. É tarefa do mecanismo FRP invalidar e recalcular com eficiência esses cache-s (osx_i
-s) se alguns delesf_i
mudarem.fonte
j+1
". Em vez disso, pense nas funções de tempo contínuo. Como Newton, Leibniz e outros nos mostraram, muitas vezes é profundamente útil (e "natural" em sentido literal) descrever essas funções de maneira diferenciada, mas contínua, usando integrais e sistemas de EDOs. Caso contrário, você está descrevendo um algoritmo de aproximação (e um fraco) em vez da coisa em si.O artigo A reatividade funcional simplesmente eficiente de Conal Elliott ( PDF direto , 233 KB) é uma introdução bastante boa. A biblioteca correspondente também funciona.
O artigo agora é substituído por outro artigo, a programação reativa funcional Push-pull ( PDF direto , 286 KB).
fonte
Isenção de responsabilidade: minha resposta está no contexto de rx.js - uma biblioteca de 'programação reativa' para Javascript.
Na programação funcional, em vez de iterar através de cada item de uma coleção, você aplica funções de ordem superior (HoFs) à própria coleção. Portanto, a idéia por trás do FRP é que, em vez de processar cada evento individual, crie um fluxo de eventos (implementado com um observável *) e aplique HoFs a ele. Dessa forma, você pode visualizar o sistema como pipelines de dados que conectam editores a assinantes.
As principais vantagens do uso de um observável são:
i) ele abstrai o estado ausente do seu código, por exemplo, se você deseja que o manipulador de eventos seja acionado apenas para cada 'n' ésimo evento ou pare de disparar após os primeiros 'n', ou comece a disparar somente após os primeiros eventos 'n', você pode simplesmente usar os HoFs (filtro, takeUntil, pular respectivamente) em vez de configurar, atualizar e verificar contadores.
ii) melhora a localização do código - se você tiver 5 manipuladores de eventos diferentes alterando o estado de um componente, poderá mesclar seus observáveis e definir um único manipulador de eventos no observável mesclado, combinando efetivamente 5 manipuladores de eventos em 1. Isso torna muito É fácil raciocinar sobre quais eventos em todo o sistema podem afetar um componente, pois estão todos presentes em um único manipulador.
Um Iterable é uma sequência consumida preguiçosamente - cada item é puxado pelo iterador sempre que deseja usá-lo e, portanto, a enumeração é direcionada pelo consumidor.
Um observável é uma sequência produzida preguiçosamente - cada item é enviado ao observador sempre que é adicionado à sequência e, portanto, a enumeração é conduzida pelo produtor.
fonte
Cara, essa é uma ideia brilhante! Por que eu não descobri isso em 1998? Enfim, aqui está a minha interpretação do tutorial Fran . Sugestões são bem-vindas, estou pensando em iniciar um mecanismo de jogo com base nisso.
Resumindo: se todo componente puder ser tratado como um número, todo o sistema poderá ser tratado como uma equação matemática, certo?
fonte
O livro de Paul Hudak, A Escola de Expressão Haskell , não é apenas uma boa introdução a Haskell, mas também passa bastante tempo em FRP. Se você é iniciante no FRP, recomendo que você tenha uma idéia de como o FRP funciona.
Há também o que parece ser uma nova reescrita deste livro (lançado em 2011, atualizado em 2014), The Haskell School of Music .
fonte
De acordo com as respostas anteriores, parece que matematicamente, simplesmente pensamos em uma ordem superior. Em vez de pensar em um valor x com o tipo X , pensamos em uma função x : T → X , onde T é o tipo de tempo, sejam os números naturais, os inteiros ou o continuum. Agora, quando escrevemos y : = x + 1 na linguagem de programação, na verdade queremos dizer a equação y ( t ) = x ( t ) + 1.
fonte
Atua como uma planilha, conforme observado. Geralmente, com base em uma estrutura orientada a eventos.
Como em todos os "paradigmas", sua novidade é discutível.
Pela minha experiência em redes de atores de fluxo distribuído, pode facilmente ser vítima de um problema geral de consistência de estado na rede de nós, ou seja, você acaba com muita oscilação e interceptação em loops estranhos.
Isso é difícil de evitar, pois algumas semânticas implicam loops ou transmissão referenciais e podem ser bastante caóticas à medida que a rede de atores converge (ou não) em algum estado imprevisível.
Da mesma forma, alguns estados podem não ser alcançados, apesar de terem bordas bem definidas, porque o estado global se afasta da solução. 2 + 2 pode ou não chegar a 4, dependendo de quando os 2 se tornaram 2, e se eles permaneceram assim. As planilhas possuem relógios síncronos e detecção de loop. Atores distribuídos geralmente não.
Tudo muito divertido :).
fonte
Encontrei este belo vídeo no subreddit Clojure sobre FRP. É muito fácil entender, mesmo se você não conhece o Clojure.
Aqui está o vídeo: http://www.youtube.com/watch?v=nket0K1RXU4
Aqui está a fonte à qual o vídeo se refere na segunda metade: https://github.com/Cicayda/yolk-examples/blob/master/src/yolk_examples/client/autocomplete.cljs
fonte
Este artigo de Andre Staltz é a melhor e mais clara explicação que eu já vi até agora.
Algumas citações do artigo:
Aqui está um exemplo dos diagramas fantásticos que fazem parte do artigo:
fonte
Trata-se de transformações matemáticas de dados ao longo do tempo (ou ignorando o tempo).
No código, isso significa pureza funcional e programação declarativa.
Os erros do estado são um enorme problema no paradigma imperativo padrão. Vários bits de código podem alterar algum estado compartilhado em diferentes "momentos" na execução dos programas. Isso é difícil de lidar.
No FRP, você descreve (como na programação declarativa) como os dados são transformados de um estado para outro e o que os aciona. Isso permite que você ignore o tempo porque sua função está simplesmente reagindo às entradas e usando os valores atuais para criar um novo. Isso significa que o estado está contido no gráfico (ou árvore) dos nós de transformação e é funcionalmente puro.
Isso reduz enormemente a complexidade e o tempo de depuração.
Pense na diferença entre A = B + C em matemática e A = B + C em um programa. Em matemática, você está descrevendo um relacionamento que nunca mudará. Em um programa, diz que "No momento" A é B + C. Mas o próximo comando pode ser B ++, caso em que A não é igual a B + C. Em programação matemática ou declarativa, A sempre será igual a B + C, independentemente do momento em que você perguntar.
Portanto, removendo as complexidades do estado compartilhado e alterando os valores ao longo do tempo. Seu programa é muito mais fácil de raciocinar.
Um EventStream é um EventStream + alguma função de transformação.
Um comportamento é um EventStream + algum valor na memória.
Quando o evento é acionado, o valor é atualizado executando a função de transformação. O valor que isso produz é armazenado na memória de comportamentos.
Os comportamentos podem ser compostos para produzir novos comportamentos que são uma transformação em N outros comportamentos. Esse valor composto será recalculado à medida que os eventos de entrada (comportamentos) forem acionados.
"Como os observadores são apátridas, muitas vezes precisamos de vários deles para simular uma máquina de estados, como no exemplo de arrastar. Temos que salvar o estado em que é acessível a todos os observadores envolvidos, como no caminho variável acima".
Citação de - Descontinuando o padrão do observador http://infoscience.epfl.ch/record/148043/files/DeprecatingObserversTR2010.pdf
fonte
A explicação curta e clara sobre Programação Reativa aparece no Cyclejs - Programação Reativa , que usa amostras simples e visuais.
É um bom ponto de partida, não uma fonte completa de conhecimento. De lá, você pode pular para papéis mais complexos e profundos.
fonte
Confira Rx, Extensões Reativas para .NET. Eles apontam que, com o IEnumerable, você está basicamente 'puxando' de um fluxo. As consultas Linq sobre IQueryable / IEnumerable são operações definidas que 'sugam' os resultados de um conjunto. Mas com os mesmos operadores no IObservable, você pode escrever consultas Linq que 'reagem'.
Por exemplo, você pode escrever uma consulta Linq como (de m em MyObservableSetOfMouseMovements, em que mX <100 e mY <100 selecionam novo ponto (mX, mY)).
e com as extensões Rx, é isso: você tem um código de interface do usuário que reage ao fluxo de movimentos do mouse e desenha sempre que você estiver na caixa 100.100 ...
fonte
O FRP é uma combinação de programação funcional (paradigma de programação construído sobre a idéia de que tudo é uma função) e paradigma de programação reativa (construído sobre a idéia de que tudo é um fluxo (filosofia observadora e observável)). É suposto ser o melhor dos mundos.
Confira o post de Andre Staltz sobre programação reativa para começar.
fonte