Getting Started With Ruby

By popular demand from previous students, we have put together a small collection of experiments/exercises to get you started with Python and Ruby. We start with some of the basic stuff like getting to know the interactive environments and converting between different data types, and then move on to simple functions, classes, tools and a graphical user interface.

While you don't need to do these exercises to pass the course, they are a good starting point to learn the language.

Familiarise Yourself with the Interactive Environments

In contrast with Python, Ruby has a separate interpreter and interactive environment. The first one is called ruby, the latter is called irb.

Start the interactive ruby interpreter by typing irb on the command line. irb is a great tool for learning the Ruby language, trying out different commands, remembering the order of parameters, etc.

When irb starts, it will look something like this:

		    
		      irb(main):001:0> 
		    

Let's start by evaluating a few simple expressions and watch how the terminal behaves.

		      1. 42
		      2. "Hello World"
		      3. x (this should raise an error)
		      4. []
		      5. [1, 2, 3, 4]
		      6. x = "Deeo"
		      7. x
		      8. y = { 1 => "First", "Second" => 2 } 
		    

Ok, that was a few Ruby literals and expressions. Use google or your books if you have trouble understanding what happens. Then, let's do something a little more complicated.

		      1. 4+47
		      2. 3245434*23423434
		      3. 10/3
		      4. 10/4.0
		      5. 4**2
		      6. "==" * 20
		      7. 20 * "==" (this should raise an error)
		      8. y 
		    

If y raises an error, you should type in y = { 1 => "First", "Second" => 2 } again. This creates a Hash table object with 1 and "Second" as keys and "First" and 2 as values mapped to by the respective keys. To get a feel for what you can do with the hash table, you can query its methods by sending it a methods message. Go ahead:

		      1. y.methods 
		    

The result should look something like this:

		      => [:rehash, :to_hash, :to_a, :inspect,
		      :to_s, :==, :[], :hash, :eql?, :fetch, :[]=,
		      :store, :default, :default=, :default_proc,
		      :default_proc=, :key, :index, :size,
		      :length, :empty?, :each_value, :each_key,
		      :each_pair, :each, :keys, :values,
		      :values_at, :shift, :delete, :delete_if,
		      :keep_if, :select, :select!, :reject,
		      :reject!, :clear, :invert, :update,
		      :replace, :merge!, :merge, :assoc, :rassoc,
		      :flatten, :include?, :member?, :has_key?,
		      :has_value?, :key?, :value?,
		      :compare_by_identity, :compare_by_identity?,
		      :entries, :sort, :sort_by, :grep, :count,
		      :find, :detect, :find_index, :find_all,
		      :collect, :map, :flat_map, :collect_concat,
		      :inject, :reduce, :partition, :group_by,
		      :first, :all?, :any?, :one?, :none?, :min,
		      :max, :minmax, :min_by, :max_by, :minmax_by,
		      :each_with_index, :reverse_each,
		      :each_entry, :each_slice, :each_cons,
		      :each_with_object, :zip, :take, :take_while,
		      :drop, :drop_while, :cycle, :chunk,
		      :slice_before, :nil?, :===, :=~, :!~, :<=>,
		      :class, :singleton_class, :clone, :dup,
		      :initialize_dup, :initialize_clone, :taint,
		      :tainted?, :untaint, :untrust, :untrusted?,
		      :trust, :freeze, :frozen?, :methods,
		      :singleton_methods, :protected_methods,
		      :private_methods, :public_methods,
		      :instance_variables, :instance_variable_get,
		      :instance_variable_set,
		      :instance_variable_defined?, :instance_of?,
		      :kind_of?, :is_a?, :tap, :send,
		      :public_send, :respond_to?,
		      :respond_to_missing?, :extend, :display,
		      :method, :public_method,
		      :define_singleton_method, :__id__,
		      :object_id, :to_enum, :enum_for, :equal?,
		      :!, :!=, :instance_eval, :instance_exec,
		      :__send__] 
		    

New, let's go ahead and use this information.

		      1. y.keys() (try leaving the () out and see what happens)
		      2. y.values()
		      3. y[1]
		      4. y["second"] (notice casing)
		      5. y["Second"]
		      6. y["Second"] += 1
		      7. y["Second"]
		      8. y 
		    

This simple piece of code will iterate over all keys of y and print it corresponding values:

		      for key in y.keys() 
		         puts y[key]
		      end
		    

The output should be something like this:

		      First
		      3
		      => [1, "Second"]
		    

where the first two lines are the output written by puts and the second line is what is returned by the for-loop. In Ruby, almost everything is an expression which means that it returns a value. For example, you can do like this:

		      x = if 1 < 2
			      4
		            else
			      5
		            end
			
		      puts x
		    

which assigns 4 to x as 1 clearly is less than 2

But let us return to our for-loop. Ruby, heavily influenced by Smalltalk, has an excellent support for closures. Closures are objects that represent blocks of code (roughly, we'll talk about this during our Ruby lectures).

Ruby collections provide a lot of methods that accept a closure as input that will be executed for e.g. every element in the collection.

Iterating over the hash table with closures will look like this:

		      y.each_value { |v| puts v }
		    

The { |v| puts v } (the closure) is a code block that gets passed as an argument to the each_value method that executes the code block once for each element stored in the hash map. The |v| means that the block has one free variable v that should be supplied when the code block is actually executed. You could say that the block takes one parameter, called v.

A nicer iteration over the hash map is this:

		      y.each { |k,v| print k, " --> ", v, "\n" }
		    

which prints

		      1 --> First
		      Second --> 3
		    

Ruby's string interpolation mechanism allows us to write that in a more readable (in the code) fashion:

		      y.each { |k,v| puts "#{k} --> #{v}" }
		    

I can now use puts to avoid the newline (why could I not before? Try!).

Ruby's lists and hash maps are really powerful, light-weight data structures. You'll find that a lot of time, there will be no need to create classes to keep data around. Lists and hash maps will do fine.

If you've peeked at the Python exercises, you've noticed that this page follows the same structure so far. Thus, at this point, we should get a reference to an object representation of the keys method. In Python, this was done thus:

		    1. y.keys 
		    
However, if you try that in Ruby, you'll notice that that is equivalent to y.keys(). Ruby has very lax syntax rules that allow code to be written in a very nice way. To get a method, we use a more OO-ish approach:

		      m = y.method(:keys)
		    

Now, m holds a reference to a method object bound to the method keys in y. To call it, we simp ly tell it to:

	    
		      m.call()
		      => [1, "Second"]
		    

You may wonder what :keys is. Why not "keys"? Ruby has something called symbols (borrowed from Lisp). Symbols are basically strings used as hash-keys and otherwise string-valued identifiers. The symbol :ruby is treated by Ruby as a special value, and each instance of :ruby in all code files will be exactly the same. You can use strings in many places where symbols are expected, but they are not the same. Ruby works much faster with symbols than with strings.

To get help about something in Ruby, you can use the ri program in the terminal. For example, if we wanted information about the new method in files (new is Ruby's name for constructors), we could issue:

		    
		      ri File.new
		    

to get some nicely printed help in the terminal. Note that this is done in the terminal, not in irb.

Now you are ready to start converting between data types. Move along.

Converting Between Different Data Types

Ruby uses OO + convention for data conversion. Conversion yields copies, it does not coerce the object in-place.

	    
		      * 'a'.ord (ordinal value for character)
		      * 97.chr (character for ordinal value)
		      * y.to_s (converts anything to a string, equal with 
		      Java's toString())
		      * "23".to_i (converts a string to a Fixnum)
		      * y.to_a (coerce to array)
		      * "2.3".to_f (as above but for floats, no base)
		    

Using the for loop construct as outlined above, write a small loop, still in the interpreter, for printing the ascii values of the letters A-Z. To our aid comes the Ruby's range (...) method that operates like this:

	    
		      >>> 0...10
			=> 0...10
		      >>> (0...10).to_a
			[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
		    

The for loop construct that we used above was a for each loop, so by use of [0], chr, ..., and print, you should be able to get this output from a 2-line program, but for capital letters:

		    
		      a has ord 97
		      b has ord 98
		      c has ord 99
		      ...
		      y has ord 121
		      z has ord 122
		    

Simple Functions

Still in the irb, type:

	    
		      MESSAGE = "Hello "
		      def greet(person)
		        print MESSAGE, person, "!\n"
		      end
		    

This defines a function greet that takes a single argument, person and prints the standard greeting message. Now go ahead and call the method:

	    
		      greet("Matz")
		    

The dynamic typing allows even this:

	    
		      greet([])
		    

or

	    
		      greet({})
		    

which will result in us getting a print out of the string representation of the empty list, the empty hash, etc.

Interestingly enough MESSAGE is not a global variable. Rather, irb executes in the context of Object, and when you assign to a new variable like MESSAGE, it is stored in Object. Also, interestingly enough, by being spelt with a capital first letter, MESSAGE becomes a constant.

Let's now define a classic method -- factorial:

	    
		      def fac(n)
		        if n > 0
		          n * fac(n-1)
		        else
		          1
		        end
		      end
		    

Try it with really big numbers. You'll be surprised. Interestingly enough, you can redefine the method by typing it in twice. So let's do it in some alternate syntax, but with equivalent semantics:

	    
		      def fac(n); return 1 unless n > 0; n * fac(n-1); end
		    

Ruby statements that end with ; allow another statement to follow on the same line. Observe the ; after def fac(n). Also observe the first statement:

	    
		      return 1 unless n > 0
		    

which should be perfectly clear to you as it reads just like English.

The greet function is still around, so lets do this:

	    
		      MESSAGE = "The result is: "
		      greet(fac(27))
		    

Notice the warning when we redefine MESSAGE. It only warns, not fails.

Ruby allows a variable number of arguments to a method by prefixing its last variable name with *:

	    
		      def example(*args); p args; end
		    

Now, calls to example will take arbitrarily many arguments which will all be packaged into the args array. Try it:

	    
		      example(1, 2, :d, 5, "Hello")
		      [1, 2, :d, 5, "Hello"]
		      => [1, 2, :d, 5, "Hello"]
		    

Exception handling works much like expected (if you know Java, but with different words):

	    
		      def example(a, b)
		        begin
		          a / b
		        rescue ZeroDivisionError
		          puts "Division by zero"
		          0
		        end
		        end

		      example(4,5)
		      example(4,0)
		    

Implement some simple utils

cat (simple file open/read)

The cat utility reads files sequentially, writing them to the standard output. Write your own cat utility that takes a number of file names as command line arguments and for each file writes it to standard out. If the -n option is specified, the lines should be properly numbered starting from 1 with each file. If the file is 800 lines long, line 1 should have number __1 (where _ are spaces).

Feel free to play around more with cat or do man cat to get more information on the util.

grep (regular expressions)

The command line tool grep is invoked like this:

	
		    grep PATTERN [FILE...]
		  

When it runs, searches the named input FILEs (or standard input if no files are named, or the file name - is given) for lines containing a match to the given PATTERN. By default, grep prints the matching lines. The pattern should be a regular expression.

Re-using the skeleton code from cat, implement a grep utility of your own. Feel free to implement the additional bells and whistles that you find in grep too.

(What happens if PATTERN is a faulty regular expression?)

Implement utils with OS interaction

Rename

(This exercise will drill you more in the use of regular expressions, but also in the OS libraries for moving files etc.)

In many *nix systems there is a file renaming utility called rename written in Perl. The rename utility will allow more advanced batch file renaming. For example, to rename all files matching "*.bak" to strip the extension, you might say

		
		    rename 's/\.bak$//' *.bak
		  

To translate uppercase names to lower, you'd use

		
		    rename 'y/A-Z/a-z/' *
		  

Read up on regular expressions if the above commands are not clear to you. Hints: s/a/b/ substitutes all occurrences of a with b. Further, y/ab/12/ transliterates all occurrences of a with 1 and b with 2. $ matches the end of the line (or before newline at the end). A good place to read is in the man pages:

> man rename man perlre man perlop

Comparing File Trees

Using the Ruby libraries, create a utility to compare the contents of two directories (including subdirectories). The utility should be invoked like this:

		
		    beefheart$ ./dircmp  Directory1 Directory2
		  

where is either -b or --both for finding the set of files that are in both directories, or -u or --unique for finding the set of files in Directory2 that are not in Directory1.

Files are considered equivalent if they have the same name and size. If you want, you can extend the tool to also compute checksums for files of the same size and compare checksums to capture content. Or some other method.

Navigating the Ruby libraries is quite simple. Start looking at the index at Ruby-Doc and go from there. As static type information is lacking, you will find that the libraries look a bit different from, say JavaDoc.

Regular Expressions

The Windows .ini file format is very simple. Each file has 0 or more clauses. A clause starts with the name on the form [NAME-OF-CLAUSE] followed by 0 or more key-value maps on the form key: value. Keys need not be unique, but have a precedence order -- a key that appears again will "override" the all previous ones.

Your task is to write a program that converts from .ini files to XML. For example:

		
		    [JAN GUILLOU]
		    arn: bad
		    hamilton: worse
		    fagerhult: great
		    arn: worst
		    
		    [PETER BRATT]
		    book: IB och hotet mot vår säkerhet
		    collaborations: Jan Guillou
		  

Your program should generate this XML file (or equivalent):

		
		    
		      
			
			
		      
		      
			
			
		      
		      
			
			
		      
		    
		    
		    
		      
			
			
		      
		      
			
			
		      
		    
		  

Note that the duplicate key has been removed.

Also try and do this with YAML rather than XML.

Classes

TBW

Class Dependency Graph

TBW

Parsing Java code with RE and create a dependency graph in DOT format.

GUI: Editor & Previewer

TBW

 

 
 
 
 
 
 
'