ProgrammingBack to Tutorials Sapphire (Ruby In Steel) Tutorials Programming
Ruby The Smalltalk Way #3 - The World According To Objects
This series is an exploration of the similarities and differences between the Ruby and Smalltalk languages. By way of a ‘course text’, it uses the opening chapter of the Smalltalk/V Tutorial. This tutorial, which dates from the late ‘80s, is freely available as a PDF download from Stéphane Ducasse’s :: Free Online Books and I strongly recommend that you read this even if you have never programmed in Smalltalk. I have used the same headings in this series of articles so that you can follow along with the Smalltalk/V text more easily.
|Background reading is the third part of Chapter One of the Smalltalk/V manual - in the PDF download, that’s pages 21 to 29 (as shown in the Acrobat Reader bar - the actual page numbers at the tops of the pages themselves are 11 to 19).|
Object Orientated Programming (OOP) is now so familiar to most programmers that it may seem superfluous to describe what it is and how it works. However, while most modern languages have adopted some features of OOP, they rarely, if ever, implement all the features defined by Smalltalk. For example, some languages such as C++, Delphi and Python implement a mix of OOP with procedural programming. C# and Java are more thoroughly object orientated but they do not fully implement a Smalltalk-like version of encapsulation.
What are objects?
An object is an item which contains data and a set of methods that act upon that data. Each object is an instance of a class. It is the class that defines the methods and the data. It is an important feature of Smalltalk that an object’s data is invisible from outside the object itself. Unlike in many other languages, you cannot directly reference an object’s variables from the world outside that object (there is no ‘dot notation’ equivalent of ob.x to get at a variable named x, for example). If you want to get or set the values of an object’s variables you are obliged to send messages to the object (that is, to “call the object’s methods”). There is no other alternative.
“Related data and program pieces are encapsulated within a Smalltalk object, a communicating black box. The black box can send and receive certain messages. Message passing is the only means of importing data for local manipulation within the black box.”
The Smalltalk/V Tutorial
This is generally the case with Ruby too. In Ruby, an instance variable such as @x of an object, ob, cannot be ‘got at’ by calling ob.@x - that isn’t even valid Ruby syntax. So, most of the time, Ruby is very close to Smalltalk in its insistence on enforcing ‘black box’ encapsulation whereby the data inside an object is hidden from the outside world and access to that data can only be gained by sending messages to an object and having some answer returned by a method of that object.
But there are a few exceptions to the rule. To take just one example, if a Ruby object uses a method that modifies the receiver (that is, one that changes the object itself rather than yields a new object), the ingoing argument to a method can be used like a ‘byRef’ argument in a C or Pascal-like language . Here’s a simple example:
def secretmethod( someVal )
Here the programmer has written a method that reverses a string. He expects people to call the method like this:
x = "hello world"
y = secretmethod( x )
However, one of the programming team notices that the ingoing parameter, x, can be used as a ‘byRef’ argument. So, instead of creating another variable, y, to which the return value is assigned, he writes this:
x = "hello world"
secretmethod( x )
OK, no big deal. It turns out that the end result is the same either way:
x = "hello world"
y = secretmethod( x )
puts x #=> This prints : dlrow olleh
puts y # => This prints : dlrow olleh
But now, at some later date, the programmer goes back and reimplements secretmethod like this:
def secretmethod( someVal )
Assuming that the principle of encapsulation protects the implementation details from the outside world, he believes that his new implementation ensures that the required operation (reversing and setting to uppercase the someVal argument) will now percolate through to everyone’s code. If everyone on the team uses the specified return value, this will indeed be so. But, in fact, it turns out that one member of the team has been using the input value of the argument, not the return value of the method. So now, we have a situation where different members of the team are getting different results when they use the reimplemented method:
def secretmethod( someVal )
x = "hello world"
y = secretmethod( x )
puts x #=> ‘dlrow olleh’
puts y #=> ‘DLROW OLLEH’
Note that, in any method which modifies the receiver (the append operator, << for example) will have the same effect. This is one example - and there are others - of a case in which Ruby gives the programmer the freedom to use or to ignore the send-message and wait-for-answer methodology whereas Smalltalk (generally) enforces that methodology.
But is Smalltalk Really encapsulated...?
OK, let me be brutally honest. It would be wrong to give the impression that Smalltalk’s encapsulation is rigorous and Ruby’s is not. In some cases, Smalltalk too lets you ‘hang onto’ ingoing variables, thereby potentially breaking encapsulation. This may happen in the few cases in which a Smalltalk object (the ‘receiver’) may be modified without yielding a new object. For example, in Squeak I can create a class, MyClass, with a method, secretmethod:, which modifies an array by putting ‘hello’ at its first index. The method then returns the number, 123:
someVal at: 1 put: 'hello'.
If I send an array, x, to this method and assign the method’s response to y, I get 123. But if I ‘hang onto’ the ingoing argument, x, I get an array with ‘hello’ at the first index:
ob := MyClass new.
x := #( 'a' 'b' 'c' ).
y := ob secretmethod: x.
“x is now: #('hello' 'b' 'c')
y is now: 123”
In essence, since the method modifies the ingoing argument, x, it gives me the choice of using that argument as a ‘byRef’ parameter. And when I do that, my code is able to bypass the MyClass object’s encapsulation - in other words, my code becomes implementation dependent. If the implementation of the method changes, the behavior of my code also changes.
This is an exception to Smalltalk’s general principle of ‘black box’ encapsulation whereby the implementation details of an object are hermetically sealed from the world beyond that object. All I can say is that this ability to ‘dirty trick’ your way around data-hiding is pretty rare in Smalltalk. It is more common in Ruby as there are a great many receiver-modifying Ruby methods (all those that end with an exclamation mark, !). Moreover, even the ability to change data inside a Smalltalk array varies according to the implementation. Dolphin Smalltalk, for example, won’t let me modify an array; if I try to do so, an error occurs warning me that I have attempted to modify a read-only object.
Another interesting question to ask is whether Smalltalk’s and (Ruby’s) inheritance mechanism itself breaks encapsulation. In a paper entitled, ‘Object-oriented Encapsulation for Dynamically typed Languages’, Nathanael Schärli, Andrew P. Black and Stéphane Ducasse argue that both Smalltalk and Ruby fail to enforce encapsulation by making the methods and instance variables of a superclass visible to its subclasses so that “whenever a feature of a superclass is modified, the programmer must check all its (direct and indirect) subclasses to ensure that the change does not break existing code. This is because any subclass might use the modified feature and may rely on its old meaning.”
‘Object-oriented Encapsulation for Dynamically typed Languages’ (PDF)
What kinds of objects can be described?
It is a commonly made claim in OOP programming languages that ‘everything is an object’. Closer scrutiny generally shows that this claim is not entirely true. In some languages ‘primitive types’ are not objects. In Ruby, while primitives are objects, blocks are not (though they can be ‘turned into’ objects using special methods and classes). In Smalltalk blocks are objects - they are instances of a Block (or similar) class.
Even the Smalltalk environment and its component parts are treated as objects. For example, to print a ‘hello world’ in a Transcript window you would evaluate:
Transcript show: 'hello world'.
To anyone used to a visual programming environment such as C# or Delphi this may not seem a big deal. After all, this isn’t too far removed from textBox1.Text = ‘hello world’, is it? Well, how about this, then...?
Smalltalk inspect; browse.
Here the Smalltalk system itself is sent the two messages inspect and browse. Evaluating this causes the Smalltalk ‘system dictionary’ first to appear in one window for inspection and then to be displayed in a hierarchical class browser. You can manipulate other features of the environment to display and write into system windows, for example, just by sending messages to the environment ‘objects’ using Smalltalk code. This is different from C# or Delphi. They provide you with visual objects that can be put into your own finished applications but they do not let you manipulate the objects of the native programming environment.
Ruby comes pretty close to Smalltalk in providing access to all the objects in the Ruby system. But there is one obvious difference: Smalltalk defines its own programming environment; Ruby does not. Ruby environments (including Ruby In Steel) may know all about Ruby but Ruby does not know anything about them. In future that might change. There is fundamentally no reason why an implementation of Ruby should not have closer interaction with the objects defined by its programming environment - but at the time of writing no such implementation exists.
How do objects communicate and behave?
In a general sense, both Smalltalk and Ruby provide similar methods of communicating with objects. Messages are sent to an object which then tries to find some way (a ‘method’) of responding. Often many different objects have methods of the same name - so that, in Smalltalk, the printString method is defined for all sorts of different objects just as, in Ruby, the to_s method is so defined. The ability to invoke a method with the same name on different objects goes by the fancy name of ‘polymorphism’. In the early days of OOP, polymorphism seemed a strange and mysterious concept. These days it is implemented in most OOP languages and is probably second nature to most programmers.
How does Smalltalk (and Ruby) organize objects and their methods?
Here Smalltalk and Ruby are very different. In principle there is no reason why Ruby class hierarchies and Smalltalk class hierarchies should not be similar. In fact, the standard Smalltalk class hierarchy is quite deep - with many levels of descent - while the Ruby hierarchy is very shallow - with rarely more than one or two levels of descent. In Smalltalk when a programmer needs a ‘special version’ of an existing class it is normal to create a new descendent of that class and code the differences. In Ruby, many programmers make a habit of modifying the existing class - adding in new methods to extend the capabilities of Array, say, rather than created a new class SomeNewKindOfArray, to implement this new behavior. This is, I realize, a generalization and is not true of all Smalltalk and Ruby programmers. However, it is certainly the case that the standard class library of Smalltalk is much deeper than that of Ruby.
To take a simple example. In Ruby this is the line of descent from the base class, Object, to the two collection classes: Array and Hash (the equivalent of a Smalltalk Dictionary). Note that both descend directly from Object:
Now, here’s the Smalltalk version (here I am using the Dolphin Smalltalk library but other Smalltalks take a similar approach). Note that both Array and Hash descend from a common Collection ancestor and that Array descends from several other ancestors each of which introduces new behavior that is inherited by Array.
Moreover, in Ruby, Array and Hash are, by default, the end of the family tree (though programmers can, of course, create new descendent classes from them). In Dolphin Smalltalk, Array has many ‘siblings’) such as:
In Ruby, incidentally, the String class is unrelated to Array. It too is an immediate descendent of Object. In Smalltalk, a String and an Array are close relatives (descendents of the ArrayedCollection class). Dolphin Smalltalk’s Dictionary, meanwhile, is the ancestor of several more specialized classes such as:
...and so on.
How do you maintain a Smalltalk (or Ruby) world of objects?
Smalltalk lets you work with source files in which you enter the text that defines your classes and objects. It also saves ‘images’ which store the state of your entire Smalltalk environment. If you save the Smalltalk image when you exit the environment, you can restart your work subsequently exactly where you left off. All your classes and objects will be in the same state as they were when you last left them; all the windows, text and graphics in the environment will be just as they were too. Saving a Smalltalk image is a bit like hibernating your PC or saving the state of a virtual PC.
Ruby has no equivalent of Smalltalk’s image. It is debatable whether this is a good or a bad thing. On the one hand, Ruby’s inability to store its state means that you cannot modify the state of Ruby dynamically and restore that state subsequently (well, unless you are running Ruby on a virtual PC, that is). On the other hand, this also means that you cannot accidentally ‘bind into’ Ruby changes that you had intended to be temporary rather than permanent. A Smalltalk image will preserve every change you make which means that you must be extremely careful when testing and trying out bits of code otherwise you may find that your Smalltalk environment permanently retains classes (even your mistakes) and objects from one session to the next. This is not disastrous. You can clean up unwanted objects and delete unneeded classes and you can also, of course, save multiple images to get back to earlier versions of your environment. All the same, image saving does require a certain amount of care and attention if you are to avoid saving more than you intend.
That brings to an end this short series comparing the fundamentals of Smalltalk and Ruby. In these articles I have looked at specific features of the two languages - such as their syntax, style and use of objects. I shall follow up with another article soon in which I’ll give a few personal views on the pros and cons of Ruby and Smalltalk. I’ll also explain a bit more about the principles of language design in general and I’ll explain a few of the issues which we have had to grapple with in the design of our forthcoming Sapphire programming language for .NET.
 Smalltalk and Ruby do not, in fact, make any explicit distinction between ‘by value’ and ‘by reference’ arguments and, most of the time, the actual implementation of the parameter passing mechanism is of no consequence to Smalltalk and Ruby programmers. Here, when I talk about ‘byRef’ arguments I am making a comparison with those languages which do make this distinction. I mean to indicate that any changes made to the object passed as a kind of ‘byRef’ argument to a method may be accessed from outside the object to which that method belongs.