Trivial record types are a common pattern in functional languages, providing additional type safety over bare numbers or strings or tuple:
```c#
//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:
```c#
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.
```c#
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:
```c#
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:
c#
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.
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.
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.
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.
@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.
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.
@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.
Discussed in https://github.com/dotnet/csharplang/issues/153
Most helpful comment
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.