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

Flatten (Issue #356) #357

Merged
merged 36 commits into from Jan 21, 2018
Merged

Flatten (Issue #356) #357

merged 36 commits into from Jan 21, 2018

Conversation

leandromoh
Copy link
Collaborator

@leandromoh leandromoh commented Oct 12, 2017

Implementation for #356.

Copy link
Member

@atifaziz atifaziz left a comment

Choose a reason for hiding this comment

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

Please fix broken build/test. Thanks!


var result = source.Flatten();

Assert.AreEqual(13, result.Count());
Copy link
Member

Choose a reason for hiding this comment

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

Change assertions to use the more readable constraints approach:

Assert.That(result.Count(), Is.EqualTo(13));

[Test]
public void FlattenOfType()
{
var source = new List<object>()
Copy link
Member

Choose a reason for hiding this comment

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

Use an array instead of List<> here and in other similar cases. A list isn't needed, distracting & verbose.

public class FlattenTest
{
[Test]
public void FlattenOfType()
Copy link
Member

Choose a reason for hiding this comment

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

This name doesn't mean anything.

/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="source"/> is null.</exception>
/// <exception cref="ArgumentNullException"><paramref name="isAtom"/> is null.</exception>
public static IEnumerable<object> Flatten(this IEnumerable source, Func<object, bool> isAtom)
Copy link
Member

@atifaziz atifaziz Oct 15, 2017

Choose a reason for hiding this comment

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

isAtom should be renamed to predicate and the condition inverted such that returning true will flatten the inner sequence in question. I would also change the signature to Func<IEnumerable, bool>.

We can also look at this as a selector/predicate combined? The function signature would become:

public static IEnumerable<object> Flatten(this IEnumerable source, Func<object, IEnumerable> selector)

If the selector function returns null then it is skipped. The benefit of this version is that it would allow flattening via a property of an object when the object itself isn't iterable. The downside is that it overloads the selector function to also serve as a predicate and which won't be obvious from the signature (since we don't have an optional type in .NET).

{
if (e is IEnumerable inner && !isAtom(inner))
{
foreach (var i in _(inner))
Copy link
Member

Choose a reason for hiding this comment

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

How about coding an explicit stack here instead of relying on recursion?


return typeInfo.IsGenericType
? CreateGenericInterfaceInstance(typeInfo)
: CreateNonGenericInterfaceInstance(typeInfo);
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand the purpose of CreateNonGenericInterfaceInstance. Why not just use new NonGenericArgs.Enumerable()?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I tried to follow the pattern, but really it is not necessary here


Assert.AreEqual(13, result.Count());

result.OfType<int>().AssertSequenceEqual(Enumerable.Range(1, 10));
Copy link
Member

Choose a reason for hiding this comment

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

These assertions are not very clear and seem like a shortcut. They don't assert the order in which elements are returned in result. It's much clearer to write the full expected result.

Copy link
Member

@atifaziz atifaziz left a comment

Choose a reason for hiding this comment

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

I made some improvements directly to speed up things. There are just some tests for disposals missing now and then I believe we'll be done.

Thanks!


if (!next)
{
(e as IDisposable)?.Dispose();
Copy link
Member

Choose a reason for hiding this comment

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

Test to exercise this disposal is missing.

{
stack.Prepend(e)
.OfType<IDisposable>()
.ForEach(x => x.Dispose());
Copy link
Member

Choose a reason for hiding this comment

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

Test to exercise these disposals in the event of an exception is missing.

@leandromoh
Copy link
Collaborator Author

I made some improvements directly to speed up things. There are just some tests for disposals missing now and then I believe we'll be done.

Thanks for the help! Surely it speed up.

Tests for dispose were delivery.

@leandromoh
Copy link
Collaborator Author

@atifaziz I finished the development. I added the overload for map an object to a property. IMHO It is only missing the parameter documentation for "selector"

Singular of "series" is "series", not "serie".
Copy link
Member

@atifaziz atifaziz left a comment

Choose a reason for hiding this comment

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

I added the overload for map an object to a property.

I don't think that was the idea. What I said in my earlier suggestion was:

We can also look at this as a selector/predicate combined? The function signature would become:

public static IEnumerable<object> Flatten(this IEnumerable source, Func<object, IEnumerable> selector)

If the selector function returns null then it is skipped. The benefit of this version is that it would allow flattening via a property of an object when the object itself isn't iterable. The downside is that it overloads the selector function to also serve as a predicate and which is not obvious from the signature (since we don't have an optional type in .NET).

Your signature for the selector function is Func<object, object> and then for the predicate function it's Func<IEnumerable, bool>. Looking at just those two, it's not obvious what's going on, especially with the selector (projecting an object to yet another object?). At the very least, the selector should be Func<object, IEnumerable> because it's not surprising to see a projection going from an object (item of a parent sequence) to a (sub-)sequence in the context of a flattening operation. Since an IEnumerable is a reference and we'd have to deal with them being returned as null, I followed that we could use that as a predicate too, for example, to skip some iterables like strings. However, that was me just making a suggestion and was hoping @fsateler and/or you would chime in and we'd reach a consensus on the final design before updating the PR. As it stands right now, I feel the overload worsens things.


using (var test = source.AsTestingSequence())
{
Assert.That(test.Flatten(), Is.EquivalentTo(expectations));
Copy link
Member

Choose a reason for hiding this comment

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

Why not use AssertSequenceEqual to be consistent as in other tests? That is:

test.Flatten().AssertSequenceEqual(expectations);

Assert.That(test.Flatten(), Is.EquivalentTo(expectations));
}

new object[] { inner1, inner2, inner3 }.Cast<IDisposable>().ForEach(seq => seq.Dispose());
Copy link
Member

Choose a reason for hiding this comment

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

Two things. First, why not type the array elements as IDisposable in the first place instead of casting later?

new IDisposable[] { inner1, inner2, inner3 }.ForEach(seq => seq.Dispose())

Second, why not just use using like below?

        [Test]
        public void FlattenFullIteratedDisposesInnerSequences()
        {
            using (var inner1 = TestingSequence.Of(4, 5))
            using (var inner2 = TestingSequence.Of(true, false))
            using (var inner3 = TestingSequence.Of<object>(6, inner2, 7))
            {

                var source = new object[]
                {
                    inner1,
                    inner3,
                };

                var expectations = new object[]
                {
                    4,
                    5,
                    6,
                    true,
                    false,
                    7,
                };

                using (var test = source.AsTestingSequence())
                    test.Flatten().AssertSequenceEqual(expectations);
            }
        }

@leandromoh
Copy link
Collaborator Author

leandromoh commented Dec 26, 2017

@atifaziz sorry, I thought had undertood your idea but I just did it now. I dont like too much about the idea of return null to indicate predicate (as you said, it is not an obvious signature).

How about maintain predicate and selector separated but put selector as Func<object, IEnumerable>? If we can not reach consensus on the final design soon, I suggest merge the PR with the MVP (predicate only) and discuss about selector in a separate thread (new issue).

@leandromoh
Copy link
Collaborator Author

@atifaziz what about the above question?

@atifaziz
Copy link
Member

I suggest merge the PR with the MVP (predicate only) and discuss about selector in a separate thread (new issue).

Let's shoot for this. Doing an MVP is a good thing anyway.

@leandromoh
Copy link
Collaborator Author

@atifaziz The PR is only with the MVP now. ready for merge. can we publish a new release before december finished?

@atifaziz
Copy link
Member

atifaziz commented Jan 3, 2018

can we publish a new release before december finished?

Sorry but was AFK, celebrating.

I'll try and look at it this week.

@leandromoh
Copy link
Collaborator Author

Any news?

@atifaziz
Copy link
Member

Did you rebase and remove commits because that's confused me a bit and we may have lost some work. The history of this PR seems to have diverged at d0297a5. I think it would have been simpler to git revert if you wanted to back out of changes. It also doesn't help if commit messages read “improvements” as it makes the review slower because then I need to go through the physical diff changes and figure out what they're about. All this means that a review that should have taken 5 minutes took 15 and then I had to abort and get back to other work until I have more time to spare.

@leandromoh
Copy link
Collaborator Author

leandromoh commented Jan 11, 2018

Did you rebase and remove commits

I checkout to the last commit without selector overload. But before that, created a new branch with all work (this one for the possíble new PR)

It also doesn't help if commit messages read “improvements” as it makes the review slower

Sorry about the commit messages, i will be more descritive next time.

@atifaziz
Copy link
Member

I checkout to the last commit without selector overload. But before that, created a new branch with all work (this one for the possíble new PR)

Okay, that explains it and so the branch was rewound. As I said, a revert would have been the right call here.

Sorry about the commit messages, i will be more descritive next time.

Cheers! 👍

atifaziz and others added 3 commits January 11, 2018 21:30
This revert is a combination of the following:

- Revert "Use arrays instead of lists"
  This reverts commit c8f59eb.

- Revert "Fix grammar"
  This reverts commit 78fd850.

- Revert "Delete trailing whitespace"
  This reverts commit c38da12.

- Revert "fix"
  This reverts commit 0db744d.

- Revert "improved"
  This reverts commit 68b7a9f.

- Revert "fix"
  This reverts commit 977193a.

- Revert "add selector overload"
  This reverts commit 9480d35.
@atifaziz
Copy link
Member

@leandromoh I've put your changes on top of a revert (c6f59f6) just to maintain the “selector overload” as part of this PR's history and discussion, in case we need to pick it up again.

@leandromoh
Copy link
Collaborator Author

Ok. Thanks.

What about the code itself? Are we ready to merge?

Copy link
Member

@atifaziz atifaziz left a comment

Choose a reason for hiding this comment

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

The finally block needs a final touch/revision so almost there! 🏁

}
finally
{
stack.Prepend(e)
Copy link
Member

Choose a reason for hiding this comment

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

The finally block should be simple rather than use any fancy LINQ to avoid causing allocations under low memory conditions (the reason for which the finally code could be running in the first place). You can re-write this in the same lines of code:

(e as IDisposable)?.Dispose();
foreach (var se in stack)
    (se as IDisposable)?.Dispose();

Note that since Stack<>'s enumerator is a struct, there's no cost to iterating over it using foreach.

@leandromoh
Copy link
Collaborator Author

@atifaziz code refactored! also I added flatten to readme and release notes.

@atifaziz atifaziz merged commit b82b5b7 into morelinq:master Jan 21, 2018
@leandromoh leandromoh deleted the flatten branch April 1, 2018 02:27
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