include/prepend satisfiable module dependencies are not satisfied
module P def m; puts "P"; super; end end module Q def m; puts "Q"; super; end include P end module R def m; puts "R"; super; end prepend Q end module S def m; puts "S"; super; end include R end class A def m; puts "A"; super; end prepend P include S end A.new.m #=> P, R, A, S, Q
This code has five module dependencies, but only two are satisfied.
P, which is not satisfied:
Q, which is not satisfied:
R, which is not satisfied:
P, which is satisfied:
S, which is satisfied:
Note that all the dependencies can be satisfied at all:
A.new.m #=> Q, P, A, S, R
Yusuke Endoh email@example.com
Updated by mame (Yusuke Endoh) over 7 years ago
matz (Yukihiro Matsumoto) wrote:
I believe the behavior is undefined (or implementation defined) when module dependency has contradiction.
And preferably error should be raised when contradiction detected.
Thank you, I agree with the policy.
However, is there any 'contradiction' in this case? No pair of these dependencies conflicts. There are no cycles.
Actually, they are satisfiable. A solution can be found by using the topological sorting.
Yusuke Endoh firstname.lastname@example.org
Updated by marcandre (Marc-Andre Lafortune) over 7 years ago
As I stated before (#1586), I feel that the solution is easy:
A.ancestors = [*A.prepended_modules.flat_map(&:ancestors), A, *A.included_modules.flat_map(&:ancestors), *A.superclass.ancestors]
In the given example, this would be:
P, A, S, Q, P, R, Object, Kernel, BasicObject
It makes absolutely no sense to me that
R could come before
A and believe it is clearly a major problem.
R is never prepended, nor included in a prepended module!
Matz: how would you explain that
R can be called before
A in that example?
Updated by jeremyevans0 (Jeremy Evans) about 1 year ago
This is still a bug in the master branch. I think the main problem in this example is that when calling
Module#include, you can get a module inserted before the receiver in the lookup chain instead of behind the receiver.
mame correctly pointed out that using a topological sort you can satisfy all dependencies in the example. However, you can only do so by inserting a module before the receiver in the lookup chain, which I don't think should be allowed.
In the example, at the time
A.include S is called:
A.ancestors # => [P, A, Object, Kernel, BasicObject] S.ancestors # => [S, Q, P, R]
In my opinion,
Module#include should have these properties:
(1) It should not add a module to the receiver's lookup chain if it already exists anywhere in the receiver's lookup chain
(2) It should not move the position of any module in the receiver's lookup chain, only add modules to the receiver's lookup chain
(3) It should not insert any modules before the receiver in the receiver's lookup chain
(4) It should insert all modules as a group, not some modules in the receiver's lookup chain at a different place than other modules
Applied to this example, assuming the above properties, after
A.include S, we should have the following
A.ancestors # => [P, A, S, Q, R, Object, Kernel, BasicObject]
In this example,
P in the included module lookup chain is ignored as it is already in the lookup chain for
A. The remaining modules in the included module lookup chain are inserted as a group after
Attached is a patch that implements the above behavior, implementing property (3). The only change is that before inserting a module into the lookup chain, we check to make sure we are inserting it after the origin class.
make check passes with this patch. I'm not sure this implements property (4). Still, I believe this is an improvement and should be committed.