dotnet / csharplang

The official repo for the design of the C# programming language
11.3k stars 1.02k forks source link

Champion "Method Contracts" #105

Open gafter opened 7 years ago

gafter commented 7 years ago

See also https://github.com/dotnet/roslyn/issues/119

Lexcess commented 7 years ago

@GlassGrass Perhaps they could refer to state outside a method, but I think that would be bad practice to do so. In the example you give T would be the type parameter, with the syntax proposed you'd only have a collision if arg was named T and that even now is a compile error.

That kind of functionality was more what Invariant was trying to solve. However that Invariant is a whole other can of worms.

Serg046 commented 7 years ago

Yes, looks like we need other syntax for invariants if it is going to be supported.

aka-nse commented 7 years ago

@Lexcess I got the point how we solve the name confliction problem.

But still we should be careful there are methods that have to depend on the state outside, e.g. MonoBehaviour of Unity. Unity framework does not allow us to define custom constructor of MonoBehaviour subclasses. Futhermore, some object (e.g. GUISkin) can construct only in OnGUI method scope. This fact causes that custom MonoBehaviour objects are not invariant with IsInitialized state.

We can take the strategy to give up support non-invariant/outside state, but we should keep in mind quite a little demands will be neglected.

Lexcess commented 7 years ago

@GlassGrass I understand completely, even without going to implementation specific restrictions there are trivial examples where operations are only valid within certain contexts.

I also agree we are giving up something for now, but if this proposal comes together and is successful we can build up from there. That said, if somebody thinks they can chew it off now, I'd love to see the specification. I'm trying to find some time here and there to specify the limited implementation described above and that is more than enough for me!

gulshan commented 6 years ago

Contract is about correctness checking. Unit test does the same. I think, simple unit tests can be introduced into the language with contracts. There can be a test keyword for tests, which can look something like-

class Adder
{
    private int state { get; set; } = 0
    public int Add(int value)
        requires value >= 0
        ensures return >= 0
        test return == 2 && state == 2 using(this = new Adder()){ state = 1; Add(1); }
    {
        state += value;
        return state;
    }
}

These tests has to be invoked separately and doesn't have anything with compilation. The syntax may not look pretty, as I have just invented it. Like for/when can be used instead of using etc.

ghost commented 6 years ago

As I explained in dotnet/csharplang#1198, this syntax will be shorter:

public <0,Count>int Insert(T item, <0,Count>int index)   {
    return InsertCore(item, index);
}

than

public int Insert(T item, int index)
    requires index >= 0 && index <= Count
    ensures return >= 0 && return < Count
{
    return InsertCore(item, index);
}

If you constrained the data type, then you've already constrained all other language elements built with it.

ghost commented 6 years ago

So, there are 4 possible ways to do it:

  1. Method contracts. It is verbose but can write generic conditions.
  2. Constrained Data Types: It is a compact syntax and can be extended to use regular expressions.
  3. Validation attributes: Like those used in ASP.NET MVC. It has no effect on the c# syntax if self, and we can use parameters that indicate the error message or other actions. But using attribute for individual method parameters and return value can make the code long and ugly!
  4. Writing an analyzer, which I cannot exactly understand !

Sure we all want some syntax to simplify validation codes. We can make compromises. For example: If the constrained types can cause problems, they can only be used to define properties and methods (parameters and return values). They are nit true types but just syntax annotations to tell compiler how validate values, so the compiler can convert then to contracts under the hood. Attributes also can be used with properties and methods to supply error message and additional options to tell the compiler what to do when the data are not valid. This approach says that contracts can still be used in more complicated situations (but I see that it is easer to use regular code statements)

Lexcess commented 6 years ago

@MohammadHamdyGhanem

Interesting stuff.

On your points:

  1. It can be verbose, but that is somewhat the cost of flexibility.
  2. Discuss below. But I'm assuming here that these are really type decorators not actual new types
  3. I'd say that validation attributes are a separate concern to Method Contracts. Validation concerns itself with input data making no assumption about the quality of data coming in. In contrast contracts do assume that the data is coming in in a certain state and aim to fail fast if that state is not met.
  4. I certainly agree that a compiler based analyser is something that is unlikely to happen!

So going onto your proposal. I see why you like the terseness. I guess I'm a little concerned that it looks like you are creating a new type when actually it is a type decorator. When you type int? you are actually creating a new different type to int. I could see that leading to confusion. In fact, I'm not sure why you wouldn't just use a standard Attribute syntax at that point (i.e. [Int32RangeAttribute(0,10)]).

Second while this terseness works for range limitations, how would more complex constraints be applied? Would there need to be language support for them? Would there be a set of prebaked constraints and that would be it? I can't help feel that it would be a bit limiting.

If we do assume that we could define your own constrained type decorators with custom rules I can't help but feel you then lose one of the key advantage of Method Contracts. That the rule implementation is inline in the method. To me this is one of the key advantages over unit tests; you don't need to go somewhere else to see the expectations of your code.

All that said I don't see why such proposals couldn't live side by side, in many ways your proposal feels a lot like how DataContracts work.

Just a quick aside to finish; when talking about conceptual constrained data types I actually quite like the idea of objects that enforce their own consistency (especially when there are complex rules). It follows some fairly good OO conventions and I could see people easily layering this over a method contract implementation while losing some of the verboseness you mention.

DzonnyDZ commented 6 years ago

What about validation attributes if attributes will be extended to accept expressions (lambda methods) as parameters? These parameters can be either passed like EF LINQ (code as data) or compiler can create some hidden method and pass MethodInfo; or it can still generate the came IL contracts currently generate.

Lexcess commented 6 years ago

@DzonnyDZ I'd be fine with the underlying mechanism being attribute based. I do think having keywords as syntactic sugar gives more flexibility to change implementation long term and also helps reduce verbosity and visual clutter.

The current CC IL isn't the main value, ultimately you could write that guard code yourself. The main consideration would still be having the compiler ideally applying some level of static analysis but at least (and more likely in the short term) provide some control over what is emitted at compile time (I.e. only emitting on debug or on public surfaces).

DzonnyDZ commented 6 years ago

When Code Contracts are in question I'm definitely for special language support. The idea with attributes was just in case special syntax would not be acceptable for whatever reason. Also having the possibility to have expression trees as attribute parameters may have value outside of Code Contracts - but that's different story.

Ing. Jan Záruba dzonny.dz@gmail.com +420 728 886 739

Dne 27. 12. 2017 8:37 odp. napsal uživatel "Lexcess" < notifications@github.com>:

@DzonnyDZ https://github.com/dzonnydz I'd be fine with the underlying mechanism being attribute based. I do think having keywords as syntactic sugar gives more flexibility to change implementation long term and also helps reduce verbosity and visual clutter.

The current CC IL isn't the main value, ultimately you could write that guard code yourself. The main consideration would still be having the compiler ideally applying some level of static analysis but at least (and more likely in the short term) provide some control over what is emitted at compile time (I.e. only emitting on debug or on public surfaces).

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/dotnet/csharplang/issues/105#issuecomment-354166621, or mute the thread https://github.com/notifications/unsubscribe-auth/ACHky4CvDYnUq6GfyeiR4gSbH69v5YGZks5tEpxmgaJpZM4MBJNC .

davidvmckay commented 6 years ago

@Lexcess Precisely! Static analysis is the benefit unique to a standard, compiler-understood syntax.

Defensive code that throws exceptions can always be written with if statements if runtime checking is needed. Then, in some other issue (or library!), a code-generated / woven / IOC-injected mechanism could be developed to introduce this defensive runtime checking automatically based on the contracts.

I crave an experience like Eiffel provides, where code will not compile if contracts are not provably satisfied by DFA.

Actually, given a good DFA-based contract analyzer, post-conditions may not be a needed feature (e.g. where return), since Pre-conditions must be satisfied at callsites regardless of the calling method's post-conditions -- and we'd therefore have a narrower discussion about leveraging / extending either generic type constraint syntax or pattern matching syntax.

Whatever syntax is chosen, to be visually distinct from runtime behavior inside the method bodies, I think the new contracts mechanism should be expressed before the method body's opening curly or fat arrow.

Pzixel commented 6 years ago

Any progress?

marinasundstrom commented 5 years ago

Just a refresher... There is a prototype for the syntax part: https://github.com/Daniel-Svensson/roslyn/commit/8611f3ae873ce0e68fd8c97b30bbb81da53dc53f

I rediscovered it today.

In its current state, it simply rewrites code expressed in the syntax into calls to the Requires and Ensures methods in the System.Diagnostics.Contracts.Contract class.

For tooling, it would be better if there was some language support for with analyzers, and using flow analysis. The method contracts should run automatically when a method is being invoked or the contract changed similarly to Live Unit tests do. It should work well with tools also.

Serg046 commented 5 years ago

Yes, and it is important to say that Code Contracts project is no longer supported. Runtime Contracts was introduced instead. However there are some limitations and unsupported features. For example we cannot use Contract.Ensures. So 8611f3a does not work.

michaelvolz commented 5 years ago

Seeing that Champion: Simplified parameter null validation code is a thing now, how would this Issue fit into it overall? I was hoping for some kind of "Code Contracts" in C# itself for a long time now.

Is there still hope or are there valid reasons to never do it? (DotNetCore)

markusschaber commented 4 years ago

Just two thoughts of mine:

1) About invariants: Maybe invariants could be qualified like other members to be private, protected etc., applying them to all methods at the same or higher visibility than the invariant.

Thus, a private methold could ignore a public invariant, as it's always called in a context with higher visibility which has to ensure the invariants on it's boundaries anyways. This allows to refactor common code into private methods while still upholding the invariants to the outside.

2) About limits / avoiding primitive obsession: Some kinds of checks (like whether a parameter is in a fixed range) could be better replaced by using a specific type for that parameter.

So maybe we should start to think about and champion an easy system to define "simple" types, like a "real" typedef (not the "just-an-alias" one in C/C++), or something along the lines of https://andrewlock.net/generating-strongly-typed-ids-at-build-time-with-roslyn-using-strongly-typed-entity-ids-to-avoid-primitive-obsession-part-5/ - each time we can use a specific type instead of applying constraints to a basic type at all the codepathes it travels, we gain compile-time safety and need fewer runtime checks.

This can also help against argument reordering - e. G. when longitude and latitude are different types, or userId and groupId, one cannot apply them in the wrong order. (You usually cannot catch things like this by constraints on the ranges of the parameters...)

We have currently good experience with defining our own basic types, although it's lots of work to declare all the different structs with all the requried members for comparison etc.

tanveerbadar commented 4 years ago

No one mentioned spec#. That's the earliest point where this whole idea of method/type constraints, pre/post-conditions entered the .NET land.

Code contracts, AFAICT, was to bring spec# to CLR languages.

davidvmckay commented 4 years ago

So maybe we should start to think about and champion an easy system to define "simple" types, like a "real" typedef (not the "just-an-alias" one in C/C++), or something along the lines of https://andrewlock.net/generating-strongly-typed-ids-at-build-time-with-roslyn-using-strongly-typed-entity-ids-to-avoid-primitive-obsession-part-5/ - each time we can use a specific type instead of applying constraints to a basic type at all the codepaths it travels, we gain compile-time safety and need fewer runtime checks. 🔗 -- @markusschaber

Experience with type-guards such as in TypeScript const isDiceValue = (v: number): v is DiceValue => v > 0 && v < 7 && v % 1 === 0;, and some exposure to Refinement Types tells me that we could leverage c# / clr types to get some basic enforcement of most of the "Domain => Range" validations mentioned above, using constructors and casts:

public struct DiceValue : IEquatable<DiceValue>
{
    #region ...Equality Members In Here... #endregion
    readonly Byte _value;
    DiceValue() => throw new NotImplementedException("We need c# to permit us to forbid blitting of some structs so that only validated construction is possible.");
    DiceValue(Byte v) => _value = v > 0 && v < 7 ? v : throw new InvalidCastException($"'{v}' was not a valid {nameof(DiceValue)}");
    public static implicit operator Byte(DiceValue v) => v._value;
    public static implicit operator DiceValue(Byte v) => new DiceValue(v);
}

static class DiceProgram
{
    static Boolean GotSnakeEyes(DiceValue first, DiceValue second) => first == 1 && second == 1;
    static void Main()
    {
        var dice = new Byte[2];
        Random.NextBytes(dice); // roll
        Console.WriteLine($"Got snake eyes: {GotSnakeEyes(dice[0], dice[1])}");
    }
}

So, here, the contract is part of the DiceValue constructor, and GotSnakeEyes is designed to operate only on DiceValue instances. Once we have valid DiceValue instances, GotSnakeEyes will also function as contracted via its method signature.

However, the compiler cannot detect the big flaw in this program: Random.NextBytes(...) is not constrained to satisfying the domain of valid Bytes for constructing the range of valid DiceValues; The programmer needs to do something extra to check for bytes greater than 6 and re-rolll, or re-do the rolling logic in terms of doubles with a quantization mechanism like round or floor -- but they won't know this until a runtime test causes the InvalidCastException to throw.

Without static Data Flow Analysis that can detect the possible violation of the "precondition" embedded in DiceValue's ctor, the protection offered by this "weak refinement type" does not preclude shipping bugs.

Moreover, this does nothing to support invariants -- i.e., after DiceValue is constructed there is nothing to mitigate mutations within the constructed object that might push _value outside the valid range (except for readonly, which doesn't apply to more complex objects such as those that model state machines, e.g. database connections.)

Ultimately, the upshot benefit of using any of these schemes (refinement types, type guards, rich type vocabulary with validations in constructors, preconditions + invariants, debug assertions, defensive imperative code....) is to embed a clear, "readable" specification into the codebase, as close to the points of relevance as possible, with some form of automatic enforcement, ideally with violations detected and alerted to the developer before releasing to production, with low cost to adopt, that defines a restriction on the valid state space of the running system.

So, I would like to insist again that enhanced Static Analysis is the killer app of Contracts, and that invariants are as important as, if not more important than, preconditions.

@markusschaber , interesting suggestion about using visibility modifiers to help define when invariants should be enforced, because certainly there are common algorithms where we would like to temporarily violate an invariant, but from an outside view of a "transaction" with an object, the invariant is always satisfied while a transaction is not in progress. This definitely hints at a can of worms regarding concurrency, but what discussion about "transactions" doesn't.

markusschaber commented 4 years ago

So, here, the contract is part of the DiceValue constructor, and GotSnakeEyes is designed to operate only on DiceValue instances. Once we have valid DiceValue instances, GotSnakeEyes will also function as contracted via its method signature.

However, the compiler cannot detect the big flaw in this program: Random.NextBytes(...) is not constrained to satisfying the domain of valid Bytes for constructing the range of valid DiceValues; The programmer needs to do something extra to check for bytes greater than 6 and re-rolll, or re-do the rolling logic in terms of doubles with a quantization mechanism like round or floor -- but they won't know this until a runtime test causes the InvalidCastException to throw.

As far as I can see, the problem is the following line in your code:

public static implicit operator DiceValue(Byte v) => new DiceValue(v);

By defining this operator as implicit, you explicitly circumvent the type safety provided by the DiceValue type, and convert compile time error into a runtime error. If anything at all, this operator should be explicit. :-)

Moreover, this does nothing to support invariants -- i.e., after DiceValue is constructed there is nothing to mitigate mutations within the constructed object that might push _value outside the valid range (except for readonly, which doesn't apply to more complex objects such as those that model state machines, e.g. database connections.)

Yes. As "primitive obsession" says, it's about rather primitive types. Such as literals etc. Think of width/height/length, or longitude/latitude (same unit, different semantics). It's similar but orthogonal to what F# does with measures (they solve the degree Celsius/Farenheit, or inches/cm problem - same semantics, different unit).

Invariants help to define those types, but they also help to write other code with less bugs. On the other hand, refinement types solve some problems centrally (in the type definition), where invariants by itself will only solve them decentrally (each user of the "type" has to repeat the checks.

So I'd vote to have both, designed so they nicely play together, and with a readable but concise syntax. :-)

Ultimately, the upshot benefit of using any of these schemes (refinement types, type guards, rich type vocabulary with validations in constructors, preconditions + invariants, debug assertions, defensive imperative code....) is to embed a clear, "readable" specification into the codebase, as close to the points of relevance as possible, with some form of automatic enforcement, ideally with violations detected and alerted to the developer before releasing to production, with low cost to adopt, that defines a restriction on the valid state space of the running system.

Fully agreed.

Genbox commented 4 years ago

Invariants help to define those types, but they also help to write other code with less bugs. On the other hand, refinement types solve some problems centrally (in the type definition), where invariants by itself will only solve them decentrally (each user of the "type" has to repeat the checks.

Not to mention performance as a factor too. Too often I've dealt with overly defensive code that validates incoming arguments on each and every layer of the application to the point where the code is not only unreadable - but also slow.

Having a construct where you define an invariant that has to pass before usage means you can guarantee the input conforms to a certain range of values and formats before processing it. Having this invariant checked statically while developing, lowers the overhead needed for runtime checks.

The .NET Core framework code is a good example of where every single method - even deep into the internals - checks the input arguments for null, value range and format. There are no hints to the developer of what is acceptable input and what is not. A statically checked contract with an invariant would instantly tell the user that they are supplying a value that is going to crash at runtime. Imagine a situation where you can remove ALL checks from the .NET Core code because your application can only pass along valid inputs.

I used Code Contracts for a number of years to build a spec that had to be followed (Remember Pex and Moles?). Once it became defunc from non-maintenance, we started doing what @davidvmckay described in his post above for the innermost important parts of the application, while keeping the outermost API CLS compliant. However, it is not nearly as easy as contracts via metadata on interfaces and a static analyzer that can whitebox test your code.

julealgon commented 4 years ago

Is there something in particular blocking this from moving forward faster? Would restricting the initial version to pre and post conditions only allow us to come to a proposed design?

It pains me as a previous user of code contacts to see the project fading away without any sensible alternative. This issue was posted 3 years ago and we still haven't moved much.

CyrusNajmabadi commented 4 years ago

Is there something in particular blocking this from moving forward faster?

Yes. All efforts are focused on other features currently.

This issue was posted 3 years ago and we still haven't moved much.

Length of time since an issue was posted isn't really relevant. Things don't become more important the longer they've been around. Currently, this just isn't deemed as important as the rest of the stuff occupying the time and resources the LDM has. :)

ghost commented 2 years ago

Also for indexer?

public object this[string key]
    requires key is { Length: > 0 }
{
    get ensures value is not null
    {
        ...
        return null; //Error, ensures value is not null
    }
    set requires value is not null
    {
        ...
    }
}
a[""]; //Error, requires key is { Length: > 0 }
a["abc"] = null; //Error, requires value is not null

property and constructor?

class Class
{
    public string Str1 
        requires init
    {
        get;
        set requires value is { Length: > 0 };
    }
    public string Str2
    {
        get;
        set;
    }

    public Class()
    {
        ...
    }
    public Class(int i)
        ensures Str1 = "abc"
    {
        ...
    }
    public Class(int i1, int i2)
        requires init Str2
    {
        ...
    }
}

new Class(); //Error, Str1 requires init
new Class() { Str1 = "abc" }; //Okay

new Class(0); //Okay, ensures Str1 = "abc"

new Class(0, 0); //Error, Str1 and Str2 requires init
new Class(0, 0) { Str1 = "abc", Str2 = "edf" }; //Okay

interface and abstract class?

interface IInterface
    requires class
{
    ...
}
struct Struct : IInterface { ... } //Error, IInterface requires class type

interface IInterface2
    requires struct
{
    ...
}
class Class : IInterface2 { ... } //Error, IInterface2 requires struct type
abstract class Shape
    requires Rectangle, Circular
{
    ...
}
class Rectangle : Shape { ... } //Okay
class Circular : Shape { ... } //Okay

class Triangle : Shape { ... } //Error, Shape requires Rectangle, Circular

and custom error message?

int GetRandomNumber(int min, int max)
    requires min < max : "Error message"
{
    ...
}

GetRandomNumber(5, 0); //Error, "Error message"
Rekkonnect commented 2 years ago

I came up with a definitely meaningful use case, handling dates. In some scenarios, you may want to construct DateTime-related instances with literals, in test cases, guarding against special values, etc. Most dates have the clear constraints:

These can all utilize the method contracts feature, guarding against mistyped values, or unverified arguments. The only problem is that passing arguments around will hurt a lot, as they have to be contracted themselves. Using this feature will make sense in many scenarios that you don't care much about argument passing, like attribute constructors.

In attributes, most values are directly typed literals. This could also go hand-in-hand with defined enum value validation (see #4657).

Consider the following attribute:

[BreakMonthDays(month: 12, days: 1, 3, 5)]

Currently, the only viable way to guard the passed arguments before runtime is a custom analyzer, or using an analyzer's valid value range API. Both solutions are tedious and aren't as flexible.

CyrusNajmabadi commented 2 years ago

Day can never be over 31

This is incorrect. There have been months that have had 32 days.

Second must be between 0 and 59 (in the case of TimeSpan, -59 and 59)

This is also incorrect. Minutes can be 61 seconds long.

--

Dates and times are not only incredibly complex, they can often have political and historical rules placed on them. Having a contract system can be actively dangerous as your programming language may not allow you to represent things that are necessary.

Rekkonnect commented 2 years ago

There have been months that have had 32 days.

Minutes can be 61 seconds long.

I'm very curious about those edge cases.

your programming language may not allow you to represent things that are necessary.

Either way, there can be similar constraints; I'm not the one to design the APIs, i.e. on dates. Whoever wrote the date APIs will take this feature seriously into consideration before applying it, let alone the how.

Genbox commented 2 years ago

Having a contract system can be actively dangerous as your programming language may not allow you to represent things that are necessary.

I'm not sure I understand what you are saying here. Are you saying that contracts are dangerous because the compiler enforces constraints defined by the developer?

Contracts are no different than ordinary validation code, except they are checked at compile-time instead of at run-time, and that limits them to check proofs (constraints) and provide guarantees rather than validating data. If the developer defines an invalid contract, they will get invalid results. That does not make contracts dangerous.

Clockwork-Muse commented 2 years ago

There have been months that have had 32 days.

Not sure what exactly this is referring to, at least in the context of the ideal proleptic Gregorian calendar that DateTime implements. Historically things like this may be true (The switch between Julian and Gregorian calendars took centuries and produced some odd gaps and jumps), and other calendars can have wildly different counts of days or months.

Minutes can be 61 seconds long.

Leap seconds.

Whoever wrote the date APIs will take this feature seriously into consideration before applying it, let alone the how.

It's not easily possible to express date/time validation with contracts, given the complexity, and most cases are going to need runtime validation due to coming from runtime data anyways. It's maybe possible if the contract system allows calling arbitrary validation methods, or the language allows for compile-time evaluation of functions (const in C++/Rust).

CyrusNajmabadi commented 2 years ago

I'm very curious about those edge cases.

There have been months with 32 days in them :)

Leap seconds allow for minutes with 61 seconds in them :)

CyrusNajmabadi commented 2 years ago

I'm not sure I understand what you are saying here. Are you saying that contracts are dangerous because the compiler enforces constraints defined by the developer?

In a very real sense yes. Dates and times in particular you don't necessarily want to represent with any static constraints as those constraints may literally not hold due to external factors. They can be alluring, until they butt their head with the dynamic realities of real scenarios.

CyrusNajmabadi commented 2 years ago

Contracts are no different than ordinary validation code, except they are checked at compile-time instead of at run-time, and that limits them to check proofs (constraints) and provide guarantees rather than validating data. If the developer defines an invalid contract, they will get invalid results. That does not make contracts dangerous.

It does as an update may literally cause code to no longer compile, breaking everything (as opposed to allowing it to still compile and run, but perhaps only representing a problem with a runtime issue that ends up not actually being a problem in practice).

CyrusNajmabadi commented 2 years ago

Not sure what exactly this is referring to

The julian calendar had at least one example of this based on political need to bridge different calendars. This is my general point here. Dates and times are a manmade construct and have no intrinsic truth or rules around them. They can and do change routinely based on the needs of societies.

CyrusNajmabadi commented 2 years ago

or the language allows for compile-time evaluation of functions (const in C++/Rust).

And this is likely not desirable anyways as it means that you bake in what is essentially a dynamic system statically at compile time. Most systems understand and want to heavily support the case that this data can and should be changing, without the program ever needing recompile. Imagine, for example, that you had to recompile and reship your entire app just because a TZ changed for one country (or something changes about DST etc.). You don't want htat. You instead just want the app to remain the same, with just the DB containing this information being updated (often at a platform/os level).

Genbox commented 2 years ago

constraints may literally not hold due to external factors

I agree. Contracts are for well-defined specifications with proofs and guarantees, not dynamic/evolving issues. However, that does not make them dangerous. If the developer defines an incorrect contract, it will be an incorrect application.

The limitation we face here is that C# has no way of defining contracts, as such we can't have correct applications either.

CyrusNajmabadi commented 2 years ago

The limitation we face here is that C# has no way of defining contracts,

This is not true. It's just that the contracts we support are in the type system.

as such we can't have correct applications either.

You can. You just can't have automated checking of other sorts of contracts using the stock compiler. You could, for example, define your own contracts with some other system and have that checked. That would allow you the benefits, while not adding hte complexity of an entire contracts sysstem into the language/compiler itself. Given how complex (And costly) these systems are themselves in all the state of the art systems out there, that would be strong preference.

Genbox commented 2 years ago

Dates and times are a manmade construct and have no intrinsic truth or rules around them.

Would you propose that we never parse date/time data then? Mind you that a contract (even an invalid one) does not change the code; it only formalizes it. The CLR type system is a formalization of data formats, but they only provide basic guarantees.

int age;  // Is this always positive? Are you sure? Can you prove it?
uint age; // Here we can be sure it is positive. Why? Because the compiler enforces it.

Another example:

public int GetLevel(int userLevel)
{
    int systemLevel = 100;

    //Must always be in the range [0..1000], but we can't be sure it always is. There is no guarantee.
    int level = systemLevel + userLevel; 
    return level;
}

With an ensurance-contract on the return value, we could tell the caller of GetLevel that they can expect it to be >= 0 and <= 1000. And it will be - because the application does not compile if the code allows it to be anything else (if we had contracts).

Imagine, for example, that you had to recompile and reship your entire app just because a TZ changed for one country (or something changes about DST etc.). You don't want that

Of course, you wouldn't create a specification that changes all the time. But when the specification changes, you would very much want to recompile it and check that the application logic is still correct.

Genbox commented 2 years ago

This is not true. It's just that the contracts we support are in the type system.

A type system is to contracts, what a skateboard is to a car. Contracts go way beyond simple value checks.

CyrusNajmabadi commented 2 years ago

Would you propose that we never parse date/time data then?

I would propose that date/time parsing not be part of a contracts system.

CyrusNajmabadi commented 2 years ago

But when the specification changes, you would very much want to recompile it and check that the application logic is still correct.

I absolutely would not. Any date/time change, and you have to recompile all apps on a system.

CyrusNajmabadi commented 2 years ago

Contracts go way beyond simple value checks.

I didn't say otherwise. I was simply stating that it was not true that there are no contracts or way of defining contracts. There are, just that it may be unpalatable for you to use.

But, as i said, the palatable forms of contracts you want are those that i very much want as orthogonal to the main language/compiler impl.

Genbox commented 2 years ago

There are, just that it may be unpalatable for you to use.

Can you provide an example?

CyrusNajmabadi commented 2 years ago

A type system is to contracts, what a skateboard is to a car.

Yes. I agree with that. Which is why i would not want such a thing. Effectively entirely excessive complexity and cost compared to what is needed to solve the scenarios a skateboard excels at. Literally orders of magnitude more. That's not a virtue in this space.

CyrusNajmabadi commented 2 years ago

Can you provide an example?

Sure. You can just encode your particular constraints at any point in time by encoding them into a nominal type that hte type system then requires as inputs/outputs. I would not recommend this.

You can likely construct an entire lambda-calculus way of defining an entire system in our type system as well, which you'd then have the compiler validate. Again, not recommended at all.

My preference is reasonable nominal typing, with flexible enough types for the majority case, backed with runtime checking (and/or other tooling) if you want more rigid checks on things. That other tooling can also be static, running at compile time if you prefer that.

theunrepentantgeek commented 2 years ago

This is also incorrect. Minutes can be 61 seconds long.

It looks as though we may soon have a minute that's 59 seconds long too ...

As things currently stand, a negative leap second may be required within the next ten years.

Quote from www.timeanddate.com

Dates and times are not only incredibly complex, they can often have political and historical rules placed on them. Having a contract system can be actively dangerous as your programming language may not allow you to represent things that are necessary.

Yup. It used to be commonplace to encounter computer systems that couldn't handle February 29th properly, if at all. This is a far from trivial issue when an institution requires you identify yourself by name and birthdate, but their computer rejects your birthdate as invalid. #askmehowiknow

Genbox commented 2 years ago

Effectively entirely excessive complexity and cost compared to what is needed to solve the scenarios a skateboard excels at.

I had this exact same conversation 15 years ago trying to explain to C developers why C#, even with all its complexity, was a better choice. Imagine if Mads Torgersen had just said "the whole concept of managed lanagues is just a waste of time". Where would we be today?

I do not disagree that it is "expensive" on multiple facets. However, just like the value of managed languages only became apparent after it was built, so will the value of contracts and code correctness only be apparent after it has been built.

We need to teach developers that correctness is a factor they need to optimize for, and they can only do that if they can declare a specification. E.g. when a > 0, b will be [0..100] which is somehow checked before run-time.

C#/Roslyn seems to have everything needed to do it the easy way. The Z3 solver can do the rest.

Lexcess commented 2 years ago

I feel this conversation is getting a little into the weeds. Even if contracts is a terrible use case for datetime (and let's face it most things are terrible for datetime because of all the subtle complexities), that doesn't prove contracts do not have any value.

Genbox commented 2 years ago

You can just encode your particular constraints at any point in time by encoding them into a nominal type that hte type system then requires as inputs/outputs.

This only provides value checks. You can't declare a contract which the values are checked against. That is; if I provide you with a DLL I made with the constrained types, how will you know my constraints? You don't... That's why nullability analysis was made, and now the time has come to do the same for any arbitrary constraint.

You can likely construct an entire lambda-calculus way of defining an entire system in our type system as well, which you'd then have the compiler validate.

This still only provides value checks. It is closer, but still very far away. You simply can't replicate the functionality of an SMT solver with a bunch of lamdas and specialized types.

My preference is reasonable nominal typing, with flexible enough types for the majority case, backed with runtime checking (and/or other tooling) if you want more rigid checks on things.

I suspected this is what you were getting at. What you are really saying is that the majority of constraints developers have can be satisfied with a combination of specialized types and validation methods. You are not wrong, but it is the same as saying C can do anything C# can, you just have to build C# first to do it. Contracts are a formal proof over the whole application that simply won't compile unless your application compiles to Q.E.D.

You can't define mathematical proofs with arithmetic. Just like you can't verify the correctness of an application with types and validation methods.

Developers fail miserably when they have to be the guardian of logic. We need the developer to define a spec (which he cannot today) and we need something to check it (which we don't have today). Code Contracts was amazing at this, and it seems a lot more feasible to day due to all the extensibility points we have in the .NET platform today.

Rekkonnect commented 2 years ago

I feel this conversation is getting a little into the weeds. Even if contracts is a terrible use case for datetime (and let's face it most things are terrible for datetime because of all the subtle complexities), that doesn't prove contracts do not have any value.

You can only blame me for providing a controversial example.

To recuperate, I may as well propose, or at least add on an existing example, contracts regarding certain values for well-defined mathematical functions/operations, like Math.Log, division, etc. These contracts could also exist on the operator level, and getting a compiler error about division by zero would then be possible, disregarding the need for a runtime check in some scenarios. Or, limiting the max score in some context in a game, as a preventive measure against cheating.

And yes, these contracts can work as an alternative/replacement for !!, around which controversy exists.

Clockwork-Muse commented 2 years ago

I absolutely would not. Any date/time change, and you have to recompile all apps on a system.

I'm sorry, I don't find this convincing; you'd have to recompile for runtime validation changes anyways.

Let's posit that (incredibly unlikely) the ISO organization decides to change their definition of the calendar, and that DateTime is to be updated to match, as the validation rules are part of internal code. If your code is creating a literal that will no longer be valid, you'd have to recompile your app to correct whatever error that happens due to the (updated, but) existing runtime checks. The only thing a contract does in this sort of case is surface the error at compile time, which is normally what you want them for. You still require runtime checks at I/O boundaries, which shouldn't be surprising - you're still getting untrusted data and have to be able to reject non-conforming values. But you shouldn't need a recompile to hit the runtime checks...

It also feels counter to current dev-ops trends which do recompile entire applications relatively regularly. Yes, this breaks the build, but the alternative is releasing a build that errors at runtime.


Personally, I want contracts to be a way to surface my runtime checks at compile time, potentially to the extent that flow analysis can prove the compiler or the JIT can remove them.
It's almost certain you'd need both;