Atualização do estado sem atribuição

10

Estou aprendendo programação funcional e tenho problemas para entender como alguns cenários específicos são implementados sem o uso de atribuição. O seguinte problema simples resume bastante minha confusão.

Escreva um programa que receba eventos sobre alterações em uma determinada estrutura de dados e emita eventos quando essa estrutura de dados atinge um determinado estado.

Então, eu tenho uma cópia da estrutura de dados que mantenho

datastructure_copy::DataStructure 

Eu tenho o fluxo de eventos que são disparados quando ele muda:

datastructure_changes::Stream Change

Eu tenho uma função que aplica uma alteração a uma estrutura de dados e retorna uma nova cópia:

apply_change::Change -> DataStructure -> DataStructure

E eu tenho um predicado que verifica se o estado dos dados atingiu o estado desejado.

is_ready::DataStructure ->Boolean

Em outras palavras, eu preciso de algo como 'reduzir' que funcione em fluxos.

Eu sei que uma maneira de implementar isso é recalcular o estado toda vez que uma mudança chega, no entanto, isso parece impraticável. Joguei um pouco com a mônada do Estado, mas parece-me que ela visa resolver um problema diferente.

Então, existe outra maneira de fazer isso?

Observe que minha pergunta é puramente conceitual e não estou profundamente familiarizado com Haskell.

Bobby Marinoff
fonte
De uma forma ou de outra, você nunca verá uma 'atribuição' (observe a diferença entre atribuição e encadernação) no Haskell porque 'Haskell não tem noção de "atribuição", "estado mutável" ou "variáveis" e é um "puro linguagem funcional "' . A Mônada Estadual deve ser o que você está procurando, você só precisa aprender como usá-la. Se você quiser, posso dar uma resposta mais abrangente ainda hoje.
Francesco Gramano

Respostas:

2

Eu sei que uma maneira de implementar isso é recalcular o estado toda vez que uma mudança chega, no entanto, isso parece impraticável.

Se as alterações aplicadas quando um evento ocorrer não forem distributivas, de uma maneira ou de outra, você terá que recalcular o estado toda vez que um evento ocorrer, pois o estado final nada mais é do que o estado inicial, além de alterações sucessivas. E mesmo que as alterações sejam distributivas, você normalmente deseja transformar sucessivamente um estado no próximo, pois deseja interromper seu processo o mais rápido que um determinado estado é alcançado e desde que seja necessário calcular o próximo estado para determinar se o novo é o estado desejado.

Na programação funcional, as alterações de estado são normalmente representadas por chamadas de função e / ou parâmetros de função.

Como você não pode prever quando o estado final será calculado, você não deve usar a função recursiva não-cauda. Um fluxo de estados, no qual cada estado se baseia no anterior, poderia ser uma boa alternativa.

Portanto, no seu caso, eu responderia à pergunta pelo seguinte código, em Scala:

import scala.util.Random

val initState = 0.0
def nextState(state: Double, event: Boolean): Double = if(event) state + 0.3 else state - 0.1 // give a new state
def predicate(state: Double) = state >= 1

// random booleans as events
// nb: must be a function in order to force Random.nextBoolean to be called for each  element of the stream
def events(): Stream[Boolean] = Random.nextBoolean #:: events()  

val states: Stream[Double] = initState #:: states.zip(events).map({ case (s,e) => nextState(s,e)}) // a stream of all the successive states

// stop when the state is >= 1 ;
// display all the states computed before it stopped
states takeWhile(! predicate(_)) foreach println 

O que pode dar, por exemplo (simplifiquei a saída):

0.0
0.3
0.2
0.5
0.8

val states: Stream[Double] = ... é a linha em que estados sucessivos são calculados.

O primeiro elemento desse fluxo é o estado inicial do sistema. zipmescla o fluxo de estados com o fluxo de eventos em um único fluxo de pares de elementos, cada par sendo um (estado, evento). maptransforma cada par em um único valor, sendo o novo estado, calculado em função do estado antigo e do evento associado. Um novo estado é, portanto, um estado calculado anteriormente, mais o evento associado que "modifica" o estado.

Então, basicamente, você define um fluxo potencialmente infinito de estados, cada novo estado sendo uma função do último estado calculado e um novo evento. Como os fluxos são preguiçosos no Scala (entre outros), só são computados sob demanda, portanto você não precisa calcular estados inúteis e pode calcular quantos estados desejar.

Se você está interessado apenas no primeiro estado que respeita o predicado, substitua a última linha do código por:

states find predicate get

Que recupera:

res7: Double = 1.1
mgoeminne
fonte
Você pode fornecer alguns insights sobre a linha que faz a mágica:val states: Stream[Double]...
Bobby Marinoff
Certo. Por favor, olhe minha edição.
precisa saber é o seguinte
1

Você diz que tem 2 funções:

apply_change::Change -> DataStructure -> DataStructure
is_ready::DataStructure ->Boolean

e se eu entendi direito, is_readyé bastante caro, então você não quer fazer isso para cada evento de mudança repetidamente.

O que você precisa é que uma função pegue um DataStructure inicial e o condense em um estado simples e em uma função que assume um estado condensado, uma Alteração e gera um novo estado condensado.

Digamos que o DataStructure seja um trigêmeo x,y,ze você esteja esperando que x, ye z sejam números primos. Seu estado condensado pode então ser um conjunto de quais x, y, z não são primos. Uma mudança que faz x prime remove x do conjunto. Uma mudança que faz x não prime adiciona x ao conjunto (se não estiver presente). O DataStructure está pronto e o conjunto está vazio.

A idéia seria que atualizar o estado condensado é muito mais barato que atualizar o DataStructure e a computação já está do zero.

Nota: Uma abordagem ainda melhor poderia ser acompanhar quais x, y, z foram verificados como primos e se eles foram. Para cada alteração, você sinaliza o campo relevante como não marcado. Então, quando is_ready for chamado, verifique e lembre-se. Isso é melhor se você não verificar is_ready após cada alteração, pois x poderá mudar várias vezes e você verificará apenas o prime uma vez.

Goswin von Brederlow
fonte