AArnott / ImmutableObjectGraph

Code generation for immutable types
Other
160 stars 32 forks source link

Provide a lens library to enable deep update of immutable types #84

Open JKronberger opened 8 years ago

JKronberger commented 8 years ago

Example I hope explains all. It could be done with reflection on the currently generated immutable update methods.

Company company = ...
var managerFirstNameUpdater = Company.Focus(p=>p.Manager.Name.FirstName);
Company c1 = managerFirstNameUpdater.Set(company,"Brad");
Assert(c1.Manager.Name.FirstName=="Brad");
Assert(managerFirstNameUpdater.Get(c1) =="Brad");
Assert(managerFirstNameUpdater.Get(company) !="Brad");

Writing focus for collections is a bit more tricky but I think can be done.

AArnott commented 8 years ago

It is certainly an interesting idea. I guess the Focus method would take an Expression tree in order to start working its magic. I won't have time in the near term for this feature, but I might accept a PR.

bradphelan commented 8 years ago

Yes that is correct. We have our own internal framework that does this and it works very nicely. Here is one of our test cases.

using System;
using FluentAssertions;
using Xunit;
using ReactiveUI;

namespace Weingartner.Lens.Spec
{
    class ImmutableX : Immutable
    {
        public int ImmutableInt { get; private set; }
    }
    class ImmutableZ : Immutable
    {
        public ImmutableX ImmutableX { get; private set; }

        public ImmutableZ()
        {
            ImmutableX = new ImmutableX();
        }
    }

    class INPCObject : ReactiveObject
    {
        ImmutableZ _Z;
        public ImmutableZ ImmutableZ
        {
            get { return _Z; }
            set { this.RaiseAndSetIfChanged(ref _Z, value); }
        }

        public INPCObject()
        {
            ImmutableZ = new ImmutableZ();
        }
    }

    /// <summary>
    /// Verify that the bridge from an INPC held
    /// immutable object to Lens works correctly
    /// </summary>
    public class PropertyLensSpec
    {
        [Fact]
        public void PropertyLensShouldWork()
        {
            var c = new INPCObject();
            var l = c.PropertyLens(v => v.ImmutableZ);

            int test = -1;
            l.Focus(p => p.ImmutableX.ImmutableInt).Subject.Subscribe(v => test = v);
            test.Should().Be(0);
            l.Current.ImmutableX.ImmutableInt.Should().Be(0);

            l.Focus(p => p.ImmutableX.ImmutableInt).Current = 5;
            l.Focus(p => p.ImmutableX.ImmutableInt).Current.Should().Be(5);
            l.Current.ImmutableX.ImmutableInt.Should().Be(5);
            c.ImmutableZ.ImmutableX.ImmutableInt.Should().Be(5);
            test.Should().Be(5);

            c.ImmutableZ = new ImmutableZ();
            l.Focus(p => p.ImmutableX.ImmutableInt).Current.Should().Be(0);
            l.Current.ImmutableX.ImmutableInt.Should().Be(0);
            c.ImmutableZ.ImmutableX.ImmutableInt.Should().Be(0);
            test.Should().Be(0);
            test.Should().Be(0);

            l.Focus(p => p.ImmutableX.ImmutableInt).Subject.OnNext(25);
            l.Focus(p => p.ImmutableX.ImmutableInt).Current.Should().Be(25);
            l.Current.ImmutableX.ImmutableInt.Should().Be(25);
            c.ImmutableZ.ImmutableX.ImmutableInt.Should().Be(25);
            test.Should().Be(25);

        }

        [Fact]
        public void PropertyLensShouldThrowAnExceptionIfInvokedWithAPathDepthGreaterThan1()
        {
            var c = new INPCObject();
            new Action(()=>c.PropertyLens(p => p.ImmutableZ.ImmutableX))
                .ShouldThrow<ArgumentException>();
        }

    }
}

We can handle immutable lists and dictionaries. Behind the scenes we use reflection to clone and update read only properties but it would work nice with the builder framework you have.

bradphelan commented 8 years ago

Due to problems contributing directly to this project I've created a dependent project.

https://github.com/bradphelan/ImmutableObjectGraphLens

This provides composable lenses over the immutable object graph. Currently it only supports property based selectors, no lists or dictionaries but that can be added.

For example the test

https://github.com/bradphelan/ImmutableObjectGraphLens/blob/master/src/ImmutableObjectGraphLensSpec/CompanySpec.cs

has the following test data

[GenerateImmutable]
public partial class Company
{
    readonly string name;
    readonly Person cto;

    static partial void CreateDefaultTemplate(ref Template template)
    {
        template.Cto = Person.Create("john smith");
        template.Name = "Microsoft";
    }

}
[GenerateImmutable]
public partial class  Person
{
    readonly string name;
}

and a test showing immutable update using lenses and selectors.

[Fact]
public void LensSpec()
{
var company = Company.Create();

var l =  ImmutableLens.CreateLens((Company c)=>c.Cto.Name);

company = l.Set(company, "brad");
l.Get(company).Should().Be("brad");
company.Cto.Name.Should().Be("brad");

}

More interesting is the ability to attach the immutable object type to a mutable field on an INPC supporting object. We can now create lenses to subproperties of the immutable object and have the mutable property of the root update.

public class Root : ReactiveObject
{
Company _Company = Company.Create();
public Company Company 
{
    get { return _Company; }
    set { this.RaiseAndSetIfChanged(ref _Company, value); }
}
}

[Fact]
public void MutableLensesShouldWork()
{
var root = new Root();
var lens = new PropertyLens<Root,Company>(root,c=>c.Company);

lens.Current.Cto.Name.Should().Be("john smith");
var lens2 = lens.Focus(c => c.Cto.Name);
string data = "";
string data2 = "";

lens2.WhenAnyValue(p => p.Current).Subscribe(current => data = current);

lens.Observe(p=>p.Name, p => p.Cto.Name, (companyName, ctoName)=>new {companyName, ctoName})
    .Subscribe(current => data2 = current.ctoName);

lens2.Current = "Brad";
data.Should().Be("Brad");
data2.Should().Be("Brad");
lens2.Current.Should().Be("Brad");
lens.Current.Cto.Name.Should().Be("Brad");
root.Company.Cto.Name.Should().Be("Brad");

}

Let me know your ideas on this.

bradphelan commented 8 years ago

The technical core of the library is this reflection heavy code that walks a property list recursively calling "With" on each node. Theoretically we could code generate a lens builder that will not require reflection. Any ideas?

https://github.com/bradphelan/ImmutableObjectGraphLens/blob/master/src/ImmutableObjectGraphLens/ImmutableLens.cs