-
Notifications
You must be signed in to change notification settings - Fork 1k
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
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]: Record Constraints #4453
Comments
Not sure what this has to do with records specifically. Looks like a general purpose argument validation syntax, something akin to method contracts. IMO the suggested syntax looks too much like a default argument and might clash with any potential future enhancements around const expressions. |
Being unable to put constraints (invariants) on my records is currently a major pain point for me. One of my use cases: I am reading data from an API of an external service, e.g. Azure DevOps - Page Stats - Get. I want to capture the data in a record type that enforces invariants, like "day stats for given page have each day at most once" and "day stat for given day, if present, has count of at least 1". Currently I opted to use plain class, so I can put the invariants checks in the primary ctor. Example of what I mean: public class WikiStats {
public DataWithInvariants(IEnumerable<WikiPageStats> data)
{
// invariant checks here
Value = data.ToArray();
}
public WikiPageStats[] Value { get; }
} Question 1What about maintaining invariants when processing the record data, e.g. with LINQ? Even with this proposal implemented I will have to access the underlying data via some member if I want to do some processing, like e.g. WikiStats postprocessedWikiStats = new WikiStats(wikiStats.Value.Select(Process)) but ideally I would like to do the following: WikiStats postprocessedwikiStats = wikiStats.Select(Process) The last snippet magically converts to Of course I could implement my own Question 2What about different error modes for the constraint/invariant violations? I can easily see the invariants being checked:
These two possibly should be treated differently. Have something akin to Question 3What about the alternative of allowing to declare the primary record constructor body? As seen in the Primary constructors in C# 10 proposal. Seems to me that would at least as powerful solution, and possibly simpler to implement, as well as conceptually? |
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
Record Constraints
Summary
This feature provides the ability to declare constraints on record elements using the shorthand syntax. These constraints get incorporated into the record definition.
The proposed syntax is :
record sample(int Foo = Constraint());
or
record sample(int Foo = IntConstraints.IsPositive());
Where
Constraint
is an operation that accepts a single parameter of the target type and returns a value of the target type (int here.)Where
IntConstraints
is a class containing reusable rules that follow the same restrictions.Motivation
The
record
is a useful addition to the language and provides a starting point for many concepts like DataDomains, ValueObjects, Structures (in the OOAD sense), and Records (in the business sense). What differentiatesrecord
from these is thatrecord
is a set of values, and the others often require a set of "Valid Values". You couldn't trust writing arecord
into a ledger without first checking the validity of the data. If you use shorthand syntax there is no way to incorporate these checks into the setters, where they belong. The code grows and becomes more complex by having to create external checking processes and objects. In cases like DataDomains and Records, these constraints are not secondary to the object, but part of it's core identity. These constraints need to be "baked-in".While this can be achieved using the longhand syntax, that unnecessarily loses some of the charm of
record
.This proposal provides a method for declaring and incorporating constraints/data rules to individual elements of the shorthand definition, in a simple, non-breaking way.
Detailed design
Attempting to use the proposed syntax now produces a "compile time constant" error.
But records aren't really compile time items. They are more models for a preprocessor that will generate a compile time classifier.
As such, they don't need to conform to all compiler conditions, only the generated classifier does.
This feature can be implemented with one change and an addition to the code generator.
First, eliminate the "compile time constant" check on parameter defaults for records so it doesn't complain about the default being an operation. If a record is a pre-compile model, this restriction isn't necessary.
Second, when generating the record code, move the call to the constraint operation out of the signature and into the appropriate setter.
The signature now meets the compile criteria, and element level data constraints are in the setters where they belong.
Neither is necessarily breaking. Removing the compile time check is broadening, and the other part just adds an independent step to the generation process.
Any "callable" construct can be used for constraints as long as it accepts a single parameter of the target type and returns a single value of that type (optionally null if the target type is nullable). It should normally depend only on the value provided when a record is created. Though design decisions could relax this in some cases. Because the constraint will get pre-processed, and must follow this format, the constraint doesn't need to show the parameter. It is added in the generation process. This helps keep the syntax compact. Constraints must handle all conditions (applying defaults or throwing exceptions when necessary).
Constraints can be declared externally and shared in many ways. They may also be declared internally.
Drawbacks
Alternatives
Unresolved questions
Design meetings
The text was updated successfully, but these errors were encountered: