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

Deconstruction of formal parameters #15111

Closed
orthoxerox opened this issue Nov 9, 2016 · 7 comments
Closed

Deconstruction of formal parameters #15111

orthoxerox opened this issue Nov 9, 2016 · 7 comments

Comments

@orthoxerox
Copy link
Contributor

Trivial record types are a common pattern in functional languages, providing additional type safety over bare numbers or strings or tuple:

//I'm using the not yet existing record syntax here
public struct EmailAddress(string emailAddress);
public struct Velocity(float velocity);
public struct PointD(double x, double y);

However, using them in C# requires additional deconstructing or constant member access:

public static Result SendEmail(
    EmailAddress to, 
    IEnumerable<EmailAddress> cc, 
    string subject, 
    string body)
{
    var to1 = EmailAddress.EmailAddress;
    //or alternatively...?
    to is EmailAddress(var to2);
    //or...?
    let EmailAddress(var to1) = to;
    
    var cc_ = cc.Select(x=>x.EmailAddress);
    //etc
}

I propose to allow positional deconstruction in method declarations.

public static PointD operator +(PointD(var x1, var y1) p1, PointD(var x2, var y2) p2)
    => new PointD(x1+x2, y1+y2);

Deconstructed parameters can lack their own names if they have only one parameter:

public static Velocity Scale(Velocity(var velocity), Factor(var factor))
    => new Velocity(velocity * factor);
    
//this is transformed into

public static Velocity Scale(Velocity velocity, Factor factor)
{
    var generatedname1 = velocity.Velocity;
    var generatedname2 = factor.Factor;
    return new Velocity(generatedname1 * generatedname2);
}

Deconstruction of types parameterized by deconstructible types, like cc in SendMail above, is an open question:

public static Result SendEmail(
    EmailAddress(var to), 
    IEnumerable<EmailAddress(var cc)>, 
    string subject, 
    string body)
{
    //cc must be an IEnumerable<string>
    //...
}

Perhaps it can be done rather straightforwardly for all monadic (LINQ-able) types.

@orthoxerox orthoxerox changed the title Destructuring of formal parameters Deconstruction of formal parameters Nov 9, 2016
@HaloFour
Copy link

HaloFour commented Nov 9, 2016

I want to say that I've seen comments regarding having patterns supported directly in method parameters. That may have been to permit overloading by pattern matching, although with infallible patterns that would seen to be about identical with this proposal.

/cc @alrz

Even if this were to be supported I'd think that you'd still be required to set formal parameter names as that is what ends up embedded in the CLR metadata and formal contract for the method.

@alrz
Copy link
Contributor

alrz commented Nov 9, 2016

It's been discussed in #6067 and rejected per this comment,

it conflicts with our desire to separate the method's contract (the header, outside its body) from its implementation (inside its body).

However, it'd be always safe to use "complete patterns" in all those places, namely, foreach, out var and parameter declarations without any additional mechanism to handle match failure as proposed here.

@HaloFour
Copy link

HaloFour commented Nov 9, 2016

It would seem that if that reasoning can be applied to dismiss deconstruction of tuples in the parameter syntax that it would likely apply to custom deconstruction as well. Of course those comments are over a year old, and a lot has changed in a year.

@orthoxerox
Copy link
Contributor Author

@alrz I do not propose to allow all patterns in parameters, only those that cannot fail (void-typed deconstructors or is operators or whatever final shape they take). They will not be used to create pseudo-overloads in the style of ML (let map [] func = []; let map x:xs func = func x : map xs func) or affect the success of the call in any way.

@gordanr
Copy link

gordanr commented Nov 10, 2016

Trivial record types definitely provide additional type safety over bare numbers or strings. I use these types in ALL formal parameters in my business domains and have very good experience with that approach. I would like to encourage all developers to start experimenting using 'record types' as a wrapper for bare types.

Of course, instead of not yet existing records, I use something very similar to translated code as shown in documentation (records.md). As a deconstrucion I use emailAddress.Value, velocity.Value, ... which is sometimes long and annoying, but not too much.

Yes. It would be nice to have shorter and easier deconstruction. I'm not sure is this proposal right direction, but in my opinion the topic is very important.

@aluanhaddad
Copy link

aluanhaddad commented Dec 28, 2016

@gordanr

I would like to encourage all developers to start experimenting using 'record types' as a wrapper for bare types.

👍 This is one of those things I continuously kick myself for forgetting, it is well worth the cost.

@orthoxerox

@alrz I do not propose to allow all patterns in parameters, only those that cannot fail (void-typed deconstructors or is operators or whatever final shape they take). They will not be used to create pseudo-overloads in the style of ML (let map [] func = []; let map x:xs func = func x : map xs func) or affect the success of the call in any way.

I don't think that is the issue.

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).

I think @gafter's point is very important. The desire to easily consume parameters via deconstruction or via or patterns will make for an inferior API in many cases that does not clearly express the abstraction that the caller must provide but rather its parts as they relate to their consumption within function body.

Consider an example from another language:
JavaScript has this problem thanks to some of the new features introduced in ES2015

// This is JavaScript for comparative purposes!

// In ES5.1 style (no destructuring)
function submitOrder(order) {
  if (blacklistedNames.includes(order.customer.name) || order.product.id) {  
    return Promise.reject(Error('blacklisted customer' + order.customer.name));
  }
  if (!order.items.some(item => availableProducts.map(product => product.id).includes(item.id))) {
    return Promise.reject(Error('one or more items is currently unavailable'));
  }
  return httpClient.post(`api/${apiVersionCreatedWith}/orders/${id}`,  order);
}

In the above code, the consumer knows that submitOrder takes an order. Now consider what this can look like in ES2015. By leveraging inline destructuring in parameter lists, we can really clean up the function from the callees point of view

// In ES2015
function submitOrder({ id,
  apiVersionCreatedWith,
  customer: { name: custName },
  product: { id: pid },
  items
}) {
  if (blacklistedNames.includes(custName)) {
    return Promise.reject(Error('blacklisted customer' + custName));
  }
  if (!items.every(({ id: id }) => availableProducts.map(({ id }) => id).includes(id))) {
    return Promise.reject(Error('one or more items is currently unavailable'));
  }
  return httpClient.post(`api/${apiVersionCreatedWith}/orders/${id}`, {
    id, customer: { name: custName }, items, product: { id: pid }
  });
}

In the above, I would argue, the simplicity for the caller is completely gone. There is no cohesion in the API, no notion of how the arguments must match up, just a subset of the objects fields associated and renamed as is convenient for the callee to consume. And this is not even by position.

I guess my point here is that convenience of consumption should be secondary to providing a solid API. There are ways to do both but deconstruction directly into an argument list declaration is problematic.

Note the JavaScript in the example can be rewritten with a destructuring binding inside the body to get the best of both worlds, or one can use TypeScript to mask the implementation signature with a proper API surface, but it is easy to get things wrong with this feature.
Note the deconstruction within the implementation is, I think, uncontroversially a great thing, making the code simultaneously shorter an clearer.

@orthoxerox
Copy link
Contributor Author

Discussed in dotnet/csharplang#153

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

7 participants