Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: update blocks #194

Closed
jamesqo opened this issue Feb 27, 2017 · 10 comments
Closed

Proposal: update blocks #194

jamesqo opened this issue Feb 27, 2017 · 10 comments

Comments

@jamesqo
Copy link
Contributor

jamesqo commented Feb 27, 2017

Background

When working with immutable data, it is very common to see code of the form

// Create an immutable object
var x = CreateImmutableObject();
// Perform a series of operations on the immutable object
x = x.SomeOperation();
x = x.SomeOperation();
...
// Do something after getting the final version of the immutable object, such as
return x;

After each method call, x must be reassigned to, as x.SomeOperation() creates a new object rather than modifying the existing one. This can be a pain point if the variable name is really long, because you have to type it out twice per statement, once to perform an operation on it and once to assign it. This tends to discourage people either from using immutable data, or from giving their variables descriptive names. More importantly, it leads to a class of bugs where people forget to reassign the variable when calling methods with mutable-sounding names, such as string.Replace. Here is a real-world bug caused by forgetting to assign the result of an immutable operation.

Proposal

We should add update blocks to the language. Syntax:

// Flavor 1: Existing variable x in scope
var x = CreateImmutableObject();
update x in
{
    x.SomeOperation(); // Assigns to x after SomeOperation finishes
    x.SomeOperation(); // Here, reading x yields a new value
}
return x;

// Flavor 2: Variable x declared in block
update var x in
{
    CreateImmutableObject();
    x.SomeOperation();
    x.SomeOperation();
}
return x;
  • The expressions in the update block will be evaluated sequentially. After each evaluation, the result of the expression will be assigned to the variable provided after update (the variable will be updated).

  • Variables can be declared with update var x in or update ConcreteType x in at the top of the block.

    • If var is used, the type will be inferred from the GCD of the return types.
  • Empty update blocks will be disallowed.

  • Void-returning methods will be disallowed.

  • If the update x in or update ConcreteType x in form is used, all expressions within the block must have a return type matching the type of x.

  • The variable will leak outside the scope of the block so you can do things with it after you've finished your computations, in this case return x.

  • Nesting of update blocks will be permitted:

update x in
{
    update y in
    {
        // Here, the result of each expression will be written to both x and y.
    }

    // Here, the result of each expression will only be written to x.
}

Implications

  • When working with immutable data, it will be easier to ensure that you don't throw away your results-- just put the variable in an update block.

  • Typing time decreases, and people aren't forced to choose between lots of assignment statements (like shown above) or huge expressions (like var x = CreateImmutableObject().SomeOperation().SomeOperation(). ...).

More examples

ImmutableList<T> Create(T first, T second, T third)
{
    var immutableList = ImmutableList<T>.Empty;
    update immutableList in
    {
        // Use it just like a regular List.
        immutableList.Add(first);
        immutableList.Add(second);
        immutableList.Add(third);
    }
    return immutableList;
}

int Min(int number0, int number1, int number2, int number3)
{
    update number0 in
    {
        Math.Min(number0, number1);
        Math.Min(number0, number2);
        Math.Min(number0, number3);
    }
    return number0;
}
@benaadams
Copy link
Member

benaadams commented Feb 27, 2017

Immutability aside (and examples are throwing away the intermediate results); these are the type of patterns I'd like to use with ref returns combined with "ref extension methods on structs" #186

@chrisaut
Copy link

If we think about it, we have something like that for operators

i+=5;

so how about

immutableList.=Add(first);

Or perhaps the proposed pipe forward operators could make this work (#96), eg.

var list = CreateImmutableObject()
|> Add(1)
|> Add(2)

@orthoxerox
Copy link

What's wrong with huge expression statements?

var x = CreateNewImmutableObject()
    .SomeOperation1()
    .SomeOperation2()
    .SomeOperation3()
    .SomeOperation4();

They only become unwieldy when you have to modify nested immutables (#162), and your proposal doesn't cover that use case.

@DavidArno
Copy link

I do not find any of your examples to be compelling reasons for such new syntax. In all cases, there are simpler - existing - ways of expressing the same functionality:

Example 1 could be written as:

return CreateImmutableObject().SomeOperation().SomeOperation();

Example 2, as:

ImmutableList<T> Create<T>(T first, T second, T third) => 
    ImmutableList.Create(first, second, third);

And example 3 as:

int MinOfFour(int number0, int number1, int number2, int number3) => 
    Min(Min(Min(number0, number1), number2), number3);

This last example could be made even simpler using one of the proposed syntaxes for forward pipes:

int MinOfFour(int number0, int number1, int number2, int number3) => 
    Min(number0, number1) |> Min(@, number2) |> Min(@, number3);

@svick
Copy link
Contributor

svick commented Feb 27, 2017

I think it's common to interleave updating immutable objects with other operations. For example, the non-buggy version of the HashCodeCombiner code would look like this:

var hash = HashCodeCombiner.Start()
    .Add(_tagName, StringComparer.Ordinal)
    .Add(_innerContent, StringComparer.Ordinal);

foreach (var kvp in _attributes)
{
    hash = hash.Add(kvp.Key, StringComparer.Ordinal).Add(kvp.Value, StringComparer.Ordinal);
}

return hash.Build();

How would that look with update? Like this?

update var hash in
{
    HashCodeCombiner.Start();
    hash.Add(_tagName, StringComparer.Ordinal);
    hash.Add(_innerContent, StringComparer.Ordinal);
}

foreach (var kvp in _attributes)
{
    update hash in
    {
        hash.Add(kvp.Key, StringComparer.Ordinal)
        hash.Add(kvp.Value, StringComparer.Ordinal);
    }
}

return hash.Build();

That's more verbose than the original. And it's only marginally safer by highlighting buggy code if it's near non-buggy code. To me, I think that tradeof is not worth it.

@jnm2
Copy link
Contributor

jnm2 commented Feb 27, 2017

Forgetting to write an update block will be just as common as forgetting to assign.

@jamesqo
Copy link
Contributor Author

jamesqo commented Feb 28, 2017

@chrisaut

so how about

immutableList.=Add(first);

I had syntax like that in mind before, actually. The problem was it seemed pretty awkward because the equals isn't whitespace-separated from the object/method. Although, maybe something like

immutableList <= Add(first);

would be a viable alternative?

Or perhaps the proposed pipe forward operators could make this work (#96), eg.

The forward pipe operator will pass the value to the first parameter of the method, not this. You'd need to modify your examples to use @ syntax. (I have taken a look at the proposal, though, and I do think that forward-pipes seem like they would largely supplant this proposal, so not disagreeing.)

@jamesqo
Copy link
Contributor Author

jamesqo commented Feb 28, 2017

@jnm2

Forgetting to write an update block will be just as common as forgetting to assign.

That's another good point. This proposal will only benefit cases where you have to do 2+ computations, since you still have to write the variable once at the top of the block and once inside the block; there would be no compelling reason for var x = CreateImmutableObject(); x = x.SomeOperation(); return x; to be changed to var x = CreateImmutableObject(); update x in { x.SomeOperation(); } return x;.

Closing per the community's feedback on this.

@jamesqo jamesqo closed this as completed Feb 28, 2017
@svick
Copy link
Contributor

svick commented Feb 28, 2017

@jamesqo

maybe something like immutableList <= Add(first); would be a viable alternative?

I think it would be really confusing if <= had completely different meaning in a statement than in an expression.

@jamesqo
Copy link
Contributor Author

jamesqo commented Feb 28, 2017

@svick Whoops, I had forgotten that <= was already being used. I often get mixed up between <=, =<, >=, and => :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants