我应该如何在 FRP (Rx.Net) 中对循环依赖建模?

How should I model circular dependencies in FRP (Rx.Net)?

我正在尝试通过使用 Rx.Net 来实现 Tic-Tac-Toe 来学习更多关于函数反应式编程的知识。我遇到的问题是我的游戏逻辑中似乎存在循环依赖。

commands 流(PlaceTokenResetGame 等)是从用户输入流生成的。

游戏的当前状态 (boardStates) 是通过将 commands 应用于先前的状态得出的,从初始状态开始:

var initialBoardState = new BoardState();

var boardStates = commands
    .Scan(initialBoardState, (boardState, command) => command.Apply(boardState))
    .DistinctUntilChanged();

但是,commands 流应该依赖于 boardStates 流。这是因为有效的命令集随当前状态而变化。

例如,PlaceToken命令应该只在用户点击一个空的图块时发出,但是空图块的集合是由当前状态定义的!

总而言之,我有两个似乎相互依赖的流。我应该如何在功能反应式编程中解决这个问题?

并不是所有的事情都需要成为一个事件。请记住 Rx/Callbacks 是一种允许您依赖的东西给您回电的方式(而不依赖于您)。

根据经验

  1. 循环依赖表示存在设计缺陷
  2. 发送命令,接收事件

2)也可以翻译成

Just call methods on your dependencies to change their state, but subscribe to their events to see their changes

因此,与其将命令视为一个流,不如将用户操作视为一个流,在收听时可能会创建一个命令。

所以 BoardState 可能看起来像这样

public class BoardState
{
    public void PlaceToken(PlaceTokenCommand placeToken)
    {
        //Process, then raise event
    }

    public void Reset()
    {
        //Process, then raise event
    }

    public IObservable<?> StateUpdates()
    {

    }
}

ViewModel(?) 代码可能看起来像这样

public class TicTacToeViewModel
{
    private readonly BoardState _board;
    public TicTacToeViewModel()
    {
        _board = new BoardState();
        MoveTokenCommand = new DelegateCommand(MoveToken, CanMoveToken);
        ResetBoardCommand = new DelegateCommand(_board.Reset);

        board.StateUpdates(state => UpdatePresentation(state));
    }

    public DelegateCommand MoveTokenCommand { get; private set;}
    public DelegateCommand ResetBoardCommand { get; private set;}


    private void MoveToken()
    {
        var token = CurrentToken;
        var location = ActiveLocation;
        var cmd = new PlaceTokenCommand(token, location);
        _board.PlaceToken(cmd);
    }

    private bool CanMoveToken()
    {
        //?
    }
}

但正如@Enigmativity,在评论中请求,没有 MCVE 很难提供明智的帮助。

最后一点,虽然 Rx 是功能性的并且是反应性的,但狂热者会反对 Rx 被考虑 FRP(参见 Conal、Behaviors 等)。

虽然@LeeCampbell 的解决方案确实有效,但它需要使您的核心模型 类 可变。相反,我发现最好复制 cycle.js 所采用的方法。你可以看看他们的解释here

问题是我们有一个循环。动作流取决于棋盘状态流,而棋盘状态流又取决于动作流:

boardStream = f(actionStream)
actionStream = g(boardStream)

cycle.js 解决方案是使用代理流将所有内容连接在一起:

// Create a proxy
proxyActionStream = new Stream()

// Create our mutually dependent streams using the proxy
boardStream = f(proxyActionStream)
actionStream = g(boardStream)

// Feed actionStream back into proxyActionStream
actionStream.Subscribe(x => proxyActionStream.OnNext(x))

在Rx.Net地,代理流应该是ReplaySubject

您唯一需要注意的地方是 运行-away 反馈循环:如果您的流从未稳定下来,那么它们就会进入无限循环!将流视为相互递归是有帮助的。