locked
Contracts for Generic Parameters in Abstract Methods RRS feed

  • Question

  • Hi,

    What are the recommendations for declaring contracts with generic parameters in abstract methods when the purpose of a method is to validate contracts who's semantics are meant to be defined by the implementer?

    Can I safely ignore the warnings and still get all of that DbC goodness?

    For example:

    abstract class Super<T>

    {

      public abstract void Validate(T t);

    }

     

    class Sub : Super<Uri>

    {

      public override void Validate(Uri t)

      {

        Contract.Requires(t.IsAbsoluteUri);  // issues a warning

      }

    }


    I already understand from the documentation that abstract members and interfaces can have contracts defined for them in special "contract" classes, but that's not what I'm asking.

    The problem here, if it's not already obvious, is that the generic type parameter T is not bound in the superclass and therefore the argument 't' cannot have any meaningful contracts applied to it in the abstract method Super<T>.Validate.  Moreover, the purpose of the Validate method is so that implementers can define the semantics of the contract when T is bound.  It seems that I should be able to safely add pre-conditions on the method parameter 't' in Sub.Validate given that its purpose is to validate that 't' holds to the public contract of Sub, which defines T as System.Uri.

    I see that in this particular scenario, if a caller were to have a reference to a Super<Uri> and called Super<Uri>.Validate, the caller may assume that it's acceptable to pass in a relative URI and that would be a bad assumption if the implementer is actually Sub.  However, this assumes that the public contract for Super<T> restricts all implementations from providing stronger contracts for the parameter 't', but that would be a bad assumption too given that the purpose of the Validate method is to validate that T holds to the contract defined by the implementer that binds T to a concrete type.  In this case, the public contract for the Super<T>.Validate method is actually that derived types will bind T and then provide the contract for the Validate method.

    This may simply be an architectural issue, but it doesn't seem that way to me.  Definitely seems like a corner case though.  I have a concrete example if anybody's interested.

    Your thoughts appreciated.

    Thanks,
    Dave
    http://davesexton.com/blog
    Monday, October 5, 2009 6:28 AM

Answers

  • Well, the problem is in the method DelegateValidate<T>(Super<T> s, T data). If you look at this method in isolation, then there is really no information as to what the requirement at the Validate call is. I think what you are looking for is maybe a slightly different handling where instead of strengthening the contracts on implementations of Super<T>, you have Super<T> define an abstract boolean Validate method as such:

    abstract class Super<T> {
        [Pure]
        public abstract bool IsValid(T data);

     ...
    }

    The idea is that every override can now implement IsValid in whatever form it needs, e.g., The Uri instantiation can implement the non-relative path requirement in your example.

    You can then use the Validate(T data) method in other contracts, e.g., I don't know whether this makes sense here, but consider another method of Super<T>

       public void DoWorkOnData(T data) {
          Contract.Requires( this.IsValid(data );

          ...
       }

    Now this makes sense. You still have some abstraction over what Valid means, but it is now a pure boolean property. The contract itself is still the same in each override.


    Cheers, -MaF (Manuel Fahndrich)
    • Marked as answer by Dave Sexton Friday, October 23, 2009 5:22 AM
    Friday, October 23, 2009 3:54 AM
  • Dave, thanks for bringing this up. In this scenario, using a contract is not recommended. Think of the API Super<T>Validate(T t). A client having such an object in its hand with only that Super<T> type must understand what is required of him. That's why we don't let implementations/overrides add requires. So there is no way we can blame the caller, if he/she doesn't satisfy your t.IsAbsolteUri, because the caller may not know about it at all.

    I understand that there are situations where you will have a Sub object in your hand and would like to make it a requirement there. But there is no guarantee that we can't pass this to a generic context where the requires is not understood as follows:

      void MethodUnderstandingSub(Sub s, Uri uri) {
    
         DelegateValidate(s, uri);
      }
    
       ...
    
      static void DelegateValidate<T>(Super<T> s, T data) {
        s.Validate(data); // this caller has no idea about Requires.
      }

    Cheers, -MaF (Manuel Fahndrich)
    • Proposed as answer by Manuel Fahndrich Wednesday, October 7, 2009 5:56 PM
    • Marked as answer by Dave Sexton Thursday, October 8, 2009 1:29 PM
    Wednesday, October 7, 2009 5:51 PM

All replies

  • Dave, thanks for bringing this up. In this scenario, using a contract is not recommended. Think of the API Super<T>Validate(T t). A client having such an object in its hand with only that Super<T> type must understand what is required of him. That's why we don't let implementations/overrides add requires. So there is no way we can blame the caller, if he/she doesn't satisfy your t.IsAbsolteUri, because the caller may not know about it at all.

    I understand that there are situations where you will have a Sub object in your hand and would like to make it a requirement there. But there is no guarantee that we can't pass this to a generic context where the requires is not understood as follows:

      void MethodUnderstandingSub(Sub s, Uri uri) {
    
         DelegateValidate(s, uri);
      }
    
       ...
    
      static void DelegateValidate<T>(Super<T> s, T data) {
        s.Validate(data); // this caller has no idea about Requires.
      }

    Cheers, -MaF (Manuel Fahndrich)
    • Proposed as answer by Manuel Fahndrich Wednesday, October 7, 2009 5:56 PM
    • Marked as answer by Dave Sexton Thursday, October 8, 2009 1:29 PM
    Wednesday, October 7, 2009 5:51 PM
  • Hi Manuel,

    Thanks for your reply.  I think the problem here is that I'm trying to accomplish two things at once, purposely breaking behavioral subtyping.

    In a more concrete example, imagine that Super<T> is actually a class that is used by a UI framework, such as WPF, to validate user input of a specified type.  And let's say that in a particular GUI we want to use Sub<Uri> declaratively to validate that a correct URI is being entered by the user.  In this scenario, whether I use contracts or throw exceptions manually it's the same result.  So using contracts here merely gives me a succinct (and eventually standard - hopefully) way of expressing the requirements of Sub<Uri>.Validate.  (Arguably, I shouldn't be using exceptions for non-exceptional conditions if they can be avoided, but I can easily imagine a scenario where they can't be avoided.)  In this scenario I'm not going to make use of static analysis.  More importantly, however, I'm never going to be able to use Super<Uri> on its own because Super<T> is an abstract base class - WPF won't allow us to use it declaratively because it cannot be instantiated directly.

    Another possible scenario is that I want to use Sub<Uri> from code directly.  Without using contracts I won't be able to take advantage of static analysis, but surely some static analysis is better than none.  I understand that allowing contracts in the override breaks behavioral subtyping in cases where the Super<T> is meant to be used directly, but what if it's not?  What if consumers of my library understand that Super<T> defines no contracts for T and that derived types will provide stronger contracts?  Essentially, this is the contract of Super<T>.

    You might then wonder why even use Super<T> in the first place?  But the abstraction of the Validate method into the Super<T> class is still useful, even if at the call site of Super<T>.Validate no guarantees can be made of its arguments; in your particular usage scenario I would also assume that static analysis (or perhaps a tool such as Pex) could still use the contracts (assuming that the analyzer understands System.Uri at the required depth - maybe I should've used bool as an example instead).  My additions in bold:

      void MethodSupplyingUri(Sub s) {

        MethodUnderstandingSub(s, new Uri("/test", UriKind.Relative);
        // analysis error; this code path causes a relative URI to be passed to the Validate method of Sub, which will always fail at runtime
      }

      void MethodUnderstandingSub(Sub s, Uri uri) {
        Contract.Requires(uri != null);  // making Sub's contract even stronger, just for fun I guess

        DelegateValidate(s, uri);
      }

       ...

      static void DelegateValidate<T>(Super<T> s, T data) {
        if (s.ShouldValidate())   // abstraction - giving purpose to calling into Super<T>.Validate without knowledge of the concrete type
           s.Validate(data); // this caller has no idea about Requires.
      }

    I understand if the analysis tools are not sophisticated enough yet to detect an error scenario such as this, but all of the information would be readily available at compile time since I'm adding Contract.Requires to the Sub.Validate method.  This seems like a deterministic error.

    Thanks,
    Dave
    http://davesexton.com/blog
    Wednesday, October 7, 2009 10:12 PM
  • Well, the problem is in the method DelegateValidate<T>(Super<T> s, T data). If you look at this method in isolation, then there is really no information as to what the requirement at the Validate call is. I think what you are looking for is maybe a slightly different handling where instead of strengthening the contracts on implementations of Super<T>, you have Super<T> define an abstract boolean Validate method as such:

    abstract class Super<T> {
        [Pure]
        public abstract bool IsValid(T data);

     ...
    }

    The idea is that every override can now implement IsValid in whatever form it needs, e.g., The Uri instantiation can implement the non-relative path requirement in your example.

    You can then use the Validate(T data) method in other contracts, e.g., I don't know whether this makes sense here, but consider another method of Super<T>

       public void DoWorkOnData(T data) {
          Contract.Requires( this.IsValid(data );

          ...
       }

    Now this makes sense. You still have some abstraction over what Valid means, but it is now a pure boolean property. The contract itself is still the same in each override.


    Cheers, -MaF (Manuel Fahndrich)
    • Marked as answer by Dave Sexton Friday, October 23, 2009 5:22 AM
    Friday, October 23, 2009 3:54 AM
  • Hi Maf,

    Ooohh, you've certainly given me something to think about :)  When I find the time I'll try applying it to my real-world scenerio and get back to you if I find any inconsistencies.

    Thanks, 
    Dave
    http://davesexton.com/blog
    Friday, October 23, 2009 5:24 AM