contracts.ruby icon indicating copy to clipboard operation
contracts.ruby copied to clipboard

Inherited contracts

Open nixpulvis opened this issue 9 years ago • 16 comments

This is a pretty common use case (for me at least). I define a base class:

class Base
  # ...
  Contract String => String
  def foo(arg)
    # ...
  end
  # ...
end

Next I write a bunch of subclasses from Base. I might write a bunch of new methods on these subclasses, but sometimes I also need to overwrite the definition of a function.

class A < Base
  # ...
  def foo(arg)
    # New stuff.
  end
  # ...
end

By default it would be ideal for the contract of Base#foo to be given to A#foo, allowing a new contract to overwrite the inherited one if needed.

nixpulvis avatar Mar 11 '15 18:03 nixpulvis

Hmm, I don't agree with this. One reason I like contracts, the contract is right above the method definition. So I don't have to go hunting to understand what a method is doing. If you see a method with no contract on it, and then it throws a contract error, that could get confusing.

I can see this being an issue when you are overriding a bunch of one line methods. But I would rather have that be more verbose for the sake of clarity.

egonSchiele avatar Apr 01 '15 23:04 egonSchiele

Maintaining interfaces is the most important thing this library provides. If I need to duplicate an contract then I need to maintain n contracts for the same interface, where n is the number of implementors of the interface.

The blaming feature should be able to handle pointing contract failure to the correct location.

nixpulvis avatar Apr 02 '15 00:04 nixpulvis

I have the question, why the contracts I wrote on the "interface" cannot be inherited?

So far, I have to use proxy pattern to solve this problem.

class Base
  # ...
  Contract String => String
  def foo(arg)
    # ...
  end

  def proxy(client)
    @client = client
    self
  end
  # ...
end

usage:

obj = Base.new.proxy(A.new).foo(...)

stevesun21 avatar Apr 29 '15 03:04 stevesun21

There is one solution: stop using inheritance for code reuse - it only increases coupling and calls more problems. Use composition instead.

waterlink avatar Sep 04 '15 21:09 waterlink

There was no so much activity here, and I am reluctant to closing this because @egonSchiele thinks that it is important to spell out contracts explicitly for each method.

WDYT?

waterlink avatar Sep 04 '15 21:09 waterlink

While I agree with you in general there are two reasons I see this as important nonetheless.

  1. People do not all agree that they should not use inheritance, and given that Ruby explicitly supports it people will use it.
  2. Enforcing contracts on subclasses actually makes inheritance a better option in some cases. It adds structure to interfaces developed for reuse.

nixpulvis avatar Sep 04 '15 21:09 nixpulvis

Maybe method-overrides could be intercepted and an exception be thrown if the new version lacks a conforming contract annotation? For inspiration, do take a look at how the Eiffel language deals with contracts and inheritance.

sebnozzi avatar Sep 04 '15 21:09 sebnozzi

That seems like more work than just assuming that all methods denied from #foo adhere to it's contract unless explicitly stated.

nixpulvis avatar Sep 04 '15 21:09 nixpulvis

@nixpulvis You can assume that, but you won't see it by looking at the code (without knowing the superclass). But then again, I'm mainly a Scala developer, where you have to explicitly state that a method is being overridden in a subclass (which I dearly miss in Ruby - but where I can understand why this is not "supported"). So I am biased towards more boilerplate and explicitly stating things...

sebnozzi avatar Sep 04 '15 21:09 sebnozzi

Actually I like @sebnozzi 's idea about validating inherited methods contracts. WDYT @egonSchiele ?

waterlink avatar Sep 04 '15 22:09 waterlink

Well, it will be hard to do if blocks/lambdas are present in contract.. Unless we force not to use lambda/procs when there is inheritance - instead just recommend usage of class with self.valid? method.

waterlink avatar Sep 04 '15 22:09 waterlink

@sebnozzi Generally speaking I agree with you, but I think that contracts should aim to be inline with how Ruby works, maybe other disagree, but where Ruby allows method inheritance we should support contract inheritance. Most of the time when you are overriding a method you are not changing the contract.

nixpulvis avatar Sep 04 '15 22:09 nixpulvis

Ok. What happens to pattern matching in that case?:

class A
  include Contracts::Core

  Contract X => Y
  def something(x)
    x.y
  end

  Contract Z => C::Maybe[Y]
  def something(z)
    z.y
  end
end

class B < A
  def something(a)
    # ... whatever here ...
  end
end

puts B.new.functype(:something)
# => something :: ??????? => ???????

waterlink avatar Sep 04 '15 22:09 waterlink

I have a suggestion: inheriting contracts is a means to an end. The problem is that it is defeated by this suggestion: favour composition over inheritance.

But with composition, how can I contract the object that must be composed in? In other words, how can we use contracts to specify (for the receiver) the interface of a composed object?

sheldonh avatar Nov 23 '16 06:11 sheldonh

Sheldon,

There is a RespondTo contract for that, see the documentation. You can also define several RespondTo contracts as roles. For example:

DuckRole = RespondTo[:quack, :swim]

Contract DuckRole
def handle_duck(duck)
...
end

On Wed, 23 Nov 2016 at 7:52 AM, Sheldon Hearn [email protected] wrote:

I have a suggestion: inheriting contracts is a means to an end. The problem is that it is defeated by this suggestion: favour composition over inheritance.

But with composition, how can I contract the object that must be composed in? In other words, how can we use contracts to specify (for the receiver) the interface of a composed object?

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/egonSchiele/contracts.ruby/issues/104#issuecomment-262444740, or mute the thread https://github.com/notifications/unsubscribe-auth/AHjlev2TwmuHlBxc_bz1nwDr48xVX7NGks5rA-KwgaJpZM4DtLKV .

alex-fedorov avatar Nov 23 '16 12:11 alex-fedorov

I know this is old, but I ran into this same problem. I'm trying to define contracts for a service and I don't want to repeat the contract definitions for each instance. Since ruby doesn't have its own concept of interfaces, I tried to fake it with an abstract base class with contracts (contrived pseudocode, haven't run):

class AbstractFooService
  Contract C::Int, C::String => C::String
  def foo(a, b)
      raise NotImplementedError
   end
end

class MyFooService < AbstractFooService
  def initialize(str)
      @str = str
  end

  def foo(a, b)
    "#{@str}:#{b}" * a
  end
end

One solution I've found is to inherit from simple delegator and treat it more like a validating proxy than an abstract base class. This also means I need a factory to wrap the instances of the implementation classes. Also, I don't think I can do invariant checks this way.

class FooServiceValidator < SimpleDelegator
  Contract C::Int, C::String => C::String
  def foo(a, b)
      super
  end  
end 

There's no reason it has to inherit in this case. In fact, I think I'd prefer to define the contracts in a module or some other way that would apply the contract checking logic orthogonally to the implementation.

IFooService = Contracts::Interface.build do |i|
  i.add_contract(:foo, C::Int, C::String => C::String)
  #i.add_invariant, etc
done

class MyFooService
  ... # As above
end

IFooService.implemented_by!(MyFooService)
# `implemented_by` without the '!' would return a new class.

I've tried to figure out how to do this myself, but I'm getting tripped up.

AndrewO avatar Sep 11 '18 12:09 AndrewO