Ruby Rocks!

Ruby is designed for language construction. But the language you create isn’t limited to the features you think to build in. Instead, any language you build is an extension of Ruby — so all of Ruby remains at your disposal. Much of Ruby’s power comes from its totally dynamic nature. It’s expressiveness comes from its convenient syntax for the kind of code you generally need to write. As with any powerful tool, there are risks and complexities. But those issues can be managed. When you do, Ruby’s value proposition is undeniable.

Originally published 2006

How I Fell in Love

When I first took a look at Ruby, it seemed too much like Perl — too many ways to do things, which would make it difficult to learn, because everything you read would look subtly different. Then I read Martin Fowler’s article on Rake. That article described the advantages of having a build language that was effectively embedded inside of a general-purpose programming language. That prompted me to take a second look.

When I began looking at Ruby more closely, I found a wealth of capabilities, mostly stemming from the fact that Ruby is totally dynamic. Since everything is loaded at runtime, you can extend things in ways you really can’t in most other languages, except for languages like Lisp and SmallTalk. (In fact, it is those aspects of the language that are the hardest to grasp for old-school programmers like myself. Making it a totally object-oriented language helps to manage that complexity, but there are still several new ideas to learn.)

As I began to learn more about Ruby, I found that I could do pretty much anything I wanted in only a few lines of code. At that point, might take 3 or 4 hours to figure out what those lines of code ought to be, but it was clear that once I had mastered the right techniques, I was going to have a very powerful language, indeed.

At this point, Ruby looked like a good choice for writing simple utility scripts. But it was also clear that it was being to used write some fairly large-scale applications. That was an appealing range of capabilities that combined the immediacy and fast cycling of script development with a capacity to seamlessly grow those scripts into a full-blown application — or at the very least, to use the knowledge gained in the scripting process. That was appealing because it meant that instead of having 3 languages (shell scripts for simple tasks, Perl for complex ones, and Java for applications), I might get down to one language, so that I had one large skill set instead of three very fragmented ones.

That was an appealing notion that got me interested in learning more. Then I went to a two-day Rails conference in Santa Clara sponsored by the Software Development Forum in 2006. What I learned there blew me away.

Ruby Lets You Create New Languages

In that conference, a small parade of Ruby gurus passed before my eyes. They described a variety of best-practices I was familiar with. They had cool demos and great stories to tell. But the meta-message that came through loud and clear, after hearing all of the talks, was this:

Ruby is Designed for Language Construction

Not since Forth, in fact, had I seen a language that was more purposely tailored to that task. Like Forth, it was easy to “add words” to extend the language. But unlike Forth, it was fully object-oriented, with a pleasant syntax. (Back when I was coding Forth, I used to wish for a Forth with C-style syntax. At the time, I didn’t even know enough to wish for OO capabilities. Little did I know that I would eventually see such a language, and that it would be called Ruby.)

A language you construct to solve a particular problem is known as a DSL, which stands for “Domain Specific Language”. But after the conference, I began to think of it as “Design Solution Language”. Even that term fails to arrive at the heart of the issue, however, because in reality Ruby lets you create a domain-specific extension to the Ruby language. It is there that real power of Ruby lies. When you create a language that helps you tackle problems in a given domain, you don’t leave normal Ruby behind. You extend and augment Ruby to make things easier, but you always have at your disposal the power of the general-purpose Ruby language.

Several examples of that power already exist:

  • Rake is a DSL designed for builds. Jim Weirich put it together to eliminate build language frustrations. (For more information, see Rake Rocks!)
  • Rails is a DSL designed for database-driven web applications. It grew out of the application construction process as David Heinemeier Hansson looked for ways to do things more easily and more quickly.
  • Rspec is a DSL designed for unit testing. It began as a thought floated by Dave Astels. It was a “thought experiment”, because it needed to add new behaviors to standard objects. Ruby’s dynamic extensibility made it possible for Steven Baker to implement the original core in a weekend.
    As for the value of unit testing consider these points to understand why you want a system that makes it as easy as possible:

    1. Unit Testing prevents bugs. (In fact, it prevents many bugs.)
    2. Unit Testing makes bugs easier to find. (Much easier, in fact, because bugs are identified closer to the point where they originate. Imagine, if you will, that routine A affects routine B which affects the data stored at location X — data that is used much later in function Y, which is called when the user performs activity Z. Without unit testing, all you know is Z didn’t work. You spend the next few hours, days or weeks tracing the chain of events until you isolate the problem to routine A. With unit testing, the problem probably appears in the tests for routine B, which makes it much easier to find.
    3. Unit testing makes comprehensive refactoring safe. In fact, it allows for a complete rewrite of critical modules, secure in the knowledge that the new version will be functionally equivalent to the old one, when it is put into place. Read more: How Unit Testing and Refactoring Work Together.
  • rXSLT is a language that overcomes the limitations of XSLT (which is terrific for simple transforms, but which makes conditionals, loops, and subroutine processing very difficult.) Based on Martin Fowler’s terrific writeup on XSL in Ruby: Moving Away From XSLT, the language was implemented as part of the RuDI project (Ruby Utilities for DITA processing), originally hosted at Kenai.com, to allow simple DITA to HTML transforms out of the box, while allowing for greater sophistication later on. (The thrust of that project was to separate document styling from document code so that, for example, designers could use a visual tool like DreamWeaver to manage styles and layouts, without having to change modify the code-generation process to change styles.)
  • The Project. If you get a chance, you have to hear Rich Kilmer’s hilarious story about “The Project” — a prototype that needed to be built in a matter of months. He outlined the design language and refined it in a day-long session with the project lead. Because he knew how Ruby works, that language was designed to be executed — so well designed, in fact, that he coded the implementation on the plane home. In the next couple of months, he added incredible graphics, merged them with geographic maps, interviewed user-candidates, and revised coding assumptions. The result was a sterling demo of the finished project, in lieu of a prototype, all resulting from a part-time effort — enabled by the fact that the basic logic was captured in a design language that was easy to execute in Ruby. After that, it was just matter of making corrections and making it sparkle with glitz and glamour.

The basic logic was captured in a design
language that was easy to execute in Ruby.

In all three cases, the languages make it a world easier for programmers to carry out common tasks. But they don’t replace the implementation language (Ruby), they add to it. That’s what makes Ruby different from a 4GL, for example, or other such languages where it is easy to do common tasks, but impossible to do anything else. You can always do what you could do before. You just have easier ways to do the things you do a lot.

Note: I owe Chiaroscuro for helping to plant this idea in my head. Here’s my summary of his concept:

Liquid Development meta programming is about dealing with software design as language design. It’s goal is to create an executable knowledge model that an experienced user can understand and manipulate. The first step in that process is to excavate the principles that underlay the model, creating an ontology. The second step is to construct an executable version of the model, in the form of a language. Because Ruby excels at language-construction and at creating programs “on the fly”, it is ideal for that purpose.

Learn more:

Ruby Heralds a New Design Paradigm

After that conference, I realized that a new design model may be about to emerge. In that model, you work with the intention of building a language that makes it easy to solve your problem. If you’re a designer, you’ll think in terms of designing that language. If you’re an agile developer, you’ll think in terms of building up to it, letting the language emerge from the use cases, as you implement them. But either way, any complex problem will tend to produce a language that makes it easier to accommodate changing requirements and solve future problems.

In that paradigm, a cadre of “hard core” Ruby coders will build and maintain the design language. They’ll know all the ins and outs of Ruby’s dynamic extensibility, and they’ll deal with the complexities that come about as a result. But there can then be a much larger body of domain programmers who use that language to solve specific problems. Those coders won’t need anything like the skill set of the core programmers, but they’ll be able to do bigger and better things, the more Ruby they know.

As was pointed out at the conference, it’s easier to read a foreign language than it is to speak it. The same holds true for a DSLs, as well. So the experienced Ruby practitioners observed that a domain expert could review the code they had written for accuracy. Even when they didn’t know Ruby they could follow the logic of a program written in a DSL, because that language uses terms that are specific to their domain.

Note:
In a similar vein, I have noticed that the scripts I have written have fewer comments — because the code is pretty darn readable. That’s a novel experience for me.

The new design paradigm is poised to give rise to potentially new social construct — a continuum of coding expertise:

  • Domain experts can read and review domain-specific code.
  • By picking up a little additional syntax, some of those experts will become domain programmers.
  • By learning more about Ruby, some of those domain programmers will progress from simple tasks to complex extensions.
  • By learning a lot about Ruby, some of the expert domain programmers will become language coders.

So in the same way that Ruby gives me a language that range the continuum of simple scripts, complex scripts, and applications, DSL extensions to Ruby creates a continuum of language skills ranging from simple domain-specific tasks, complex tasks, and the DSL itself.

Ruby is a Fully Dynamic Language

Ruby’s language-extensibility is made possible by the fact that Ruby is a totally dynamic language. Everything happens at runtime. There is power in that capability — but there is also risk. This section takes a look at both.

  • You can add features to existing classes — and even to existing objects
    Wish you had an operation on String that isn’t defined in the standard class? No problem. Just define the method, specifying the class as if you were creating it for the first time. Class definitions are additive, so your method gets added to the class. You can even add methods to a specific instance of a class — methods that aren’t shared by any other instance. (That’s how class extensions are performed, in fact. Each class definition is an object in Ruby. When you add a method to a class, in reality you’re adding a method to the class-definition object — methods which aren’t shared by any other instance of the Class class.)
  • Modules are loaded when they’re required
    When Ruby encounters a require or load statement, it loads the specified module. (With require, it only loads it if it hasn’t been loaded before. With load, it brings in a fresh copy each time.) That feature drastically improves start up time. But if that feature is combined with additive classes, it means that an object’s capabilities depend on the code that has been executed. Take a different code path, and the functional may not exist — or have a different behavior. That’s very powerful stuff, but also very dangerous.
  • Evaluations
    Like Lisp, it’s easy to generate code and then execute it. Again, there is a great deal of power here. But readability suffers. What is that code doing? You have determine what code is being created, and then determine the result that will be produced by that code. It’s a two-step deduction that makes the code much harder to read — but at the same time, infinitely more powerful.
  • methodMissing() and other language “hooks”
    With methodMissing() you tell Ruby what to do when someone accesses a method that isn’t there. That’s the key to making database fields automatically available in Rails. When you ask for the address field of a Person object, Rails goes to the People database table, finds that the Address column is present, and returns the value for that Person as though you had coded an address() method — only you never have to, because Rails does it for you. Other hooks let you determine what to do when a method is added. It’s a huge amount of self-referential power that comes from languages like SmallTalk. It’s all very powerful, but it is also very easy to get in trouble.

The fact that Ruby is totally dynamic makes it very easy to write. You can get a lot done in a few lines. And when you take advantage of its dynamic extensibility and evaluations, you can do some very powerful things in some very elegant ways. But that power comes at an expense: Ruby code can be harder to read, which can make it a lot harder to maintain.

When you’re coding, and you want to know what an object is capable of, it can be difficult to find out. The object’s class could be generated dynamically. It could be extended at runtime. Or the object might have been extended in ways that don’t apply to the class as a whole. Those capabilities tend to make systems that are dearly loved and easily manipulated by those who built them, but which may be close to impenetrable by outsiders.

There are several strategies for dealing with that complexity:

  • Testing
    Adequate testing is clearly a requirement in such a dynamic setting. On the other hand, adequate testing is a requirement anyway. It is always a requirement. So if you’re already doing it, this is no big deal. If you’re not, you should. And if you’re using Ruby, you definitely should. You can’t afford not to, in fact.
     
  • Tools
    Debuggers will be needed that highlight new features that have been added to a class. It should even be possible to specify a breakpoint on class, so the debugger stops when it is modified.
     
  • Project specificity
    Ruby is clearly appropriate when the original designers are likely to be involved for the lifetime of the
    project. It’s power and expressiveness make it too compelling to ignore. On the other hand, a
    large-scale enterprise development project may be in operation for 20 years. In that time, hundreds
    of developers may come and go. In such projects, ongoing maintenance dominates the cost equation
    (as much as 10 times the cost of the original development, or more). In such cases, readability may
    well be the most desirable trait in the programming language. (For that Java is excellent.)
     
  • Design language paradigm
    Finally, the complexity can be managed with the design language paradigm. If a domain-specific language extension can be constructed, then only a small core of developers need to master the really arcane dynamic aspects of Ruby. The vast majority of coders can then use the simpler, domain-specific language.

Ruby is an Expressive O-O Language

Ruby doesn’t require you to specify types, although you can. Perhaps more importantly, it uses duck typing. (If it quacks like a duck, it must be a duck.) In other words, if an object responds to the appropriate messages (methods) in the appropriate way, then it can be used in a similar context. So it may be possible to use either a Boat or Car in a context that requires a drivable object without having to create a Drivableinterface or Vehicle superclass.

You can create a superclass if you want, of course, because Ruby is fully object-oriented. I’ve even seen an article on the web that tells how to construct an interface in Ruby. But you don’t have to. That gives you a lot of freedom.

Here are some of the language features that make it so easy to express your intentions in Ruby:

  • Lists and Maps
    Lists and maps are Ruby’s main data structures, other than Strings. Lists are defined using array syntax: [“x”, “y”, “z”]. Maps are defined in braces: {“a” => “x”, “b” => “y”}.
     
  • Iteration methods and Code Blocks
    The each method iterates over a list. Only rather than simply returning objects for you to operate on, you send a block of code to execute on the list. Typically, for a one-line block of code, you use braces: someList.each { |item| … }. For a multi-line block of code, you use do…end: someList.each do |item| … end. It’s a new idiom for old school programmers like me, but very powerful, once you get used to it.
       
  • Blocks and Closures
    Iterations are just one of the ways that blocks are used. Closures are blocks (anonymous code segments) that know where they came from. So you can create a closure in a method by passing in a parameter, and then execute the resulting code. (You create a reference to it using the keyword lambda or proc.) Blocks and closures are used all the time in Ruby code. You’re forever passing a block to a method, which may operate by passing a code block back or passing one on to a different method. It’s can be difficult to follow, until you get used to it. (I can’t wait for that day to come.)
     
  • Regular expressions
    Regular expression processing is built into the language, as well. They’re defined between forward slashes: /.*/, and have their own comparison operator: =~. (The regular exression can occur on either side of the operator.)
     
  • Expression-based Case Statement
    Icon was the first language I used that gave me built-in regular expressions. It also had a powerful case statement that wasn’t limited to primitive values. Instead, it matched expressions. I’ve missed both. Ruby gives them back to me.
     
  • A Host of Methods
    There are dozens of methods that make things easy. There are ways to slice and dice strings and arrays. You can iterate over the contents of a file in a single line of code, or just as easily put its contents into a string for some serious processing. There are classes for Internet operations, and dozens of others. If there is something you want to do, odds are there is a way to do it.
     
  • Useful Syntax
    There are dozens of language features that you discover as you go along. For example, the a range of values is expressed with Pascal notation (3..5). There are Modules — like classes, only you can’t instantiate them. Instead, you mix them into existing classes to literally extend their behavior, adding new functionality directly to the class. And there are Structs — methodless objects that are simply data structures.
     
  • C-language extensible
    When performance is a serious consideration, or when there is some new device you need to access, you can code in C. Of course, for a high-level language, C is a terrific assembler (joke) — but you do have that kind of raw horsepower, when you need it.

  • Java Compatibility
    Thanks to the Java HotSpot runtime engine, Java programs run as fast (and often faster) than native applications — although at times there is a small start-up penalty before the optimization happens. But given that Java runs reasonably uniformly, just about everywhere, and that the rich set of Java classes can be invoked from Ruby, after a while I began doing all of my Ruby development with JRuby.

Wide Range of Applicability

To summarize, Ruby is useful for a wide range of tasks by itself:

  • Simple and complex scripts for
    • File processing
    • Pattern-based string processing
    • Sending email
    • URL processing
    • CGI scripts
  • Large, object-oriented desktop applications
  • Large, object-oriented server applications
  • Domain-specific language design and construction

But with the appropriate extensions, it’s scope of applicability widens to include:

  • Build scripts
  • GUI apps
  • Testing frameworks
  • Web applications

And that’s just what’s available today. Who knows what’s coming?

Conclusion

Ruby is an extremely powerful, dynamic language. The more you know, the easier it is to create readable, virtually self-documenting programs. It can also be dangerous. There are a variety of strategies to manage the complexity. Using them minimizes the risks and maximizes the benefits of using Ruby.

Learn more: Getting Started in Ruby

Copyright © 2006-2017, TreeLight PenWorks

Add a Comment

2 Comments on "Ruby Rocks!"

...

Sort by:   newest | oldest | most voted
trackback

[…] the program can run on more than one or two pre-programmed tunes. It also needs to be converted to Ruby, which is a lot more fun to […]

trackback

[…] Those tools make it easy to express the solution for the problem you are trying to solve. The ease of expression translates directly into rapid construction of new solutions, and ready comprehension of existing ones. (For more, see Ruby Rocks!) […]

wpDiscuz

Categories