Affordances: Arguments
This post is part of an ongoing series about Affordances and Programming Languages.
Programming languages provide different facilities for defining functions and methods. In particular, they provide more or less flexible ways of specifying parameters.
Smalltalk’s simple, consistent syntax provides only one option: ordered, required parameters. If you want to provide optional default values, arbitrary parameter ordering, or a variable number of arguments, you need to use separate explicit methods.
"The basic version"
findFontNamed: aString size: anInteger style: aSymbol
"find the font"
"Alternate ordering"
findFontNamed: aString style: aSymbol size: anInteger
^self findFontNamed: aString size: anInteger style: aSymbol
"Default arguments"
findFontNamed: aString size: anInteger
^self findFontNamed: aString size: anInteger style: self defaultStyle
findFontNamed: aString style: aSymbol
^self findFontNamed: aString size: self defaultSize style: aSymbol
findFontNamed: aString
^self findFontNamed: aString size: self defaultSize style: self defaultStyle
defaultStyle
^:normal
defaultSize
^12
"Variable arguments"
fontsInFamilies: aCollection
"list all fonts in families listed in aCollection"
"Short-hand for single families"
fontsInFamily: aString
^self fontsInFamilies: (Array with: aString)
Smalltalk’s keyword method syntax makes for very regular, readable code and that is a significant advantage. But it gets tedious to define a flexible API, as you can see above.
C++ is more flexible, in that it allows for optional default values.
In C++11, there are also several ways to allow a variable number of
arguments, including
varargs
,
variadic templates,
and
std::initializer_list
.
I use the latter here:
const int DEFAULT_SIZE = 12;
const FontStyle DEFAULT_STYLE = FontStyle::NORMAL;
// The basic version, including default arguments
Font find_font(const std::string& name, int size = DEFAULT_SIZE, FontStyle style = DEFAULT_STYLE)
{
// Find the font
}
// Alternate ordering, again including default arguments
Font find_font(const std::string& name, FontStyle style = DEFAULT_STYLE, int size = DEFAULT_SIZE)
{
return find_font(name, size, style);
}
// Variable arguments
FontList fonts_in_families(std::initializer_list<std::string> names)
{
// List all fonts in families listed in names
}
Creating a flexible API in C++ involves less code than in Smalltalk, which is an advantage. But with a complex API, it can be hard to know what the various parameters mean because of the lack of keyword arguments.
Ruby provides the best of both worlds, especially in Ruby 2.0 with its
keyword arguments. It’s possible to use a Hash
in Ruby 1.9 and
earlier to have the same effect, but it involves a bit more code.
# The only version needed
def find_font(name, size: 12, style: :normal)
# Find the font
end
def fonts_in_families(*names)
# List all fonts in families listed in names
end
In Ruby, we only need one variant of the find_font
method, because
keyword arguments can be specified in any order and also have default
values. This allows for very flexible APIs. The cost is that, when
designing an API, you have to decide which facilities to use.
I’d be remiss if I didn’t give a nod to Common Lisp’s argument facilities here.
(defun find-font (name &key (size 12) (style 'normal))
"find the font"
)
(defun fonts-in-families (&rest names)
"list all fonts in families listed in names"
)
As with most things in programming languages, there are tradeoffs to be made. Smalltalk’s simple, consistent syntax makes for very clear, readable code. However, it doesn’t provide good affordances for flexible APIs. C++, Ruby, and Common Lisp provide more affordances, making it easier to write flexible APIs. However, that puts more pressure on the programmer to decide which facilities are best to use where. Doing that well takes experience.
With great power comes great responsibility.