Api simplicity and design in QtRuby/Korundum

Michael Larouche writes about the new KDialog api, and how it is simpler to understand than the old KDE3 one. It reminded me of one of my favourite talks at the 2004 Ludwigsberg aKademy, when Mathias Ettrich discussed similar usability improvements in Qt4. I'd like to describe a couple of features in QtRuby and Korundum that make such constructor code clearer still.

Here is the original C++:

KDialog *newDialog =new KDialog(parent);
newDialog->setCaption( i18n("Caption") );
newDialog->setButtons( KDialog::User1 | KDialog::User2 );
newDialog->setDefaultButton( KDialog::User1 );

And three versions the same code in Korundum:

# (1) Similar to C++ version, with setXXX() method calls
newDialog = KDE::Dialog.new(parent)
newDialog.setButtons(KDE::Dialog::User1.to_i | KDE::Dialog::User2.to_i)
# (2) Passing a block with no args to the constructor, with the setXXX() method calls
# evaluated in the context of the newly created KDE::Dialog as 'self'
newDialog = KDE::Dialog.new(parent) do
setButtons(KDE::Dialog::User1.to_i | KDE::Dialog::User2.to_i)
# (3) Passing an arg 'd' to the block, which captures both the new KDE::Dialog 'd'
# *and* its context in the surrounding code
newDialog = KDE::Dialog.new(parent) do |d|
d.caption = i18n("Caption")
d.buttons = KDE::Dialog::User1.to_i | KDE::Dialog::User2.to_i
d.defaultButton = KDE::Dialog::User1

Version (1) is very similar to the original C++ code, and doesn't really need much explanation. A minor difference is that if you want to OR together two enums in QtRuby, you need to call 'to_i' to explicitally convert them to Integers, whereas the C++ code does that implicitally.

In version (2) when you pass a block to a constructor it effectively allows you to create a 'customised constructor' with any code needed to fully construct a useful instance passed in the block. With the original C++ version it isn't fully constructed until after you have made the setDefaultButton() call, but you have to work that out from reading the code. With the Ruby code it is explicit, and it is clearer when your KDE::Dialog is fully initialized and ready to use.

In the final version an argument of 'd' is passed to the block, and any method calls for the new instance are made by calls to d, such as 'd.setCaption(i18n("Caption"))'. In version (2), the special variable 'self' was the new KDE::Dialog inside the block, and so you just need to call 'setCaption(i18n("Caption"))'.

Version (3) shows how any setXXX() method with a single argument can be written like an assignment to an attribute. So these two calls are exactly equivalent:

d.fooBar = thing

A further difference between versions (2) and (3) is more subtle. The environment that contains the newly constructed KDE::Dialog can be referenced within the block in version (3). For instance, suppose you had set up the dialog caption previously like this:

my_caption = i18n("Caption")
newDialog = KDE::Dialog.new(parent) do |d|
d.caption = my_caption

In version (2) if you tried to do that, Ruby would assume that 'my_caption' was a new local variable within the block and it would be set to 'nil', and not "Caption" because the assignment to 'my_caption' was outside to context of the constructor.