Функционально-реактивный оператор в RxJava2, который создает новое состояние из предыдущего состояния и дополнительных входных данных

#rx-java #reactive-programming #rx-java2 #purely-functional

#rx-java #реактивное программирование #rx-java2 #чисто функциональный

Вопрос:

Я работаю над небольшой демонстрацией, которая должна продемонстрировать, как вы можете писать интерактивные программы чисто функциональным способом, используя функционально-реактивное программирование (в частности, RxJava2).

Моя цель — создать простую интерактивную игру. Основная функция игры (назовем ее Next Game State, или сокращенно NGS) принимает текущее состояние игры, вводимые пользователем данные, а также случайное число и вычисляет следующее состояние игры на основе этих трех входных данных. Пока все довольно просто. Пользовательские входные данные и случайные числа — это Flowable s, которые были созданы из Iterable s или с помощью генераторов. Я предполагал, что само игровое состояние также будет текучим (но в этом я могу ошибаться).

Я изо всех сил пытаюсь найти правильный функционально-реактивный оператор, который применяет функцию к трем входам и создает следующее состояние игры. Изначально я думал, что Flowable.zip(source1, source2, source3, zipper) это будет правильная операция: он мог бы взять три потока и объединить их с помощью функции NGS в новый поток. Это, к сожалению, не учитывает тот факт, что сам результирующий поток должен быть одним из входных данных операции zip, что кажется невозможной настройкой. Моей следующей идеей было использовать Flowable.generate , но мне нужны два дополнительных входа от других Flowable s для вычисления следующего состояния, и нет способа передать их в generate оператор.

В двух словах, я пытаюсь реализовать что-то похожее на эту (псевдо-) мраморную диаграмму:

   Game --------------(0)-------------(1)-----------------(2)-----------------(3)---
  State                   _______   /        _______   /        _______   /
                        _|       |_/   _____|       |_/   _____|       |_/
                          | Next  |           | Next  |           | Next  |
                     _____| Game  |      _____| Game  |      _____| Game  |
                    /   __| State |     /   __| State |     /   __| State |
                   /   /  '_______'    /   /  '_______'    /   /  '_______'
                  /   /               /   /               /   /
  User           /   /               /   /               /   /
  Input --------o---/---------------o---/---------------o---/--------------------
                   /                   /                   /
  Random          /                   /                   /
  Number --------o-------------------o-------------------o---------------------------
 

Верхняя строка, я признаю, несколько нестандартна, но это моя лучшая попытка визуализировать, что, начиная с начального состояния игры (0) , каждое последующее состояние игры (1) , (2) , (3) , и т.д. Создается из предыдущего состояния игры плюс дополнительные входные данные.

Я понимаю, что, вероятно, существует относительно простой способ сделать это с Emitter помощью или, возможно, внутри нижестоящего подписчика, но одна из целей этого упражнения — показать, что это можно решить полностью без побочных эффектов или void методов.

Итак, короче говоря, существует ли существующий оператор, который поддерживает такое поведение? Если нет, то что будет задействовано при его создании? Или, может быть, есть какое-то альтернативное решение, которое использует несколько иную общую настройку?

Комментарии:

1. Зависит ли состояние игры от пользовательского ввода и случайного числа, или возможно, что состояние игры каким-то образом изменяется само по себе? Я бы предположил, что игровое состояние всегда выводится из начального состояния плюс некоторые входные значения. При изменении входного значения (случайное число, ввод пользователя) вычисляется новое игровое состояние.

2. @HansWurst состояние игры действительно всегда выводится из предыдущего состояния игры и некоторых дополнительных входных значений; состояние игры не изменяется само по себе, оно может изменяться только в ответ на ввод пользователя

3. Вам нужно, чтобы пользовательский ввод и случайное число обрабатывались как пара? Например (newInput1, randomNumber1, curr_state) -> новое состояние; (newInput2, randomNumber2, curr_state) -> новое состояние. Или возможно ли, что входные данные передаются без нового случайного числа? Например (newInput1, randomNumber1, curr_state) -> новое состояние; (newInput2, randomNumber1, curr_state) -> новое состояние. В этих примерах показано, что входные данные всегда объединяются с последними значениями для случайного числа (combineLatest / withLatestFrom).

4. @HansWurst Да, пользовательский ввод и случайное число всегда представлены в виде пары.

Ответ №1:

Предположение

Пользовательский ввод и случайное число — это два отдельных потока, которые могут генерироваться в любое время самостоятельно.

Решение

короче говоря, существует ли существующий оператор, который поддерживает такое поведение?

Да, есть. Вы могли бы использовать #scan(seed<T>, { current<T>, upstream<Q> -> newValue<T> } .

 import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.functions.BiFunction
import io.reactivex.rxjava3.processors.PublishProcessor
import org.junit.jupiter.api.Test

class So65589721 {
    @Test
    fun `65589721`() {
        val userInput = PublishProcessor.create<String>()
        val randomNumber = PublishProcessor.create<Int>()

        val scan = Flowable.combineLatest(userInput, randomNumber, BiFunction<String, Int, Pair<String, Int>> { u, r ->
            u to r
        }).scan<GameState>(GameState.State1, { currentState, currentInputTuple ->
            // calculate new state from `currentState` and combined pair
            GameState.State3(currentInputTuple.first, currentInputTuple.second)
        })

        val test = scan.test()

        randomNumber.offer(42)

        userInput.offer("input1")
        userInput.offer("input2")

        test
            .assertValues(GameState.State1, GameState.State3("input1", 42), GameState.State3("input2", 42))
    }

    interface GameState {
        object State1 : GameState
        data class State3(val value: String, val random: Int) : GameState
    }
}
 

В этом примере показано, как использовать scan для вычисления нового состояния из заданной пары входных данных и текущего состояния. С scan помощью вы можете безопасно «удерживать» состояние, не подвергая его побочным эффектам.

Примечание

  • seed-value переданное значение scan будет передано в качестве первого значения при подписке.

Комментарии:

1. Спасибо за подробный ответ, @HansWurst. В моем конкретном случае конкретное решение zip сопровождалось scan : zip(random, responses, Input.FACTORY::create).scan(State.FACTORY.create(1, 100, Optional.empty()), instance::next)

2. Мой полный рабочий пример (Java 8 RxJava 2) можно найти здесь: github.com/raner/top.java.purely . функциональный / blob / master / src / …