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.
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.keysHowever, 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.
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)
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?)
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$ ./dircmpDirectory1 Directory2
where
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.
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:
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.
TBW
TBW
Parsing Java code with RE and create a dependency graph in
DOT format.
TBW
Regular Expressions
[JAN GUILLOU]
arn: bad
hamilton: worse
fagerhult: great
arn: worst
[PETER BRATT]
book: IB och hotet mot vår säkerhet
collaborations: Jan Guillou
Classes
Class Dependency Graph
GUI: Editor & Previewer