SodiumFRP / sodium

Sodium - Functional Reactive Programming (FRP) Library for multiple languages
http://sodium.nz/
Other
848 stars 138 forks source link

Special RunVoid overload #106

Closed ziriax closed 6 years ago

ziriax commented 7 years ago

I'm using the functional Sodium (C#) together with a mutable WPF-like GUI (NoesisGUI, a similar object-oriented two-way binding framework).

For each mutable property in the view-model I have a Cell<T> (holding the latest value) and a StreamSink<T> (when the property is set). Commands map to StreamSink<Unit>. The mutable property itself is not accessible by the FRP logic, it only sees cells and stream.

In XAML, I use two-way binding to the mutable properties. When a mutable property is set, the new value is send to the StreamSink. However, this must only be done when no Sodium transaction is currently running. If a current transaction exists, this indicates that the UI engine itself contains side-effect logic that it wants to enforce upon us, and we don't want that, the cells in the view-model must dictate all state. WPF sometimes does this.

For this case, I added the following code to the Transaction class:

/// <summary>
/// Runs the first action if there is no current transaction; 
/// otherwise runs the second action after the current transaction completes.
/// </summary>
/// <returns>true if the first action was run, false otherwise</returns>
public static bool RunVoid(Action action, Action postAction)
{
    lock (TransactionLock)
    {
        if (currentTransaction != null)
        {
            Post(postAction);
            return false;
        }

        action();
        return true;
    }
}

This is used in the view-model's property setter:

set
{
    Transaction.RunVoid(
        delegate
        {
            _sink.Send(value);
        },
        delegate
        {
            Debug.WriteLine($"WARNING: Property {PropertyName} was set while a Sodium transaction was still open");
            OnPropertyChanged();
        });
}

I added the latter OnPropertyChanged since the backing Cell's value might have changed after the transaction completes. This is not perfect, since this could cause the UI to be stuck in an endless loop. Nevertheless it seems to work fine for now.

Do you think such an RunVoid overload is useful in the awesome Sodium library, or can I implement this using the standard methods?

Obviously I would prefer to have a full blown UI library that are FRP friendly like the controls in the Sodium book, but I don't think this is going to happen soon, since UI engines are huge.

Thanks a lot, Peter Verswyvelen

the-real-blackh commented 7 years ago

In my opinion, making UI libraries more useable with FRP is an important use case, and these kinds of tricks are important.

I am wondering if it might be better just to expose a Transaction.isActive() "Is there a transaction active on the current thread?", because Transaction.post() is already public. I know that in many cases it's better to provide a higher-level API but is there any real benefit in this case?

What do you think?

ziriax commented 7 years ago

Well, I wanted the method to be thread safe. I'm not sure this is needed in this particular case, since the UI runs on a single thread , but it seems all the other code in the transaction class uses a lock.

That being said, my code assumes that if a transaction is open, that it must be a transaction created by an earlier UI event. In a multi-threaded scenario another event from another thread might have created this transaction, while the user is still able to manipulate the UI, and pushing these UI events/changes to the sinks should be queued until the transaction closes (I guess) So either my code can only run correctly in a single threaded system, or I need to know what thread created the transaction, because I certainly do not want to queue secondary UI events that are side effects of the UI system.

Anyway, for my case an isActive property is good enough, since I assume a single thread for now.

I've made a code generator that automates the creation of view-models from a very simple interface specification. It is all very experimental and lacks features, but I will push it to github anyway. It frees the developer from writing almost all boiler plate code, so he can focus on just the pure business logic functions, FRP circuit logic and XAML UI. I still need a way to automate the initialisation of the view-model from a data-model, to extract the data-model from the view-model (or keep it in sync), and how to handle undo/redo. But I'm not sure this actually needs to be automated. This is all very easy in an Elm/Redux style application, but somehow the FRP approach feels attractive to me. I don't have enough experience with either to know if it is suitable for production quality software. It is a vicious circle, for 25 years I made software using classical object oriented or procedural approaches, and that always works, more or less ;-) I would love to write an application where all logic is handled by Sodium, but I have no idea if that is going to be a dream or nightmare. Okay, off topic, I got carried away ;-)

jam40jeff commented 7 years ago

@Ziriax I have actually recently encountered this problem as well. However, I came to the conclusion that I did in fact want the UI control to be allowed to exercise the side effects. To me, they were no different than a user executing the change immediately after the UI control updated its state. The problem is that the change needs to happen in its own transaction, so my solution was to use Transaction.Post for all UI control changes sent through the StreamSink (for 99% of the changes, this has no effect, but will ensure the UI side effect changes are executed immediately after the current transaction completes).

If you simply ignore the UI changes during a transaction, your UI control can get out of sync with the data in the view model.

ziriax commented 7 years ago

@jam40jeff Could you give an example of this?

jam40jeff commented 7 years ago

A control could make behavioral decisions such as a tree-view selecting the root node when the selection is cleared (I believe the WPF tree view does do this if there are any nodes in the tree). Behaviorally, this should be treated no differently than a user immediately selecting the root node after the view model asked for the selection to be cleared. If this is not the desired UI behavior, then a different control should be used or that control should be changed if possible. However, ignoring this is bad since the UI will in fact show the root node as being selected, but the view model would think there is no selection. What should happen is that the binding should send through a selection immediately after the current transaction ends, which is accomplished via Transaction.Post().