Giter Club home page Giter Club logo

tinkstatesharp's Introduction

Build codecov

TinkState# - Reactive State Handling for C# (and Unity!)

An uncomplicated library for dealing with mutable state in a nice reactive way.

Documentation.

Features:

  • Lightweight and generic observable state primitives
  • Efficient binding mechanism with support for per-frame batching
  • Computed states with automatic update propagation
  • Asynchronously computed states with async/await
  • Out-of-the-box support for Unity with convenience helpers
  • Experimental support for Godot!

Quick Peek

Here's a "hello world" teaser in Unity (source code below).

Source Code
using TinkState;
using TMPro;
using UnityEngine;

public class HelloWorld : MonoBehaviour
{
	[SerializeField] TMP_InputField nameInput;
	[SerializeField] TMP_Text greetingLabel;

	void Start()
	{
		// define piece of mutable observable state
		var name = Observable.State("World");

		// bind the state two-ways to an input field
		name.Bind(nameInput.SetTextWithoutNotify);
		nameInput.onValueChanged.AddListener(newValue => name.Value = newValue);

		// derive automatically updated observable value from it
		var greeting = Observable.Auto(() => $"Hello, {name.Value}!");

		// bind the auto-observable to a text field
		greeting.Bind(text => greetingLabel.text = text);
	}
}

Status

BETA. Everything seems to be working and test coverage makes sure of it, but the code could use some polishing, naming review, performance audit. See also TODOs in the code.

Thanks

It is a C# port of an excellent Haxe library tink_state by Juraj Kirchheim (@back2dos).

This is free and unencumbered software released into the public domain.

tinkstatesharp's People

Contributors

nadako avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

boyquotes

tinkstatesharp's Issues

Add ability to disable tracking for parts of code in `Observable.Auto(Run)`

Basically expose AutoObservable.Untracked in a public API, because sometimes we want to have an Observable.AutoRun code that tracks most of the accessed observables, but not all (e.g. in child calls that might also access observables). While it's a pretty error-prone thing to do and should be discouraged (TODO: add this to docs!), it is useful in some cases and AutoRun without it would be incomplete.

Diagnostic analyzer for unused `Bind` return.

It could be an interesting little thing to add that might help some people avoid mistakes while using this library (and also help me learn some stuff about MS codeanalysis framework):

Show a warning when Observable<T>.Bind is called but the resulting IDisposable is not used in any way. This means that there's no unsubscription and as a result could cause a memory leak

Add some syntax sugar for observable models

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}!");
}

Add support for custom observable implementations.

It would be nice to expose some internal APIs that wires together observables, auto-observables and whatnot in case someone wants to have their own Observable<T> implementation that works well with the rest.

For that I just need to polish those APIs a bit and make them public.

Visualize & debug observable dependencies

Disclaimer: I personally never really needed this, but still it looks useful.

The original Haxe tink_state library has a special compilation flag (tink_state.debug) that adds extra debugging info to each observable and a way to get the dependency tree of any observable at the given moment, which is pretty handy for debugging.

For example this code would output a nice indented tree of observables that depend on each other.

import tink.state.Observable;
import tink.state.State;

function main() {
	var duration = 100;
	var timestamp = new State(0);
	var endTime = timestamp.value + duration * 0.6;

	var timeLeft = Observable.auto(() -> endTime - timestamp.value);
	var progress = Observable.auto(() -> timeLeft.value / duration);

	Sys.println(progress.value); // track dependencies

	var dependencyTree = progress.dependencyTree();

	Sys.println(dependencyTree.toString());
}
tink.state.internal.AutoObservable#2(src/Main.hx:10)
  tink.state.internal.AutoObservable#1(src/Main.hx:9)
    tink.state._State.SimpleState#0(src/Main.hx:6)

As we can see, it also stores the file/line where the observable were instantiated, which definitely helps tracking them down. It is also possible to provide custom toString methods to give them specific names, but this is usually not needed.

Note that in C# the positions can also be magically aquired, similar to haxe.PosInfos, via CallerLineNumber attribute and friends. I imagine that this can be also integrated into a Unity/Godot inspector to display nicely.

Another thing this compilation flag does is logging invalidations and the whole auto-observable machinery, which also seems quite useful.

Add disposable utilities (possibly as a "separate" library?)

When working with lifetime helper and embracing IDisposable it's often needed to have composite disposables, dummy empty disposables, callback disposables, cancellation-token disposables, event unsubscription disposables, disposable proxies and so on.

I normally just have what I need in specific project but this is a good candidate for the library code. It's not specifically related to TinkState# and state handling tho, so it might make sense to have a separate "lifetime helper" library with all this (also move Unity extensions there).

Add Godot scheduler and lifetime helpers.

Godot is a super nice open-source game engine and it supports C#, so this library might be useful there as well. We just need to provide a Scheduler implementation and some helpers to align bindings lifetime to the scene nodes.

I experimented a bit and it definitely works, now I just need to polish it and provide an easy way to install the library into a Godot project.

Godot_v4.0-stable_mono_win64_NsvkUVhbmv.mp4

Explore explicit invalidation idea

It might be useful to have a State of a mutable object (like a ZString) with a possibility to explicitly invalidate and notify all bindings/auto-observables without changing the Value itself.

This would be similar to how ObservableList/ObservableDictionary work.

Gotta play with the idea and think of the best way to make it play nice with comparators.

Support observing collections like an `Observable<read-only-collection>`

Observable collections like ObservableList and ObservableDictionary need some love.

Firstly, we want to have read-only interfaces so you can pass them to the readers without the ability to modify them.

Secondly, it would be nice to have e.g. ObservableList also behave like an Observable<IReadOnlyList> or something, so you can bind to it like a normal observable.

Consider collection modification event "streams"

In some cases, reinitializing the whole view when an observable collection is changed is undesired, and manually comparing differences is annoying. Consider having granular add/remove/replace event "stream" observables in ObservableDictionary and ObservableList that an user can bind to and handle specific modifications.

Also, "streams" in general seem to be an useful feature for generic event dispatching that integrates well with observables and the scheduling, let's see if we can come up with something nice and easy here.

Provide a way to reset internal static fields (and use it in Unity)

In Unity (and maybe other environments) it is possible to restart the game in the editor without reloading .NET domain. This speeds up the iteration process by saving the time spent on reloading, but comes with the potential issues due to static fields not being reset properly.

So we should provide a method to reset our statics and use it in Unity whenever the domain is reused:

  • revision counter
  • auto-observable current
  • scheduler state

This is not the most important issue, because these static values are very short-lived, except for revision. Resetting revision, while is a logical thing to do, can cause clashes with user's own observables that persist statically through restarts without domain reloading, so this should be well-documented and maybe even toggleable through a compiler define.

Add support for Flax Engine

Flax Engine looks really nice. It has first-class C# integration, so TinkState# should be easily usable there.

Basically we need to copy-paste the unity batch scheduler implementation and adapt it to Flax API.
Initialization is likely to be done via the GamePlugin API, and the update callback is hooked into Scripting.LateUpdate event

Implement granular observability for collection elements

Basically haxetink/tink_state#49 (which is also implemented in haxetink/tink_state#74, so we can adapt that).

The idea is that if an Observable.Auto/AutoRun is only accessing a specific key from an observable list or dictionary, the recomputation should trigger only if that specific key is changed, not when anything is changed inside the collection.

Example:

var dict = Observable.Dictionary<string, int>();
dict["a"] = 1;
dict["b"] = 1;

Observable.AutoRun(() => Console.WriteLine(dict["a"]));

dict["a"] = 2; // should trigger re-run
dict["b"] = 2; // should NOT trigger re-run

explore what's required for serialization

We might need to expose some APIs to provide a way to (de-)serialize State. With Json.NET, it doesn't actually need any changes to the library, here's a quick and dirty implementation of a JsonConverter:

class StateJsonConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        if (objectType.IsGenericType)
        {
            // field type definition for deserialization - the interface directly
            if (objectType.GetGenericTypeDefinition() == typeof(State<>))
            {
                return true;
            }
            
            // run-time object type for serialization - the implementation
            foreach (var iface in objectType.GetInterfaces())
            {
                if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(State<>))
                {
                    return true;
                }
            }
        }
        return false;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        GetConverter(value.GetType()).WriteJson(writer, value, serializer);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        return GetConverter(objectType).ReadJson(reader, existingValue, serializer);
    }
    
    StateConverter GetConverter(Type stateType)
    {
        // TODO: cache converters
        var rt = typeof(StateConverter<>).MakeGenericType(stateType.GetGenericArguments()[0]);
        return (StateConverter)Activator.CreateInstance(rt, false);
    }
}

abstract class StateConverter
{
    public abstract void WriteJson(JsonWriter writer, object value, JsonSerializer serializer);

    public abstract object ReadJson(JsonReader reader, object existingValue, JsonSerializer serializer);
}

class StateConverter<T> : StateConverter
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        WriteJsonInternal(writer, (State<T>)value, serializer);
    }

    public override object ReadJson(JsonReader reader, object existingValue, JsonSerializer serializer)
    {
        return ReadJsonInternal(reader, existingValue, serializer);
    }

    void WriteJsonInternal(JsonWriter writer, State<T> state, JsonSerializer serializer)
    {
        serializer.Serialize(writer, state.Value);
    }

    State<T> ReadJsonInternal(JsonReader reader, object existingValue, JsonSerializer serializer)
    {
        var stateValue = serializer.Deserialize<T>(reader);
        if (existingValue is State<T> existingState)
        {
            existingState.Value = stateValue;
            return existingState;
        }
        return Observable.State(stateValue);
    }
}

Add a documentation page comparing TinkState# with R3/Rx

TinkState could be compared to a subset of R3/Rx since they serve similar purpose. Rx is more generic, TinkState is more focused, so let's create a page comparing the features, approaches, pros and cons, to help people choose.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.