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

Moving to ValueTuples #3973

Merged
merged 39 commits into from Oct 24, 2019
Merged

Conversation

IgorFedchenko
Copy link
Contributor

@Aaronontheweb After updating only few projects others won't compile because of changing signatures.

Should I continue creating separate PRs per project? In this case we could create some "value-tuples" branch in your repo and safely merge PRs until all projects will be updated and will compile, and then merge to dev with possible conflicts resolving.

Because we can not merge this PR to dev anyway until solution will compile, right?

@IgorFedchenko
Copy link
Contributor Author

Some Tuples are still there in updated projects, because sometimes class types are required instead of structs. Method constraints like where T: class, or Interlocked.Exchange(ref object obj, object val) also requires reference types.
But this is not going to keep Tuples in public APIs anyway.

I am using Search with Pattern ReSharper's feature, and find/replace with regex when I need to replace types when this feature does not help. And then resolving compile errors manually.

@Aaronontheweb
Copy link
Member

Some Tuples are still there in updated projects, because sometimes class types are required instead of structs. Method constraints like where T: class, or Interlocked.Exchange(ref object obj, object val) also requires reference types.

That makes sense.

@Aaronontheweb
Copy link
Member

Should I continue creating separate PRs per project? In this case we could create some "value-tuples" branch in your repo and safely merge PRs until all projects will be updated and will compile, and then merge to dev with possible conflicts resolving.

I'd work backwards - do separate PRs per-project, but do higher level projects that don't have any downstream dependencies (i.e. Cluster.Sharding, Akka.Persistence.Query, Streams) first, the move onto the heavy-duty projects like Akka.Remote, Akka, Akka.Cluster...

@IgorFedchenko IgorFedchenko changed the title Moving to ValueTuples: Updated Akka and Akka.Tests projects Moving to ValueTuples Oct 14, 2019
@IgorFedchenko
Copy link
Contributor Author

All right, now this is going to be single huge PR that will replace Tuples with ValueTuples where possible.

@IgorFedchenko
Copy link
Contributor Author

@Aaronontheweb Have a question about Akka.Streams update.
Our update is going to be breaking change anyway, that is fine. Basically, when moving from Tuple to ValueTuple, we do not allow passing null values anymore.
While in other projects this may be fine (I did not find much issues with this, or even heavy Tuple class usage at all), Akka.Streams project uses tuples a lot, and there are lots of cases when null value is used as a tuple value.

Here is an example, where all big change starts:

public static FlowWithContext<TCtx, TIn, TCtx, TOut2, TMat> Collect<TCtx, TIn, TOut, TOut2, TMat>(
	this FlowWithContext<TCtx, TIn, TCtx, TOut, TMat> flow, Func<TOut, TOut2> fn) where TOut2 : class
{
	var stage = new Collect<(TOut, TCtx), (TOut2, TCtx)?>(func: x =>
	{
		var result = fn(x.Item1);
		return ReferenceEquals(result, null) ? ((TOut2, TCtx)?)null : (result, x.Item2);
	});
	return flow.Via(Flow.FromGraph(stage));
}

The flow.Via method used here is:

public FlowWithContext<TCtxIn, TIn, TCtx2, TOut2, TMat> Via<TCtx2, TOut2, TMat2>(
	IGraph<FlowShape<(TOut, TCtxOut), (TOut2, TCtx2)>, TMat2> viaFlow) =>
	FlowWithContext.From(Flow.FromGraph(Inner).Via(viaFlow));

So it requires (TOut2, TCtx2) tuple, which can not be null, while expression ReferenceEquals(result, null) ? ((TOut2, TCtx)?)null : (result, x.Item2) may return null in some cases.

The solution of making this work without logical change is change (TOut2, TCtx2) to (TOut2, TCtx2)? (make it nullable). And this works well, but after making lots of changes like that turned out that almost all tuples, used in Akka.Streams public API, should be replaced with nullable ValueTuples.

While this can be done, I am getting lots of places where code like Select(t => t.Item1) should be replaced with Select(t => t.Value.Item1) and get Possible null reference exception warning in addition. Lots of places like that, but there is no null check in original code which makes me think that nullable tuples are not expected there.

Additionally, there is lots of code like this:

Source.From(Enumerable.Range(1, 5))
	.MapMaterializedValue<(Task<IImmutableList<int>>, Task<IImmutableList<int>>, Task<IImmutableList<int>>)?>(_ => null);

This is a part of test method, and null is used as materialization value - so I have to cast to nullable ValueTuple here, and in lots of other places. And use .Value property to get result. While obviously this source then chained with other flows and result will not produce null-tuples, compiler forces usage nullable tuples and .Value property.

Moving to nullable ValueTuple may break lots of code because of this. The other option would be to not use null as a result of Akka.Stream methods that are expected to return tuple.

So, other suggestion is - should I change null in such code to default(<tuple_type>) for example? It seems like null is actually not expected anywhere in this methods anyway, so it can be any value.

What do you think?

@Aaronontheweb
Copy link
Member

@IgorFedchenko thanks for a detailed and thoughtful analysis. I'd also take a look at #3282 and see what @vasily-kirichenko did there when he worked on this problem a while back.

Moving to nullable ValueTuple may break lots of code because of this. The other option would be to not use null as a result of Akka.Stream methods that are expected to return tuple.

Nullable value tuples seem like hot garbage in this API, and generally speaking Akka.Streams shouldn't be returning null - what it sounds like we really want is a result that indicates "no results," such as NotUsed.

I think the best approach here is to move away from nullable ValueTuple output, but I say that without knowing how feasible that is - the devil is in the details of the implementation.

Can you make a list, briefly, of the areas where there will be some complex refactoring as a result of losing nullability?

@Aaronontheweb
Copy link
Member

Aaronontheweb commented Oct 16, 2019

A second thought: null items inside a ValueTuple is fine, just not a null ValueTuple itself.

@IgorFedchenko
Copy link
Contributor Author

@Aaronontheweb About null items inside ValueTuple - sure, the issue is with null ValueTuple itself.

In general, I suspect that any time something is returning null tuple value in stream this means that the result is not going to be used. If we need to report special "no result", Option class could be used, otherwise there is no hint that null is possible value of a tuple and everybody would be getting NullReferenceException, all the time. So, my idea is that we can put anything instead of null and this is not going to be used anyway. Only if somewhere there is a special if (tuple == null) DoOneThing() else DoAnother() check, I guess Option class would be used in that case.

This idea is somewhat approved with PR you have referenced. In this PR, some classes that I have issues with are missing (like this - but I know that FlowWithContext is recent feature, or this), but anyway whenever there was returning null in tests it is changed to default(<tuple_type>).

So I am going to use that way and keep going with non-nullable ValueTuples default values. The only place I will appreciate help with so far is this one , because I am not sure about code semantics. Seems like we are converting output elements with preserving context. But why to return null tuple value when conversion function returns null?.. Ok, I see why - this is a case I was talking about, there is a special check here that changes behavior depending on null or non-null values. @Aaronontheweb I am suggesting to change this to use Option class instead of null. Or just check for Item1.HasValue.

What do you think?

@IgorFedchenko
Copy link
Contributor Author

About my last question: I have just tried to use Option in that case, and this will change signature of the method and will break more code, so change Collect class check to check only Item1.HasValue condition would be preferable.

@Aaronontheweb
Copy link
Member

cc @Horusiath - see @IgorFedchenko's post above about FlowWithContext API changes. Might need some help figuring out the best way to rewrite that without using null.

@IgorFedchenko
Copy link
Contributor Author

This function, that returns null if conversion result is null, is used on Collect class in single place. It is stored in _func field and used like this:

var result = WithSupervision(() => _stage._func(Grab(_stage.In)));
if (result.HasValue)
{
	if (result.Value.IsDefaultForType())
		Pull(_stage.In);
	else
		Push(_stage.Out, result.Value);
}

WithSupervision executes passed lambda, _stage._func(Grab(_stage.In)) returns null without exceptions, so result is going to be Option with value null and result.HasValue = true... All right, I see now. IsDefaultForType() method is performing check I was looking for, and this will work with ValueTuple's default value also.

Seems like I got it, but please correct me if I am wrong.

All in all, changing null to default looks like best solution for all cases I was talking about.

@Aaronontheweb
Copy link
Member

@IgorFedchenko that looks like a good solution.

Copy link
Member

@Aaronontheweb Aaronontheweb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM - but we're blocked until #3920 is resolved.

{
var entityId = messageExtractor.EntityId(msg);
var entityMessage = messageExtractor.EntityMessage(msg);
return Tuple.Create(entityId, entityMessage);
return (entityId, entityMessage);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First real piece of non-test related ValueTuple code I've seen (193 files) - this is SO MUCH nicer than what we've had historically with the traditional Tuple<,> syntax. This is going to be great.

@@ -475,7 +475,7 @@ internal static class Shards
case Shard.PassivateIdleTick _:
shard.PassivateIdleEntities();
return true;
case var _ when shard.ExtractEntityId(message) != null:
case var _ when shard.ExtractEntityId(message).HasValue:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And yes, I love this too - no more null checks. Or at least, fewer of them.

@@ -812,14 +812,14 @@ private DataEnvelope SetData(string key, DataEnvelope envelope)

private Digest GetDigest(string key)
{
Tuple<DataEnvelope, Digest> value;
(DataEnvelope, Digest) value;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A nitpick, and we definitely should not try to fit this into this pull request - we should name the ValueTuple properties, especially in public APIs, so users don't have to use the .Item1 and .Item2 properties - this will also allow us to take advantage of C#7/8 language features like tuple deconstruction.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's plan on doing this in separate PRs as part of an ongoing "cleanup" effort, and those can be small case-by-case PRs.

@@ -4780,6 +4780,17 @@ namespace Akka.Util
public static int StringHash(string s) { }
public static int SymmetricHash<T>(System.Collections.Generic.IEnumerable<T> xs, uint seed) { }
}
public struct Option<T>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

{
public class static ObjectExtensions
{
public static Akka.Util.Option<T> AsOption<T>(this T obj) { }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

public static bool IsDefaultForType<T>(this T obj) { }
}
public struct Option<T>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other API changes in this file, all public changes to the Akka.Streams APIs, are fine since they're mostly topical.

This change removes one class from the Akka.Streams.Util namespace and moves it into Akka - totally fine with that too. There's a good possibility one day that as C# adds more functional features to the language that it too will include a first-party Option type. We will cross that bridge when we get there, but for the time being I'm happy having our own third-party solution to that problem.

{
var result = fn(x.Item1);
return ReferenceEquals(result, null) ? null : Tuple.Create(result, x.Item2);
return ReferenceEquals(result, null) ? default((TOut2, TCtx)) : (result, x.Item2);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is probably what we're going to have to do in cases where we can't use Option or null.

// Copyright (C) 2009-2019 Lightbend Inc. <http://www.lightbend.com>
// Copyright (C) 2013-2019 .NET Foundation <https://github.com/akkadotnet/akka.net>
// Copyright (C) 2015-2016 Lightbend Inc. <http://www.lightbend.com>
// Copyright (C) 2013-2016 Akka.NET project <https://github.com/akkadotnet/akka.net>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copyright is off here for some reason

@@ -89,10 +89,10 @@ public TMat2 RunWith<TMat2>(IGraph<SourceShape<TIn>, TMat2> source, IMaterialize
/// <param name="materializer">The materializer.</param>
/// <returns>A tuple containing the (1) materialized value and (2) a new <see cref="Sink"/>
/// that can be used to consume elements from the newly materialized <see cref="Sink"/>.</returns>
public Tuple<TMat, Sink<TIn, NotUsed>> PreMaterialize(IMaterializer materializer)
public (TMat, Sink<TIn, NotUsed>) PreMaterialize(IMaterializer materializer)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Areas like these on the public APIs are where I'd really appreciate having some names for the items inside the tuple, again for deconstruction purposes. I think we can add these in separate PRs.


if (tuple == null)
if (!(message is ValueTuple<T, T> tuple))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@Aaronontheweb Aaronontheweb added this to the 1.4.0 milestone Oct 18, 2019
@Aaronontheweb
Copy link
Member

Looks like my other PR didn't totally fix the issue on Linux - have some other build failures stemming from NuGet.exe failing to download... wonder if there's some azure connectivity problems, given that we had issues with pipelines on that other PR too.

@IgorFedchenko
Copy link
Contributor Author

IgorFedchenko commented Oct 22, 2019

@Aaronontheweb In error report I still see errors like this:

Routing/ConsistentHash.cs(51,17): error CS8356: Predefined type 'System.ValueTuple`2' is declared in multiple referenced assemblies: 'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' and 'System.ValueTuple, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51' [/home/vsts/work/1/s/src/core/Akka/Akka.csproj]
Routing/ConsistentHash.cs(50,17): error CS8356: Predefined type 'System.ValueTuple`2' is declared in multiple referenced assemblies: 'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' and 'System.ValueTuple, Version=4.0.3.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51' [/home/vsts/work/1/s/src/core/Akka/Akka.csproj]

But did not noticed anything related to connectivity issues, at least at that log... Should I take some time to try to resolve this? You fix looks good, and it is not clear where does it get reference to System.ValueTuple, Version=4.0.3.0

@IgorFedchenko
Copy link
Contributor Author

Hmm, can not reproduce this errors locally, when trying to build in inside mcr.microsoft.com/dotnet/core/sdk docker container all is building fine with dotnet build -f netstandard2.0. @Aaronontheweb Do you have some hints to reproduce this issue outside of CI? I guess this is some environment configuration difference

@Aaronontheweb
Copy link
Member

My money is on the Mono compiler struggling with ValueTuple in some of the projects

@Aaronontheweb
Copy link
Member

Don't have this issue with .NET Core compilation on Linux

@IgorFedchenko
Copy link
Contributor Author

IgorFedchenko commented Oct 23, 2019

@Aaronontheweb Seems like you were right - disabling ValueTuple package reference under mono fixed the problem.

But we get another one, during Akka.MultiNodeTestRunner.Shared.Tests.csproj project build:

AsyncContext/AsyncContext.TaskQueue.cs(39,32): error CS8356: Predefined type 'System.ValueTuple`2' is declared in multiple referenced assemblies: 'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' and 'System.ValueTuple, Version=4.0.1.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51' [/home/vsts/work/1/s/src/core/Akka.Tests.Shared.Internals/Akka.Tests.Shared.Internals.csproj]

I could not quickly find where does this project get that reference - seems like it is inside one of the referenced packages... Anyway, this does not belong to public API, so I just rolled back Tuple<T1,T2> for this class, and it did not break anything.

@IgorFedchenko
Copy link
Contributor Author

@Aaronontheweb Build issues under linux are resolved now, but I had to make 2 more small fixes (rolling back to Tuple<T1, T2>) in two more places in test assemblies.

While this should not be a problem to the users, we will not be able to use ValueTuple in that projects because of mono issues. This projects do not reference System.ValueTuple directly, so I suspect that some of the referenced packages are referencing it. So this may be a problem to resolve such conflicts without modifying their sources.
But if we will detect which reference is a source of the problem, possibly we'll be able to fix that (or at least send PR for fixing that in the future). Not sure if this should be a part of this PR, so maybe better to put it in separate issue?

@Aaronontheweb
Copy link
Member

But if we will detect which reference is a source of the problem, possibly we'll be able to fix that (or at least send PR for fixing that in the future). Not sure if this should be a part of this PR, so maybe better to put it in separate issue?

I'd keep a separate PR for that - this will probably be fixed as a result of changing which version of .NET Framework / .NET Core / .NET Standard we ultimately support as part of Akka.NET v1.4.0.

@Aaronontheweb
Copy link
Member

@IgorFedchenko

Akka.Persistence.TestKit.Tests.CounterActorTests.CounterActor_internal_state_will_be_lost_if_underlying_

Failure on all 3 platforms

@IgorFedchenko
Copy link
Contributor Author

There may be race conditions because of creating new actor with same name as terminated actor from outside of actor's parent. Committed naming fix to avoid such conflict in this test.

@Aaronontheweb Aaronontheweb merged commit c09c300 into akkadotnet:dev Oct 24, 2019
@Aaronontheweb
Copy link
Member

Couldn't get that last check to re-run, but it was a few racy specs that have a high flip rate. We should probably disable them. Nice work @IgorFedchenko .

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

Successfully merging this pull request may close these issues.

None yet

2 participants