Roslyn: [Discussion] nested withers for deep immutable object updates (lenses?)

Created on 12 Jan 2017  路  3Comments  路  Source: dotnet/roslyn

Let's say we have the following immutable records (I'm using the latest discussed record and wither syntax here, [ReadOnly] is a :spaghetti: attribute to show that the records are actually immutable):

```c#
[ReadOnly] public class Order(Customer customer, ImmutableMap Lines);
[ReadOnly] public class OrderLine(Product product, Quantity quantity);
[ReadOnly] public class Quantity(uint value);

e.g.:

```c#
var expr = new Order(
    customer,
    {
        [boots] = new OrderLine(boots, 100),
        [gloves] = new OrderLine(gloves, 200)
    });

If we need to update the innermost element, even with some kind of a wither this looks like this:

```c#
public static Order UpdateQuantity(Order order, Product product, Quantity newQuantity)
=> order with {
Lines = order.Lines.SetItem(
product,
order.Lines[product] with {
Quantity = new Quantity
}
)
};

public static Order SwitchProduct(Order order, Product oldProduct, Product newProduct)
=> order with {
Lines = order.Lines.SetItem(
newProduct,
(order.Lines?[newProduct] ?? new OrderLine(newProduct, 0)) with {
Quantity =
order.Lines?[newProduct].Quantity ?? 0 +
order.Lines[oldProduct].Quantity
}
),
Lines = order.Lines.Remove(oldProduct)
};

The withers should be nestable, like this:

```c#
public static Order UpdateQuantity(Order order, Product product, Quantity newQuantity)
    => order with { Lines[product].Quantity = newQuantity };

public static Order SwitchProduct(Order order, Product oldProduct, Product newProduct)
    => order with {
        Lines[newProduct] = 
            order.Lines?[newProduct] ?? new OrderLine(newProduct, 0),
        Lines[newProduct].Quantity = 
            order.Lines[newProduct].Quantity + 
            order.Lines[oldProduct].Quantity,
        Lines = order.Lines.Remove(oldProduct)
    };

The hardest part here is the indexer. Somehow most immutable collections should be able to recognize they are being used in a wither. Perhaps immutable property setters are needed for this to work transparently.

Area-Language Design Discussion

Most helpful comment

This has been brought up before and I agree that it should be possible, though I'd prefer the the already proposed syntax in the spec draft,

var p = person with
{
  Name.FirstName = "NewFirstName"
};

// equivalent to

var p = person with
{
  Name with { FirstName = "NewFirstName" }
};

This probably addresses the open issue regarding wither syntax more strongly,

Open issue: Does this syntactic sugar actually pay for itself?

Since in the basic form it is just the same method invocation with different tokens as stated,

A with_expression of the form

 e1 with { identifier = e2, ... }

is treated as an invocation of the form

e1.With(identifier2: e2, ...) 

Same analogy could be applied to property patterns as well,

if (person is { Name.FirstName: "Foo" })

// equivalent to

if (person is { Name: { FirstName: "Foo" } })

Above examples omitted the static types as they are known at the compile-time (see #11744).

All 3 comments

This has been brought up before and I agree that it should be possible, though I'd prefer the the already proposed syntax in the spec draft,

var p = person with
{
  Name.FirstName = "NewFirstName"
};

// equivalent to

var p = person with
{
  Name with { FirstName = "NewFirstName" }
};

This probably addresses the open issue regarding wither syntax more strongly,

Open issue: Does this syntactic sugar actually pay for itself?

Since in the basic form it is just the same method invocation with different tokens as stated,

A with_expression of the form

 e1 with { identifier = e2, ... }

is treated as an invocation of the form

e1.With(identifier2: e2, ...) 

Same analogy could be applied to property patterns as well,

if (person is { Name.FirstName: "Foo" })

// equivalent to

if (person is { Name: { FirstName: "Foo" } })

Above examples omitted the static types as they are known at the compile-time (see #11744).

@alrz I've updated the syntax to match the currently proposed one, even though it has the dreadful }), which I cannot force myself to write together despite never placing a ) on a line by itself otherwise.

Good idea about property patterns, I think this is much easier to implement since it's a straightforward transformation.

Was this page helpful?
0 / 5 - 0 ratings