September 2006 - Posts
In a recent “exchange” about the relative merits of computer languages between the creator of Ruby on Rails,
David Heinemeier Hansson (DHH) and
Joel Spolsky (Joel), DHH cited trying to save "developer cycles" over CPU cycles as one of his guiding principles. Now, leaving behind the details of that particular debate and the people involved, I'm going to steal his quote for use in my argument.
I agree with this sentiment, and I think it should be applied to many of the unending debates over language supremacy. Ultimately, the debate over which language is better or best is about as useful as a debate over which color is best. By that I mean that it is an argument so arbitrary as to be meaningless.
Programming languages don't exist in a vacuum. Ruby, through Rails, may be great for rapid development of websites, but are you going to use it to create a high performance first-person shooter engine? For those of us in the .NET world, would you give up WinForms for faster executing C if you were creating a desktop application? To say that LISP is great because you can write you our own domain specific language for any problem is perhaps true, but if you are on a tight timeline to deliver a web app into an organization that is all J2EE, would you still use it?
Simply put, the idea that any one language is somehow inherently better for any problem than any other language is a simplification that ignores the realities of what programmers do, solve real problems. This simplification does have some benefits; you can really specialize in the “best” language; you can feel confident that you have the right tool for any situation.
However, it has some negatives as well; on a professional level, you close off opportunities for yourself because you'll only use your language. On a personal development level you isolate yourself and lose out on the insights that come from seeing how other languages do things. Finally, you might find yourself spending a lot of your free time defending the best language and assaulting all of the rest. These negatives make an abstract debate over language a philosophical luxury, interesting to observe, but not really enriching.
So red may be a good color to make a warning sign because our culture understands red to mean danger and khaki may be a nice color to make clothes out of so that it is easier not to clash at work, but that doesn't make red or khaki the best color.
To switch metaphors up a little bit, people naturally like to see a winner after a competition. In both football and baseball the team that scores the most wins. However, would you suit up the Yankees and have them play in the NFL or have the Colts wide receivers try to turn a crucial double play? Of course not. Kind of fun to think about, but won't help you win football or baseball games.
In the end the point I'm making is not a new one. Passionate people will have debates over these things for as long as they are passionate. Debate can spark competition and innovation. For example, a debate over the best way to rapidly develop web applications can lead to an innovative new framework for one language and subsequent reactive actions from others. However, a debate without this kind of clear context just leads to more debate, and on and on. Kind of like an out of control recursive method, gradually wasting more and more CPU cycles.
In the first installment of Ruby Chips, I showed you a very basic class to log debug messages to a text file. To take our logging capabilities to the next step, today we are going to concentrate on adding the ability to log messages to multiple locations, such as email or a database, not just a text file.
The goal is to keep a simple syntax for the end user (the developer) and hide the complexity of multiple logging destinations. So a call to “log” won't just execute the log method in the SimpleFileLogger, it will also execute the log method of other, as of yet unwritten logging classes, such as a DatabaseLogger or EmailLogger.
Rather than dwell on the implementation details of how a database or email logging class would work, I'll just define the following skeleton classes, using the #{expression} syntax for string substitution.
class MyDatabaseLogger
def MyDatabaseLogger.log(message)
puts "Logged #{message} to database."
end
end
class MyEmailLogger
def MyEmailLogger.log(message)
puts "Emailed #{message}."
end
end
Now the next step is stringing together these 3 logging classes, so that one logging call will feed into all of them.
To do this we'll create one master or central logging class which we'll route all of our logging through:
class MyLogDistributor
def MyLogDistributor.log(message)
MyEmailLogger.log(message)
MyDatabaseLogger.log(message)
MySimpleFileLogger.log(message)
end
end
No matter what language you are used to programming in, seeing something like this should start to make you feel nervous.
Why? This implementation does have the advantage of simplicity and if you really knew you'd always want to log to these 3 sources, then maybe it would be fine. More likely, depending on the application, or even the situation, you will want to be able to dynamically determine which sources to log to. In addition, while class methods eliminate the need to keep track of any object instances, they are also less obviously configurable. Do you always want to log to the same filename? To the same email address, etc?
We will address these various criticisms as we go, with the first task being to make our central logging component a little more configurable.
The following code listing shows all 3 logging classes (with minor style cleanup) and the new LogDistributor class, along with the updated test harness.
class MySimpleFileLogger
def log(msg)
File.open("log.txt","a") do |file| #opens file in append mode and closes it after write
file.puts msg
puts "Logged #{msg} to file."
end
end
end
class MyDatabaseLogger
def log(msg)
puts "Logged #{msg} to database."
end
end
class MyEmailLogger
def log(msg)
puts "Logged #{msg} to email."
end
end
class MyLogDistributor
def initialize
@logSinks = Array.new
end
def add_sink(sink)
@logSinks << sink
end
def remove_sink(sink)
@logSinks.delete(sink)
end
def log(msg)
@logSinks.each do |sink|
sink.log(msg)
end
end
end
myLogger = MyLogDistributor.new
myLogger.add_sink(MySimpleFileLogger.new)
myLogger.add_sink(MyDatabaseLogger.new)
myLogger.add_sink(MyEmailLogger.new)
100.times do |number|
Thread.new(number) do |number|
myLogger.log("Message #" + number.to_s)
end
end
Thread.list.each do |t|
if (t != Thread.current) then
t.join
end
end
The new LogDistributor keeps an array of all the loggers (sinks) that it needs to log a message to. When a message is sent, via the log method, each sink's log method is called.
As a dynamically or loosely typed language, you can express this code in the compact form I've shown above. You don't need to setup an inheritance hierarchy or cast anything. Things kind of just work like you would expect.
Loose typing does open the door to some runtime errors if you were to add a sink that did not respond to the log method. Assuming you tested your code you would quickly find this error, however, if you really wanted to be careful, you could use the following defensive code style in the add_sink method:
def add_sink(sink)
if (sink.respond_to?(:log))
@logSinks << sink
else
raise "A sink must respond to the log method"
end
end
The method “respond_to?” is a method that all objects in Ruby can use to test if they have the given method defined. “Raise” is the equivalent of the throw keyword in C#.
Next time, we'll continue to improve on our logging component and add some routing, to determine which messages go where.
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...