nadako / TinkStateSharp

Handle those pesky states, now in C#
https://nadako.github.io/TinkStateSharp/
The Unlicense
43 stars 1 forks source link

Add some syntax sugar for observable models #4

Open nadako opened 1 year ago

nadako commented 1 year ago

I'd like to add something like coconut.data. There's no macros in C# so we'll have to do some IL weaving magic, but I'm pretty sure it would be possible to have something like:

[Model]
class Data {
    [Observable] public string Name { get; set; } 
    [Observable] public string Greeting => $"Hello, {Name}!";
}

and generate something like this from that:

class Data {
    public string Name { get => _Name.Value; set { _Name.value = value; }}
    public string Greeting => _Greeting.Value;

    State<string> _Name = Observable.State(default);
    Observable<string> _Greeting = Observable.Auto(() => $"Hello, {Name}!");
}
nadako commented 1 year ago

To do that, we need to have an assembly post-processing task made with Mono.Cecil, it would iterate over types, find [Model] classes, find [Observable] properties in it, remove their default backing fields, add backing observables and change the get/set methods to use that.

nadako commented 1 year ago

We also need a MSBuild task to easily add this post-processing, and for Unity we need to hook into the CompilationPipeline.

nadako commented 1 year ago

I've played with this today and had some success, one thing that comes in mind is that we also want to expose underlying Observable and State objects for easy binding. In coconut.data this is easy because the macros generates observables fields with everything. With IL weaving this is harder, since it's a post-processing operation and we cannot add things that will be visible by the users of the model.

So far I have one idea how to implement it. Instead of the [Model] attribute, we'll have models implementing the special Model marker interface for which we'll have an extension method like this:

static class ModelExtensions
{
    static Observable<T> GetObservable<T>(this Model model, string fieldName) {
        return ((ModelInternal)model).GetObservable<T>(fieldName);
    }
}

So you can then do something like data.GetObservable<string>(nameof(Data.Name)).Bind(...), which is not very nice (not type-safe enough), but it's something.

Internally, we'd have another special interface like ModelInternal that actually defines per-type GetObservable<T> method, and then we'd implement this interface when doing the weaving with a switch or something.

If someone has better ideas how to do it, suggestions are welcome :)

nadako commented 1 year ago

So you can then do something like data.GetObservable<string>(nameof(Data.Name)).Bind(...), which is not very nice (not type-safe enough), but it's something.

OK, for type-safety (and type inference!) we could also use the expression tree feature of C#, so the API would be something like var observable = GetObservable(() => player.Name).

nadako commented 1 year ago

FYI I'm experimenting with it in the model branch (beware - code is bad).