dotnet / csharplang

The official repo for the design of the C# programming language
11.4k stars 1.02k forks source link

Add "destiny operator" to C# #3380

Closed mhamri closed 4 years ago

mhamri commented 4 years ago

whatever Svelte is doing we can do much better in c#. it makes reactivity first-class citizen in the language. C# become the first language that supports such a feature.

the full argument in here: https://youtu.be/AdNJ3fydeao

YairHalberstadt commented 4 years ago

@mhamri

I've made a simple framework, that should make doing what you're trying to do quite simple, and much more boilerplate free than your original example, without changing the language:

using System;
using System.Reactive.Subjects;
public class Program
{
    public static void Main()
    {
        var a = RX.New(10);
        var b = a.MakeDependent(a => a + 1);
        b.NowAndOnChange(b => Console.WriteLine(b));
        a.Value = 20;
    }
}

public static class RX
{
    public static RX<T> New<T>(T value) => new RX<T>(value);
}

public class RX<T>
{
    private readonly Subject<T> _subject = new Subject<T>();

    private T _value;

    public RX(T value)
    {
        _value = value;
    }

    public T Value
    {
        get => _value;
        set
        {
            _value = value;
            _subject.OnNext(value);
        }
    }

    public RX<TDependent> MakeDependent<TDependent>(Func<T, TDependent> dependentFunc)
    {
        var dependent = new RX<TDependent>(dependentFunc(Value));
        _subject.Subscribe(t => dependent.Value = dependentFunc(t));
        return dependent;
    }

    public void NowAndOnChange(Action<T> action)
    {
        action(Value);
        _subject.Subscribe(action);
    }
}

The previous program prints:

11
21

Now it's not perfect. For example, if you want to depend on two different RX's you need to add another overload to MakeDependant:

    public RX<TDependent> MakeDependent<T1, TDependent>(RX<T1> t1, Func<T, T1, TDependent> dependentFunc)
    {
        var dependent = new RX<TDependent>(dependentFunc(Value, t1.Value));
        _subject.Subscribe(t => dependent.Value = dependentFunc(t, t1.Value));
        t1._subject.Subscribe(t1 => dependent.Value = dependentFunc(Value, t1));
        return dependent;
    }

Which will allow you to write:

        var a = RX.New(10);
        var b = RX.New(17);
        var c = a.MakeDependent(b, (a, b) => a + b);
        c.NowAndOnChange(c => Console.WriteLine(c));
        a.Value = 20;
        b.Value = 30;

Printing:

27
37
50

And another overload if you want to depend on three types, and so on ad infinitum.

So there's definitely issues here, but by putting the issues in a concrete setting they become much easier to solve.

For example this same issue effects everything in the framework. There are multiple definitions of Func, Action, Tuple, ValueTuple etc. taking no, 1,2,3,4,5,6,7... type parameters. So can we find a way to allow the language to abstract over an arbitrary number of type parameters? Doing so will help many use cases, including your own.

Are there other issues with my example?

Yes for sure. But now we have a concrete idea for what we're trying to achieve, we know what the semantics and code gen should look like, and we're just trying to explore how we can make this more user friendly. Doing so makes the design discussion far more fruitful!

Hope that helps!

CyrusNajmabadi commented 4 years ago

if it's as bad as you are claiming it is.

I never claimed any such thing.

I laid out what is necessary for language changes to happen. You are free to attempt engage within that framework or not. I'm just pointing out that if you do not, the language changes will not happen.

I wonder If there was that level of familiarity, why even this basic question is being asked?

Because that is how language design works. There are many patterns out there with lots of information on the topic. That doesn't mean the patterns will get operators enshrined in the language to help them out. These questions are being asked because they form the basis of the argument as to why the language would change.

CyrusNajmabadi commented 4 years ago

@mhamri As i mentioned above:

Honestly, my recommendation at this point is to just start with a new proposal.

This one got off to a very bad start, and does not seem to have corrected itself. There's no problem making a second attempt, this time with more information and data provided to explain how this would work.

Note: there are many open questions i still have here. For example, say one of these observable values is captured by some lambda. And those lambdas are captured by other pieces of hte sytem. What happens when the observable value is changed? Do the lambdas that capture them re-run? How does that work with captured state? i.e. i may have some lambda capturing this that is use din a linq query that is used in a foreach loop inside a method that belongs to many object instances. Do they all rerun? How would you make that actually happen?

YairHalberstadt commented 4 years ago

Just for fun, I made Rx implement async await.

The semantics are that anything in an async function after you await an Rx is dependent on that Rx, so will be rerun if the value of the Rx changes.

So the following program:

class Program
{
    static void Main(string[] args)
    {
        var aRx = Rx.New(10);
        var bRx = M(aRx);
        static async Rx<int> M(Rx<int> aRx)
        {
            var b = (await aRx) + 1;
            Console.WriteLine(b);
            return b;
        }
        aRx.Value = 20;
    }
}

Prints:

11
21

as desired by the OP.

I'm sure there are some bugs in there, and the number of allocations is very high, but I think it's a fun playground to explore the concept in.

Here is the full code:

public static class Rx
{
    public static Rx<T> New<T>(T value) => new Rx<T>(value);
}

[AsyncMethodBuilder(typeof(RxMethodBuilder<>))]
public class Rx<T>
{
    private readonly Subject<T> _subject = new Subject<T>();

    private T _value;

    public Rx(T value)
    {
        _value = value;
    }

    public T Value
    {
        get => _value;
        set
        {
            _value = value;
            _subject.OnNext(value);
        }
    }

    public RxAwaiter<T> GetAwaiter() => new RxAwaiter<T>(this);

    public struct RxAwaiter<T> : INotifyCompletion
    {
        private readonly Rx<T> _rx;

        public RxAwaiter(Rx<T> rx)
        {
            _rx = rx;
            IsCompleted = false;
        }

        public bool IsCompleted { get; }
        public T GetResult() => _rx.Value;
        public void OnCompleted(Action completion) => throw new InvalidOperationException();
        public IDisposable Subscribe(Action completion) => _rx._subject.Subscribe(_ => completion());
    }
}

public class RxMethodBuilder<T>
{
    private IDisposable _token;

    public RxMethodBuilder(Rx<T> task)
    {
        Task = task;
    }

    public static RxMethodBuilder<T> Create() => new RxMethodBuilder<T>(new Rx<T>(default));

    public void Start<TStateMachine>(ref TStateMachine stateMachine)
        where TStateMachine : IAsyncStateMachine => stateMachine.MoveNext();

    public void SetStateMachine(IAsyncStateMachine stateMachine) { }
    public void SetException(Exception exception) => throw exception;
    public void SetResult(T result) => Task.Value = result;

    public void AwaitOnCompleted<TAwaiter, TStateMachine>(
        ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : INotifyCompletion
        where TStateMachine : IAsyncStateMachine
    {
        if (!(awaiter is Rx<T>.RxAwaiter<T> rxAwaiter))
        {
            throw new InvalidOperationException();
        }

        var local = stateMachine;
        IDisposable nextToken = null;

        Action action = () =>
        {
            var copy = local;
            copy.MoveNext();
            nextToken?.Dispose();
            nextToken = _token;
        };

        var thisToken = rxAwaiter.Subscribe(action);
        var copy = local;
        copy.MoveNext();
        nextToken = _token;
        _token = nextToken is null ? thisToken : Disposable.Create(() => { nextToken.Dispose(); thisToken.Dispose(); });
    }
    public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
        ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : ICriticalNotifyCompletion
        where TStateMachine : IAsyncStateMachine
    {
        AwaitOnCompleted(ref awaiter, ref stateMachine);
    }

    public Rx<T> Task { get; }
}

I think overall, if there was any feature request I would make based on my experience playing with this, it's to make the Async State Machine api more general, and suitable for a larger number of problems rather than being very Task specific. However I don't have anywhere near enough expertise there to create a concrete proposal.

gneu77 commented 4 years ago

I came here while looking if someone already tried to make something similar to Svelte for a language that allows for definition of own DSLs (something like e.g. Nim).

I did not read the full thread here, but I feel that I must clarify some statements made about what Svelte does, that are simply wrong:

...though the language implementation in the svelte is different. var a = 10; $: b = a + 1; Assert.AreEqual(11, b); a = 20; Assert.AreEqual(21, b);

This is just wrong. It would lead to an error: Cannot access 'b' before initialization

b is a reactive declaration and must only be accessed from a reactive statement. E.g. what you can do is the following (I switched the asserts for log-statements, so everybody could easily try for himself here ):

let a = 10;
$: b = a + 1;
$: console.log(`b = ${b}`); // reactive statement
a = 20; // executed before any reactive statement is executed

Here, b = 21 will be logged and NOT b = 11, because the reactive handling is not yet initialized when assigning 20 to a. What you could do is e.g.:

let a = 10;
$: b = a + 1;
$: console.log(`b = ${b}`);
setInterval(() => {a += 1}, 1000);

which would happily log an incremented b each second, starting with b = 11. By the way, $: a += 1; would also just log b = 12, because the execution order of reactive statements for sure respects dependencies (easily verified by $: a = b + 1; leading to an error: Cyclical dependency detected: b → a → b

Svelte is a DSL and in its domain, the simple syntax for reactive declarations and reactive statements are extremly helpful. The markup in a Svelte file is automatically transformed in render statements which are reactive statements, hence e.g. all event handlers are also executed within an initialized reactive context. The way all this is put together in Svelte takes away a huge amount of boilerplate and makes your program very easy to reason about.

However, if you go away from a DSL like Svelte and try to generalize this concept (what I think was the intention of this proposal), one would loose some of these benefits vs. just using e.g. the C# equivalent to RxJs (and loose even more, if one tries to make the new syntax feature equivalent). The idea of putting reactive statements first class into a language is great, but it will require a lot of thinking. It's several years since I last wrote any line of code in C# and I will not even try to propose something here, but what I can say is that many things I saw proposed in the posts here are definitely not the way to go. E.g. the expectation that a Console.WriteLine(b); outside a reactive statement should be executed whenever a reactively declared b is changed, is just ridiculous, except your design goal would be to create programs that are unmaintainable and very hard to reason about (and I hope, I could clarify that this is not the way Svelte works, because setting this straight was the only reason for me leaving a comment here).