Proposal: "with" as a LINQ query expression clause (for Zip()) #117
Replies: 12 comments 1 reply
-
I'm using zip() more often than I'd like too, but mostly user-defined variants, since the standard zip() implementation often doesn't work the way I want when dealing with differently-sized IEnumerables. Thus, I'd rather like a keyword which allows me to define my own zip method, without requiring me to begin using the
instead of
The delegate in the with-clause might accept any two IEnumerables: The first one defined by the from clauses above, containing all variables defined until then, maybe represented as objects with the type of the new tuple construct), and the second one defined by the from clause just in front of it. If non-existant, this "with" statement would default to "return the cross product of those two IEnumerables", as multiple from statements already do; if it is specified, we use this one instead, and can create a Zip, a union or something else with it, as long as the returned values have the right type. The type of the method passed as delegate would (only in the example above!) have to fit this delegate, which can be determined at compile-time:
Zip() would make use of the first option, not touching the names of the variables, because the property names of the tuple are already unique in the existing scope. Later, with another possible delegate type, one might also extend the with syntax to allow Unions, Intersects or similar method calls:
Allowing the compiler to allow this usage:
|
Beta Was this translation helpful? Give feedback.
-
Prototype ready in https://github.com/orthoxerox/roslyn/tree/features/linq-with |
Beta Was this translation helpful? Give feedback.
-
Today, you can abuse from x in Enumerable.Range(1, 2)
join y in Enumerable.Range(1, 2) on 1 equals 1
join z in Enumerable.Range(1, 3) on 1 equals 1
select new { x, y, z } The ugly part is from x in Enumerable.Range(1, 2)
join y in Enumerable.Range(1, 2)
join z in Enumerable.Range(1, 3)
select new { x, y, z } then it simply translates to calling |
Beta Was this translation helpful? Give feedback.
-
Zip ain't a cross join. A cross join is expressed as multiple sequential |
Beta Was this translation helpful? Give feedback.
-
That may seem to be the effect but multiple For example: from x in Enumerable.Range(1, 2)
from y in Enumerable.Range(1, x) // you can use & depend on x here
from z in Enumerable.Range(1, x + y) // you can use & depend on x and y here
select new { x, y, z } But here, you can't: from x in Enumerable.Range(1, 2)
join y in Enumerable.Range(1, 2) // can't use x
join z in Enumerable.Range(1, 3) // can't use neither x nor y
select new { x, y, z } In current LINQ's |
Beta Was this translation helpful? Give feedback.
-
@atifaziz But cross-join is still very different from a zip. For example, with your " from x in Enumerable.Range(1, 2)
join y in Enumerable.Range(1, 2) on 1 equals 1
select new { x, y } the output is:
But for zip: Enumerable.Range(1, 2).Zip(Enumerable.Range(1, 2), (x, y) => new { x, y }) the output is:
|
Beta Was this translation helpful? Give feedback.
-
Right. And @orthoxerox, in asking for |
Beta Was this translation helpful? Give feedback.
-
@svick @chrisoverzero @orthoxerox When I wrote my comments earlier, I was being quick and brief as I was having quite a packed Friday. Reflecting back, I see that I might have done more harm than good so now that I am having a quiet Sunday, let me take the time to explain the position a bit better. Before I proceed, I want to add the disclaimer that I am neither an academic nor do I have any experience in language design. I am coming at this purely from the angle of mechanical/logical deduction and floating an alternative that requires the least change. I don't want to imply that a cross-join is the same thing as a zip and perhaps borrowing cross-join from set theory may have been misleading. Nevertheless, l think we all agree that the following gives you a cross-join for lists: from x in Enumerable.Range(1, 2)
join y in Enumerable.Range(1, 2) on 1 equals 1
join z in Enumerable.Range(1, 3) on 1 equals 1
select new { x, y, z } What I am suggesting is that the public static IEnumerable<TResult>
Join<TOuter,TInner,TKey,TResult>(
this IEnumerable<TOuter> outer,
IEnumerable<TInner> inner
Func<TOuter,TKey> outerKeySelector,
Func<TInner,TKey> innerKeySelector,
Func<TOuter,TInner,TResult> resultSelector); it would instead look for: public static IEnumerable<TResult>
Join<TOuter,TInner,TResult>(
this IEnumerable<TOuter> outer,
IEnumerable<TInner> inner
Func<TOuter,TInner,TResult> resultSelector); When you drop the key projection functions and look at that signature, it's identical to public static IEnumerable<TResult>
Zip<TFirst,TSecond,TResult>(
this IEnumerable<TFirst> first,
IEnumerable<TSecond> second,
Func<TFirst,TSecond,TResult> resultSelector); The parallel stands out even more if you name the type and method parameters identically across the two: IEnumerable<V> Join<T, U, V>(this IEnumerable<T> xs, IEnumerable<U> ys, Func<T, U, V> f);
IEnumerable<V> Zip <T, U, V>(this IEnumerable<T> xs, IEnumerable<U> ys, Func<T, U, V> f); This is what I meant by “it's just For most computational contexts that I've worked with that aren't about sets or sequences, and this is important, the two will have an identical implementation! That is, from x in Some(1)
join y in Some(2)
join z in Some(3)
select new { x, y, z } You can do this today with multiple from x in Some(1)
from y in Some(2)
from z in Some(3)
select new { x, y, z } but it's unnecessary and even restricting when a Returning to sequences, if we have cross-join then we can support Suppose the following wrapper: sealed class ZipList<T> : IEnumerable<T>
{
readonly IEnumerable<T> source;
public ZipList(IEnumerable<T> source) =>
this.source = source;
public IEnumerator<T> GetEnumerator() => source.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public ZipList<V>
Join<U, K, V>(IEnumerable<U> second,
Func<T, K> k1, // unused!
Func<U, K> k2, // unused!
Func<T, U, V> resultSelector) =>
source.Zip(second, resultSelector).ToZipList();
} Suppose further an extension method that converts a sequence into a zip-list: static partial class ZipList
{
public static ZipList<T> ToZipList<T>(this IEnumerable<T> source) =>
new ZipList<T>(source);
} Now you can write: from x in Enumerable.Range(1, 2).ToZipList()
join y in Enumerable.Range(1, 3) on 1 equals 1
join z in Enumerable.Range(1, 4) on 1 equals 1
select new { x, y, z } It evaluates to:
which is to be expected because Of course, the annoying part of If you want to start with a zip-list in the partial class ZipList
{
public static ZipList<T> Return<T>(this T item)
{
return _(item).ToZipList();
IEnumerable<T> _(T item) { for (;;) yield return item; }
}
} If you want, you can even hack a bogus from _ in ZipList.Return(0)
join x in Enumerable.Range(1, 2) on 1 equals 1
join y in Enumerable.Range(1, 3) on 1 equals 1
join z in Enumerable.Range(1, 4) on 1 equals 1
select new { x, y, z } I am not advocating this; it's just for sake of illustration. Anyway, the cool thing with these joins is that you can zip to any product without overloading on arity. Hope this makes better sense. |
Beta Was this translation helpful? Give feedback.
-
@atifaziz That does clarify what you meant. Though I think adding syntax for zip still makes more sense than supporting cross-joins:
|
Beta Was this translation helpful? Give feedback.
-
I find that to be odd reasoning for something that exists in the language today and where one half of it can be made optional (subtracting versus adding). It's like saying that cross-joins don't make sense when we already have
I am not a Haskell expert (not even a beginner) but I believe that's the approach taken there and I hope that gives it some merit. There is exactly a wrapper type for lists called -- | Lists, but with an 'Applicative' functor based on zipping.
newtype ZipList a = ZipList { getZipList :: [a] }
instance Applicative ZipList where
pure x = ZipList (repeat x)
liftA2 f (ZipList xs) (ZipList ys) = ZipList (zipWith f xs ys) The difference is that
Except to see the LINQ query syntax only through the eyes of sequences would be short-sighted. It gives you a common vocabulary across computational expressions. The dependence created with
It's not that I don't care. It's that you'll find it's one and the same for other computations that produce one result. The exception here is rather sequences. |
Beta Was this translation helpful? Give feedback.
-
@atifaziz I asked for zip support because I actually want zip support for sequences. Your suggestion changes the semantic for sequences from pairwise combination to Cartesian product. |
Beta Was this translation helpful? Give feedback.
-
I'm sad to see this still hasn't gotten any traction, I'd love to see a liftA2 or zip-shaped operator in LINQ. Enabling what is essentially ApplicativeDo syntax to C# would be tremendous in supporting the growing number of devs that use functional techniques in C#. |
Beta Was this translation helpful? Give feedback.
-
Pasted from dotnet/roslyn#8221
I propose to add a
with
clause to the query expression syntax. This will allow us to zip sequences together using either LINQ syntax.A query expression with a
with
clause followed by aselect
clauseis translated into
A query expression with a
with
clause followed by something other than aselect
clauseis translated into
This transformation would happen in section 7.16.2.4 of the spec:
is first translated into
and then translated into (if I understand how transparent identifiers work)
And
is first translated into
and then translated into
2017 update: I won't mind if this is called
zip join
or anything similar, butwith
makes sense for more LINQables thanIEnumerable<T>
.Beta Was this translation helpful? Give feedback.
All reactions