-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
Support pattern-matching in let clauses in Linq #6877
Comments
You could make it also where as a where clause and omit anything that doesn't match. |
I agree, letting from person in list
let Student { Grade is var grade, Name is var name } = person else continue
where grade > 65
select new { Grade = grade, Name = name }; |
@mattwar I agree, but which Linq method would it translate into? |
@HaloFour that's exactly the issue I was thinking about, it implicitly skips failed patterns. but |
With Going with implicit filtering: from person in list
let Student { Grade is var grade, Name is var name } = person
where grade > 65
select new { Grade = grade, Name = name }; converted into: list
// beginning of let expansion
.Select(person => person switch (
case Student { Grade is var grade, Name is var Name } :
(true, person, grade, name)
case * :
(false, person, default(int), default(string))
))
.Where(tuple => tuple.Item1)
.Select(tuple => new { person = tuple.Item2, grade = tuple.Item3, name = tuple.Item4 })
// end of let expansion
.Where(projected => projected.grade > 65)
.Select(projected => new { Grade = projected.grade, Name = projected.name }); |
I'd say it could be list
.Let(person => person switch( ... ))
.Where($x => $x.grade > 65)
.Select($x => new { Grade = $x.grade, Name = $x.name });
public static IEnumerable<TResult> Let<T, TResult>(
this IEnumerable<T> source,
Func<T, TResult> selector)
{
foreach(var item in source) {
var result = selector(item);
if (result != null) yield return result;
}
} |
@alrz What does |
@gafter It will be an anonymous type anyway, I forgot to put the |
@alrz No, it is not required to be an anonymous type. We were hoping to start using tuples in many cases for translating |
@gafter Great point. The compiler could spit out a private static function that accepts the original range variable (or a tuple of all current range variables) and returns an Going with implicit filtering: from person in list
let Student { Grade is var grade, Name is var name } = person
where grade > 65
select new { Grade = grade, Name = name }; converted (somewhat) into: IEnumerable<(Person, int, string)> $match(Person person) {
switch (person) {
case Student { Grade is var grade, Name is var Name }:
yield return (person, grade, name);
default: yield break;
};
};
list
.SelectMany($match)
.Where(projected => projected.Item2 > 65)
.Select(projected => new { Grade = projected.Item2, Name = projected.Item3 }); |
@HaloFour The Linq pattern is not tied to |
@gafter Apologies for botching the example. I didn't imply that it would be tied to |
@gafter Wait, my example was correct. I'm not sure where the confusion is? |
into
? Is it possible to avoid the allocation for that array? |
The compiler wouldn't be forced to impose the restriction of closures not being iterators on itself. I assume that the compiler would emit a private static iterator function and then pass a delegate to that function to Your example using arrays is much clearer than mine in illustrating the concept, I think. |
What about this approach? var students =
from person in people
where person is Student student
select student;
//is transformed into
var students = people
.Select(person => (person is Student student)?(true, student):(false, default(Student)))
.Where(tuple => tuple.Item1 = true)
.Select(tuple => tuple.Item2); |
@orthoxerox Looks pretty similar to my first suggestion for an expansion, although you're using |
Why are all you folks tying this to Enumerable (e.g. using |
@HaloFour Your first proposed expansion (also @orthoxerox 's most recent proposed expansion) would work. |
@gafter I thought you said SelectMany above; I was trying to figure out how that would have worked. I think @HaloFour's tuple expansion #6877 (comment) is better. It seems odd to have |
@bbarry I thought |
@gafter Indeed, implementing this via Anyway, that is all implementation details. I'd be more concerned with hammering down the semantics and syntax (and variations thereof) at this stage and worrying about how exactly that could be expanded later. Particularly of interest would be any capability of this feature functioning as a filter as well as a projection, either by default or through additional syntax. Aside that, it is nice to hear that LINQ may take advantage of tuples for |
@HaloFour static M<U> SelectMany<T, U>(this M<T> m, Func<T, M<U>> k) No need to mention, it's the bind operator. |
@alrz How would you use |
Nevertheless, I think for from item in db.Products
select (item.Name, item.Model) it would be reasonable that it be translated as db.Products
.Select(item => new {item.Name,item.Model})
.Select(t=>(t.Name,t.Model)) Otherwise, if you accidentally use a tuple for projecting |
You'd be opting-into tuples explicitly in that case which is very different from whatever |
@HaloFour I'm thinking that EF guys should be loud about this and write a big bold statement somewhere that states DON'T USE TUPLES, while the main motivation behind implementing the same behavior as anonymous types (inferring item/property names from the input expression) is using it with EF (at least this is what it would be expected). |
There is nothing stating that linq expression tree visitor code couldn't be modified to support tuples, that is an enhancement on those things. I think it would be useful for the expression tree representing that tuple to know the compile time names used, but I don't see why a person who "accidentally" projects an |
@alrz I bet they add support for projecting to tuples long before any of this ships. |
@bbarry I would totally love that. Expression trees are generated in compile-time, so are tuple names. nothing should stop you from being able to get those sweet names from an expression tree (#2060). |
If we unify nullables, with help of higher kinded types I think we can implement generic LINQ operations for nullables and utilize it for incomplete Besides, although we have nullable reference types, |
I'm going to posit that by default a failed pattern should result in a run-time exception. As with pattern matching in any other context the compiler should go to whatever lengths it can to prevent that. Where the pattern may be fallible I think that reusing the In the case of from person in list
let Student { Grade is var grade, Name is var name } = person else continue
where grade > 65
select new { Grade = grade, Name = name };
// translated into
list
// beginning of let expansion
.Select(person => person switch (
case Student { Grade is var grade, Name is var Name } :
(true, person, grade, name)
case * :
(false, person, default(int), default(string))
))
.Where(tuple => tuple.Item1)
.Select(tuple => new { person = tuple.Item2, grade = tuple.Item3, name = tuple.Item4 })
// end of let expansion
.Where(projected => projected.grade > 65)
.Select(projected => new { Grade = projected.grade, Name = projected.name }); In the case of from person in list
let Student { Grade is var grade, Name is var name } = person else break
where grade > 65
select new { Grade = grade, Name = name };
// translated into
list
// beginning of let expansion
.Select(person => person switch (
case Student { Grade is var grade, Name is var Name } :
(true, person, grade, name)
case * :
(false, person, default(int), default(string))
))
.TakeWhile(tuple => tuple.Item1)
.Select(tuple => new { person = tuple.Item2, grade = tuple.Item3, name = tuple.Item4 })
// end of let expansion
.Where(projected => projected.grade > 65)
.Select(projected => new { Grade = projected.grade, Name = projected.name }); And, for completeness, support from person in list
let Student { Grade is var grade, Name is var name } = person else throw new InvalidOperationException("oops!")
where grade > 65
select new { Grade = grade, Name = name };
// translated into
list
// beginning of let expansion
.Select(person => person switch (
case Student { Grade is var grade, Name is var Name } :
(person, grade, name)
case * :
throw new InvalidOperationException("oops!")
))
.Select(tuple => new { person = tuple.Item1, grade = tuple.Item2, name = tuple.Item3 })
// end of let expansion
.Where(projected => projected.grade > 65)
.Select(projected => new { Grade = projected.grade, Name = projected.name }); And, of course, multiline embedded statements: from person in list
let Student { Grade is var grade, Name is var name } = person else {
Console.WriteLine("whoa, unexpected!");
continue;
}
where grade > 65
select new { Grade = grade, Name = name };
// translated into
list
// beginning of let expansion
.Select(person => {
switch (person) {
case Student { Grade is var grade, Name is var Name } :
return (true, person, grade, name);
case * :
Console.WriteLine("whoa, unexpected!");
return (false, person, default(int), default(string));
}
})
.Where(tuple => tuple.Item1)
.Select(tuple => new { person = tuple.Item2, grade = tuple.Item3, name = tuple.Item4 })
// end of let expansion
.Where(projected => projected.grade > 65)
.Select(projected => new { Grade = projected.grade, Name = projected.name }); |
@HaloFour Still, you are translating My two cents, extension<T> IEnumerable<T> {
public IEnumerable<TResult> Select<TResult>(Func<T, Option<TResult>> selector) {
foreach(var item in this) {
let Some(var value) = selector(item) else continue;
yield return value;
}
}
}
from obj in list
let Foo foo = obj
select foo;
list.Select(obj => obj is Foo foo ? Some(foo) : None());
from person in list
let Student { Grade is var grade, Name is var name } = person
where grade > 65
select new { Grade = grade, Name = name };
list.Select(person => person is Student { Grade is var grade, Name is var name } ?
Some((grade,name)) : None())
.Where(t => t.grade > 65)
.Select(t => new { Grade = t.grade, Name = t.name}); Then you might think of Using tuples for fallible patterns is not intuitive at all, because still you need to allocate |
There is nothing that states that a LINQ clause must be directly translated to a single extension method. I am working with what I have. .NET 3.5 LINQ has no single function which performs both a map and a filter. If a future version of the BCL does include such a function then that would be a good target for optimization, but as it stands I see no reason to limit the feature to some not-yet-released CLR. I am aware that I am using tuples in my example translation which would not exist until a new BCL is released. When targeting a currently released framework the translation would use anonymous types instead, just as it does today with
I think it makes a lot of sense and also provides good symmetry for the non-LINQ destructuring form of
The developer doesn't have to do anything. The use of anonymous types, tuples or even Also, I don't think that going for an "overload" of |
I think that was the first thing that came up and lead to
Same was true for
So, perhaps you need to end your embedded statement with a semicolon? Furthermore, embedding an embedded statement in a query expression doesn't seem like a good idea.
True, but those values will be allocated eventually right? |
That was a conceptual implementation that would have avoided the multiple extension methods but would have created an unwanted dependency on
Actually
Why? It's not the end of the expression. I don't see any problem with embedding a statement or a block in a query expression. However if that would be considered confusing I personally would be fine with that statement being limited to
No, |
@HaloFour "Why?" because the semicolon is a part of an embedded statement. "the clause does not need to be an expression since no value is being returned." That is also true for |
embedded-statement is a list of other potential statements. Some of those statements do have semicolon requirements, although block doesn't. I'd be fine if the spec for
Probably because you're using the expression form of
64-bits of empty space, no big deal. I'm all for going with a more optimal route given a newer BCL but I think it's ridiculous to not permit a language feature to work on the older frameworks when there is clearly an easy way to implement it, even if it might be slightly less efficient.
I managed to accidentally find some kind of keyboard combination which submits the comment immediately. I don't even know what I hit 😦 |
@HaloFour As the last inquire, would it make sense to turn from obj in list
let Foo foo = obj else continue
do WriteLine(foo.Bar);
foreach(var obj in list) {
let Foo foo = obj else continue;
WriteLine(foo.Bar);
} |
@alrz Works for me. I'd want the compiler to use the most efficient implementation that it could. |
@alrz LINQ from x1 in e1
let P1 x2 = e2 else continue
do s3; would be rewritten into: e1.Select(x1 => {
let P1 x2 = e2 else return;
{ s3; }
}) |
Issue moved to dotnet/csharplang #355 via ZenHub |
Given the let-statement proposed in the draft pattern-matching specification for #206, it has been proposed by @alrz to extend the
let
clause in Linq to support pattern-matching as well:It isn't clear how this would be expected to work when the pattern is fallible.
The text was updated successfully, but these errors were encountered: