> > On Tue, 28 Mar 2000, Andrew Hunt wrote: > > > > > Well, that brings up an interesting question. What do > > > you think about implementing Design By Contract in a > > > non-staticaly typed language such as Ruby? I've toyed around > > > with several implementations of DBC in Ruby, and once Dave > > > and I get a bit more of the Ruby book finished I'll look at > > > it again. > > > > > > But would that be a usefull feature to have in Ruby? > > Having looked at this a little, I can see it would be a good idea. On Thu, 30 Mar 2000 h.fulton / att.net wrote: > I am trying to implement the best system I can > *without* changing the language spec... Me too! I've thought about usefulness of invariants and method conditions and tried to find imagine why they are more usefull than normal assertions. I don't know. I guess since they are named a little bit differently they could be used as a part of the documentation. And there I can see their usefulness easily. So after the the discussion on the list started I got more and more excited. Then I decided to give it a try and try to create Design By Contract -module. Here you are, this is the first version: module DBC class DBCError < StandardError ; end class DBCPreError < DBCError ; end class DBCPostError < DBCError ; end class DBCInvariantError < DBCError ; end def pre raise DBCPreError unless yield end def post(klass, method, invariant) raise DBCPostError unless yield if defined? invariant and klass.public_instance_methods.include? method then raise DBCInvariantError unless invariant() end end end And then testing code: include DBC class Foo @a def invariant @a != 3 end def initialize @a = 0 end def bar(b) pre { b != 1 } @a += b print "@a = ", @a, "\n" post( Foo, "bar", self) { @a != 2 } end end for i in (0..4) do print "\nTesting foo.bar(#{i})\n" foo = Foo.new begin foo.bar(i) rescue DBCPreError print "DBCPreError happened as expected\n" rescue DBCPostError print "DBCPostError happened as expected\n" rescue DBCInvariantError print "DBCInvariantError happened as expected\n" rescue Exception print "Some unexpected error happened\n" end end Produces: Testing foo.bar(0) @a = 0 Testing foo.bar(1) DBCPreError happened as expected Testing foo.bar(2) @a = 2 DBCPostError happened as expected Testing foo.bar(3) @a = 3 DBCInvariantError happened as expected Testing foo.bar(4) @a = 4 > Of course, I am running into problems. But it > remains to be seen whether these problems are > insoluble, or just require more digging in the > Ruby reference. I was running into many many problems before this seemed to work or do something in "right direction" :). Now I list few: - how it's possible to access something inside the class from other class? Current version defines invariant, but that method is public so everybody can call it. Making it private hides it from post too. I tried to use instance variables, class variables etc., but this was the first one that I managed to work. Just to give you some idea, at some point I had: Invariant = proc { @a < 3 }; I thought attr, especially attr_reader could be for some help. I rejected possibility to mix-in DBC, because then the same code would be in exactly same form in every DBC-using class. What a waste of CPU at compiling, and memory. - how to check where we're coming? I tried to use caller, but it's returning values are basically only for printing. It returns some strings in weird format like "file.rb:25". I'd like to have full (read) access to stack. See what was the method where we came, the associated object, arguments etc. Basically I'd like to change my code to say post-assertion without need to "tediously" type calling instance, it's class and method. (Actually now I noticed that we probably can drop class away, since we should be able to say something like self.type at post-check.) How to get calling method automatically? With stack access this should be no problem. - how to recompile some code efficiently? Here I would add some code for pre-execute, and post-execute hooks. Since there is none, I considered to take method declaration and modify it and then substitute the original code with modifications. At this point I considered also to wrap function automagically: class Foo def bar end end def foo_invariant end Foo.bar = DBC.check( proc { pretest }, proc {posttest}, foo_invariant ) This check routine replaces so that Foo's bar becomes def Foo.bar DBC.pre Foo # original code DBC.post Foo end I just couldn't produce it. Maybe somebody can help here (too!). - btw it's tedious (to remember) to type semicolon to class DBCError < StandardError ; end - many more It's just way too late for last efficient working day before hacking weekend :). the future is under construction