contracts.ruby
contracts.ruby copied to clipboard
Inherited contracts
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.
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.
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.
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(...)
There is one solution: stop using inheritance for code reuse - it only increases coupling and calls more problems. Use composition instead.
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?
While I agree with you in general there are two reasons I see this as important nonetheless.
- People do not all agree that they should not use inheritance, and given that Ruby explicitly supports it people will use it.
- Enforcing contracts on subclasses actually makes inheritance a better option in some cases. It adds structure to interfaces developed for reuse.
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.
That seems like more work than just assuming that all methods denied from #foo
adhere to it's contract unless explicitly stated.
@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...
Actually I like @sebnozzi 's idea about validating inherited methods contracts. WDYT @egonSchiele ?
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.
@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.
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 :: ??????? => ???????
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?
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 .
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.