eiffelroom

articleEiffel for Rubyists

francesco.ferreri's picture

After several months programming with Ruby scripting language, and related libraries, I got quite used to the intensive use of iterators and blocks (closures) that give Ruby programs a particular coding flavour. Switching back to Eiffel, I wondered how to reproduce this coding style in Eiffel programs: apart from discussing about this being an healthy practice or not, let's see some funny examples !

Simple iterators

One of the most frequently used iterators in Ruby is the each method in container classes like Array:

languages = Array.new(['Eiffel', 'Ruby', 'Python', 'C++', 'Perl', 'Java'])

languages.each do |lang|
  puts "I like " + lang
end

this fragment of code basically gives:

I like Eiffel
I like Ruby
I like Python
I like C++
I like Perl
I like Java

The same result can be achieved in Eiffel by means of the do_all iterator of ARRAY class; the basic step is to substitute Ruby blocks (the portion do |varbind| ... end) with Eiffel inline agents:

feature
   languages: ARRAY[STRING] is
            --
        once
            Result := <<"Eiffel", "Ruby", "Python", "C++", "Perl", "Java" >>
        end

    test is
        --
      do
         languages.do_all (agent (lang: STRING) do
             print("I like " + lang + "%N") 
         end)
      end

Integer class iterators

Other common Ruby iterators can be found in the Integer class:

4.times do |t| puts "Hello, world!" end
1.upto(5) do |n| puts "going up: #{n}" end
10.downto(5) do |n| puts "going down: #{n}" end

The output of these three instructions is the following:

Hello, world!
Hello, world!
Hello, world!
Hello, world!
going up: 1
going up: 2
going up: 3
going up: 4
going up: 5
going down: 10
going down: 9
going down: 8
going down: 7
going down: 6
going down: 5

In Eiffel, INTEGER class is not equipped with such iterators, one could think of an INTEGER_ITERATOR class that implements them or, alternatively, write the following features that make the first integer argument explicit:

feature
    times (n: INTEGER; action: PROCEDURE[ANY,TUPLE]) is
            -- repeat `action' for `n' times
        local
            l_times: INTEGER
        do
            from
                l_times := 0
            until
                l_times = n
            loop
                action.call ([l_times])
                l_times := l_times + 1
            end
        end

    upto (min,max: INTEGER; action: PROCEDURE[ANY,TUPLE]) is
            --
        require
            min_lt_max: min <= max
        local
            l_index: INTEGER
        do
            from
                l_index := min
            until
                l_index > max
            loop
                action.call([l_index])
                l_index := l_index + 1
            end
        end

    downto (max,min: INTEGER; action: PROCEDURE[ANY,TUPLE]) is
            --
        require
            min_lt_max: min <= max
        local
            l_index: INTEGER
        do
            from
                l_index := max
            until
                l_index < min
            loop
                action.call([l_index])
                l_index := l_index - 1
            end
        end

Keeping in mind the Ruby example, these features should be called as follows:

times(4, agent (num: INTEGER) do
  print("time: " + num.out + "%N")
end)

upto(1,5, agent (num: INTEGER) do
  print("going up: " + num.out + "%N")
end)

downto(10,5, agent (num: INTEGER) do
  print("going down: " + num.out + "%N")
end)

Reading a file the Ruby way

And now a more advanced example, in Ruby a text file can be read as follows:

File.open("myfile.rb", "r") do |infile|
  while (line = infile.gets)
     puts line
  end
end

The open method in class File takes as argument a block operating on the file itself, it also takes care of closing the file after block execution, indeed a neat and compact style (but maybe not so explicit: should it be called open_do_something_and_close ?).

So, let's mimic this behaviour in Eiffel:

class
    RUBY_FILE

feature

    open (name: STRING; action: PROCEDURE[ANY,TUPLE]) is
            --
        local
            l_file: PLAIN_TEXT_FILE
        do
            create l_file.make_open_read (name)
            action.call ([l_file])
            l_file.close
        end
end

class RUBY_FILE exports the open feature as seen for the File class in Ruby, an usage example is the following:

feature
    test_ruby_file is
            --
        local
            l_ruby_file: RUBY_FILE
        do
            create l_ruby_file
            l_ruby_file.open ("myfile.rb", agent (f: PLAIN_TEXT_FILE) do
                from
                    f.start
                until
                    f.after
                loop
                    f.readline
                    print (f.last_string + "%N")
                end
            end)
        end

have fun with Eiffel and Ruby !

integer intervals and file line processing

upto can be done in Eiffel with integer intervals:

(1 |..| 5).do_all (agent ...)

it doesn't currently allow descending but we're not too far...

For reading files I'd rather see an iterator like API rather, where the processing agent takes a line, than having to iterate lines by hand: solves 2 problems in one go.

about - contact