This post is part of an ongoing series about Affordances and Programming Languages.

Every programming language I’m familiar with provides some kind of syntax for literal values of “primitive” types like numbers, characters, and strings. Many languages go further and allow you to create literal values of more complex types, like arrays and hashes.

Of the three languages I use the most, C++98 is the most limited in this regard:

Complex Literals in C++98
// C-style arrays are OK
int array[] = { 1, 2, 3 };
// No literal syntax for standard library collections
std::vector<int> v;
for (int i = 0; i != 3; ++i) v.push_back(i);
// Similarly for maps
std::map<int, std::string> m;
m[1] = "foo";
m[42] = "bar";
m[123] = "baz";
// Structs work well
point_struct p1 = { 1, 2 };
// So do arrays of structs
point_struct points1[] = { { 1, 2 }, { 3, 4 } };
// Objects are OK
point_class p2(1, 2);
// Arrays of objects not so much
point_class points2[2];
points2[0] = point_class(1, 2);
points2[1] = point_class(3, 4);

Smalltalk is also somewhat limited:

Complex Literals in Smalltalk
"Arrays of literals are OK"
array := #(1 2 3).
"Other collection types need a bit more work"
set := #(1 2 3) asSet.
"or"
set := Set withAll: #(1 2 3).
"Dictionaries have no literal syntax"
dict := (Dictionary new)
at: 1 put: 'foo';
at: 42 put: 'bar';
at: 123 put: 'baz';
yourself.
"Objects in general also have no literal syntax"
p := Point x: 1 y: 2.
"But the built-in Point class has a shorthand notation"
p2 := 1 @ 2.
"Arrays of objects are verbose"
points := Array with: (Point x: 1 y: 2) with: (Point x: 3 y: 4).
"If your Smalltalk supports brace constructors, it's a bit better"
points := { (Point x: 1 y: 2). (Point x: 3 y: 4) }.

Ruby fares much better:

Complex Literals in Ruby
# Simple arrays are simple
array = [1, 2, 3]
# Other collection types need a bit more work
set = [1, 2, 3].to_set
# or
set = Set[1, 2, 3]
# Hashes also have a literal syntax
hash = { 1 => 'foo', 42 => 'bar', 123 => 'baz' }
# But objects don't
p = Point.new(1, 2)
# Array literals work with any kind of object
points = [Point.new(1, 2), Point.new(3, 4)]

C++11’s new uniform initialization mechanism is much better than C++98:

Complex Literals in C++11
// C-style arrays are still OK
int array[] = { 1, 2, 3 };
// Standard library collections now work the same way
std::vector<int> v = { 1, 2, 3 };
// Maps are now much better
std::map<int, std::string> m = { {1, "foo"}, {42, "bar"}, {123, "baz"} };
// Structs continue to work well
point_struct p1 = { 1, 2 };
// So do arrays of structs
point_struct points1[] = { { 1, 2 }, { 3, 4 } };
// Objects can now use uniform initialization
point_class p2 = { 1, 2 };
// So can arrays of objects
point_class points2[] = { { 1, 2 }, { 3, 4 } };

Because of C++11’s explicit types, we can even go one step further. It is possible to use uniform initialization to create temporary objects to pass to a method or function:

Passing Objects
void f(const point_class& p);
// C++98:
point_class p(1, 2);
f(p);
// C++11
f({1, 2});

Each of these languages provides different affordances for working with simple collection and object types. These affordances impact the kind of code you choose to write in those languages. Smalltalk’s relatively verbose syntax drives different solutions than Ruby’s more compact syntax.

Which is better? I’m torn. I like the cleanliness of Ruby’s rich affordances a lot. It results in cleaner-looking code. I haven’t worked with C++11 much yet, but I expect I will like uniform initialization a lot as I start to work with it more. Smalltalk’s lack of affordances in this area sometimes frustrates me.

On the other hand, I think these affordances make it easier to fall into the trap of primitive obsession. Smalltalk’s more verbose syntax drives me to look for solutions that involve domain concepts rather than built-in basic types.