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: Allow patterns in foreach statement #6067

Closed
alrz opened this issue Oct 16, 2015 · 25 comments
Closed

Proposal: Allow patterns in foreach statement #6067

alrz opened this issue Oct 16, 2015 · 25 comments

Comments

@alrz
Copy link
Contributor

alrz commented Oct 16, 2015

A syntactic sugar for foreach statement to be able to use patterns in place of the variable declaration,

foreach(let <pattern> in list) {
  ...
}

// equivalent to 

foreach(var $item in list) {
  let <pattern> = $item else continue;
}

If the pattern was not fallible there would be no else continue in the translation.

@alrz
Copy link
Contributor Author

alrz commented Oct 17, 2015

This can also be used to ignore parameters (#20, #2145 and #2397)

@alrz
Copy link
Contributor Author

alrz commented Oct 17, 2015

The problem with this design is that if you try to override some method that has multiple overloads, compiler doesn't know which one you are overriding. So there should be a hint to the compiler about type of the target object.

class Foo {
    public virtual bool Equals(Bar bar) { ... }
}
class Baz : Foo {
    // ambiguous between object.Equals(object) and Foo.Equals(Bar)
    // **if Bar could be possibly a Foo**
    public override bool Equals(case Foo foo) { ... }
    // type hint
    public override bool Equals(object case Foo foo) { ... }
}

Although, this might be addressed by considering pattern compatibility of the case and the target type.

@paulomorgado
Copy link

I'm sorry but I can't understand what you're proposing.

Can you provide more examples of producing/consuming code using this proposal?

@alrz
Copy link
Contributor Author

alrz commented Oct 17, 2015

@paulomorgado The original post is all examples by the way! It's about inserting a case pattern in place of a variable/parameter declaration so one can use extracted values from patterns. Basically

case (int a,int b) => ...

is equivalent to

(Tuple<int,int> t) => t match(case (a,b): ... );

@aluanhaddad
Copy link

If I understand the examples correctly, this seems very powerful and has a lot of potential. More clarification is needed for several of the examples however.
Specifically how is

void F(case (int a, int b)) {}

different from

void F(int a, int b) {} 

which is part of #347?

The same questions apply to lambda expression examples.

Also, what are the element types of enumerables in the foreach and LINQ examples?

This could be great, but I'm, not sure I understand it fully.

@alrz
Copy link
Contributor Author

alrz commented Oct 22, 2015

The former is a tuple-pattern mentioned in #5154 comment. The latter uses regular parameters. Introducing case keyword for patterns help with backward compatibility, because allowing patterns directly in method parameters would dramatically change method declaration syntax and semantics. But this would be a compatible extension to the existing syntax.

In the foreach example, element types will infer from list type (if it's a list of tuples). Otherwise it would be pattern incompatible or types should be specified (this actually depends on pattern specification).

@gafter
Copy link
Member

gafter commented Oct 22, 2015

In your original examples, none of the patterns are complete. So they all produce warnings?

I'm not sure I understand the semantics of any of the proposals exhibited by your examples.

@alrz
Copy link
Contributor Author

alrz commented Oct 22, 2015

@gafter Tuples, single case ADTs and active patterns are considered as a complete pattern in this context (depending on the static type of target object, just like F#). For event handlers foo.Event += (case Foo sender, e) => {}; since the target type is object, or type annotated case (Foo case Bar bar) => {} this might be not complete. However, in overloading via patterns section, if you cover all the cases, then it's complete.

@gafter
Copy link
Member

gafter commented Oct 22, 2015

Tuples, single case ADTs and active patterns are considered as a complete pattern in this context (depending on the static type of target object, just like F#).

But these declarations don't have a target object.

Perhaps I need to understand your proposed overload resolution rules. Can you please at least give me a hint?

@alrz
Copy link
Contributor Author

alrz commented Oct 22, 2015

But these declarations don't have a target object.

It's the parameter. For example in intList.Select(a => ... ) type of a will be inferred from type of intList right? So there is no need to explicitly specify type for it, although you can do intList.Select((int a) => ...) as in explicit-anonymous-function-signature.

Same rules apply to case parameters: tupleList.Select(case (a,b) => ...) Well, type of the parameter is inferred as in implicit-anonymous-function-signature, hence types of a and b will be inferred too because of the static type of the target object which is the parameter type whether it's inferred or it's specified as in (Foo case Bar bar) => ... or (case Foo sender, e) => .... Note that the latter uses a implicit-anonymous-function-signature hence you don't need to specify the type for e.

Overload resolution rules remain the same since there is a static parameter type for each case. But for you to be able to declare multiple versions of the same method with different parameter patterns, identical methods should be merged. So

public override bool Equals(object) => false;
public override bool Equals(case Foo foo) => Equals(foo);

translates to:

public override bool Equals(object obj) =>
    obj match(case Foo foo: Equals(foo) case *: false);

First method Equals(object) uses #6115 which may be considered as case * when compiler is checking for completeness of these patterns.

Second method might be ambiguous if there is another virtual method like Equals(Bar) and if Foo can be possibly a Bar meaning that it's pattern compatible with both object and Bar. In that case you should explicitly annotate the case with the desired target type, like

public override bool Equals(object case Foo foo) => Equals(foo);

In this example , the Equals method used in the method body has the signature Equals(Foo) since Foo can be possibly a Foo obviously, so you must annotate the case with object if Equals(Foo) marked as virtual too.

"Overloading via patterns" is not essentially part of this proposal and might be considered as an additional feature.

@gafter
Copy link
Member

gafter commented Oct 22, 2015

I can't tell what is part of this proposal and what isn't.

Same rules apply to case parameters: tupleList.Select(case (a,b) => ...) Well, type of the parameter is inferred as in implicit-anonymous-function-signature, hence types of a and b will be inferred too because of the static type of the target object which is the parameter type whether it's inferred or it's specified as in (Foo case Bar bar) => ... or (case Foo sender, e) => .... Note that the latter uses a implicit-anonymous-function-signature hence you don't need to specify the type for e.

How is this any different from the same syntax without case?

@alrz
Copy link
Contributor Author

alrz commented Oct 22, 2015

@gafter I meant that this can be done without going through complexity of "multiple methods". F# supports patterns in method parameters but doesn't support this one — which is common in functional languages like Haskell:

isEmpty [] = True
isEmpty _ = False

@alrz
Copy link
Contributor Author

alrz commented Oct 23, 2015

How is this any different from the same syntax without case?

You mean like (Foo foo) => ...? well, that's not a pattern anymore, that's the parameter type itself. while (Foo case Bar bar) => ... defines a lambda which takes a Foo but matches it against the Bar bar pattern. or for event handlers you would define (object case Foo sender, EventArgs e) => .. which takes an object as first parameter to be used as an EventHandler.

@aluanhaddad
Copy link

Overload resolution rules remain the same since there is a static parameter type for each case. But for you to be able to declare multiple versions of the same method with different parameter patterns, identical methods should be merged. So

public override bool Equals(object) => false;
public override bool Equals(case Foo foo) => Equals(foo);

translates to:

public override bool Equals(object obj) =>
    obj match(case Foo foo: Equals(foo) case *: false);

I take this to mean that the purpose is to defer selection of the overload until runtime and then pattern match against the actual value to select the overload that will be invoked.
So if one has

Foo x = new Foo();
object y = new Foo();

var equal = x.Equals(y);

then the value of equal will be true because the overload taking an object is selected at compile time but the use of case to declare a parameter in another overload implies that a pattern match will be performed at runtime and bool Equals(case Foo foo) => Equals(foo); is actually called. If case was not used then equal would be false. Is this the correct interpretation?

If so, how is the following evaluated?

class Foo 
{
    ...
    public override bool Equals(object o) => false;
    public override bool Equals(case Foo foo) => Equals(foo);
    public bool Equals(int i) => i % 2 == 0;
}
...
Foo foo = new Foo();
var equal = foo.Equals((object)4);

I assume the result would be false but that if the third method were modified to be

bool Equals(case int i) => i % 2 == 0;

then it would be true.

@alrz
Copy link
Contributor Author

alrz commented Oct 23, 2015

@aluanhaddad

I take this to mean that the purpose is to defer selection of the overload until runtime

There is no overload and the translation occurs at compile-time.

bool override  Equals(object o) => false;

this is simply an error because you are overriding the method for the second time. Note that I said the Equals(object) in my example is implicitly an Equals(object case *). For equal to be true you would define Foo as:

class Foo 
{
    ...
    public override bool Equals(case Foo foo) => Equals(foo);
    public override bool Equals(case int i) => i % 2 == 0;
    public override bool Equals(case *) => false;

}

If Equals(int) is not part of object.Equals override, then you should call foo.Equals(4) in your example.

but that if the third method were modified to be bool Equals(case int i) => i % 2 == 0;

Then it will be ambiguous with object.Equals. you should either make it override or remove the case from the parameter.

@gafter
Copy link
Member

gafter commented Oct 23, 2015

I think the chances of us supporting a syntax that promotes the body of a method out into the surrounding context is practically nil.

@alrz
Copy link
Contributor Author

alrz commented Oct 23, 2015

@gafter I think the chances of needing to write something like tuples.Select(case (a,b) => ...) is totally not nil. .Select(t => ((var a, var b) = t; ...)) makes no diffrence btw.

@gafter
Copy link
Member

gafter commented Oct 23, 2015

@alrz And I thought C# was Turing-complete...

@alrz
Copy link
Contributor Author

alrz commented Oct 23, 2015

@gafter so is css...

@gafter
Copy link
Member

gafter commented Oct 23, 2015

@alrz So what did you mean by "needing"?

@alrz
Copy link
Contributor Author

alrz commented Oct 23, 2015

@gafter I meant that it would be very common, besides, what else would you do in tuples.Select(t => ...) other than deconstructing the t first? Unless you want to pass it to another method, which in that case, the "another method" will do the deconstruction. That's why I told you before that tuple items' names are not important. Actually in functional languages, the only way to access items is by deconstruction (through patterns).

Tuples, ADTs, records, patterns, all this come from functional paradigm, so normally you would always see this kind of features together in a language. Because when you have one, you will "need" the other. That is the way that it would make sense. for me at least.

You're saying that it's a "syntax that promotes the body of a method out into the surrounding context" because you're looking at it from an object-oriented point of view, while in a functional language that's a totally reasonable feature.

@gafter
Copy link
Member

gafter commented Oct 23, 2015

@alrz Yes, I can see how, if your tuples don't have member names like ours do, you'd benefit from promoting pattern matching into the declaration. But we do have names for our tuple members, and it conflicts with our desire to separate the method's contract (the header, outside its body) from its implementation (inside its body).

@aluanhaddad
Copy link

@alrz I think he is saying this is unnecessary for tuples because you will be able to write

tuples.Select(((a, b)) => ...)

Where a and b are the names of the tuple elements.
@gafter is that correct?

Also with regard to promoting the method body into the surrounding environment, I agree that it is a bad idea in a language with method overloading. Haskell does not have overloading. Scala which does, does not allow this either.

@gafter
Copy link
Member

gafter commented Oct 24, 2015

@aluanhaddad No, that is not correct. We are not proposing to extend the parameter syntax.

@alrz
Copy link
Contributor Author

alrz commented Oct 24, 2015

@aluanhaddad

@gafter so it will not be possible to declare a function, anonymous or otherwise, that takes a tuple as an argument? That is what I meant by ((a, b)) => it has a single argument of type tuple.

What you are using is deconstruction syntax, which is basically a pattern. That's not possible to use patterns directly in method parameters. But I proposed that it could be with a case or whatever keyword like case (a,b) => ...

A lambda that takes a tuple as argument would be possible but probably like ((int,int) t) => ((var a, var b) = t; ...) very intuitive so far. You may add another pair of parentheses if you are passing this to another method.

@alrz alrz changed the title Proposal: Allowing patterns in more places Proposal: Allow patterns in foreach loop Feb 12, 2016
@alrz alrz changed the title Proposal: Allow patterns in foreach loop Proposal: Allow patterns in foreach statement Feb 12, 2016
@alrz alrz closed this as completed Apr 18, 2016
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

4 participants