Ruby Chips – Part 1
This is the first post in a series of a posts I am planning that will showcase features of Ruby as I encounter them. The first few posts will focus on a sandbox project I started recently, a logging component.
At its heart, logging is a simple concept, but for the sake of learning it is possible to complicate it nicely.
As I go along, I'll try to point out the features of Ruby that are salient to the code we are working on. I'm learning this as I go, so I can't promise complete definitions, but I will try to point you in the right direction.
So, we'll start with the most basic implementation, the “Hello world” of logging. This implementation will just write a given piece of text to a log file:
class MySimpleFileLogger
def MySimpleFileLogger.log(message)
File.open("log.txt","a") do |file|
file.puts(message)
end
end
end
At a high level all we've done is defined a class with one class level (i.e. static) method that writes the text to a file. The method is identified as a class level method by prefacing it with the class name in the definition.
The construct of “File.open” is somewhat similar to a using statement in C#. It not only opens the file, but also closes it when it the method call is complete.
The chunk of code that starts with “do |file|” and ends with “end” is called a block. Blocks are everywhere in Ruby. In some ways blocks are like anonymous methods. As is often the case in Ruby, if you know how to “pronounce” the different parts of the code you can read aloud the code to figure out what's going on.
Here's how I'd pronounce that chunk of code: “Tell the class File to open log.txt. I'll call the result of opening log.txt “file” and then I want to tell the file to “puts”/write the message to itself.”. So File.open returns a File object, which I call “file” and then I call a method on that object to write out the message.
To smoke test this class I just appended a simple test harness after the class definition:
100.times do |number|
Thread.new(number) do |number|
MySimpleFileLogger.log("logged: " + number.to_s)
end
end
Thread.list.each do |t|
if (t != Thread.current) then
t.join
end
end
In Ruby, everything is an object, so “100.times” is a valid call of the method “times” on a number object. Times is a method that takes a block and executes it as many times as the number it is called on.
So once again we encounter a block, in fact, a block within a block. The purpose of this code is to create a 100 threads and have each thread log one message. The second block, after “Thread.new” is the method you want each thread to execute after it is created.
Finally, the last bit of code uses a class level method “list” on the class Thread which gives you an array of all the active threads. This array is then iterated through and every thread that is not the current thread is waited on until it terminates.
Confused? Well, maybe seeing the output will help.
After I ran this code, I checked the log.txt file and saw what I expected:
logged: 0
logged: 1
logged: 2
logged: 3
logged: 4
logged: 5
logged: 6
...
Hopefully this gave you a feel for what Ruby looks like in a somewhat real context. If you still think blocks are confusing, check out
Why's Poignant Guide to Ruby, which if it doesn't help you figure out blocks, will at least make you laugh.
Until next time...