Channels ▼

Walter Bright

Dr. Dobb's Bloggers

Uniform Function Call Syntax

March 28, 2012

Over my years of programming, I have become more and more convinced that the key to code reusability and scalability is encapsulation — having subsystems communicate through small, well-defined interfaces and letting them hide their own implementation details. I know what you're thinking, "Did he fire 5 shots or 6?" Er, scratch that, you're thinking "Duh!" and "Join the party!" Please indulge me. I think you'll find the payoff worthwhile.

Language features that make encapsulation easier or more effective are good things. Designing proper abstractions (and their corresponding encapsulations) turns out in real life to not be as straightforward as various texts and articles would have you believe. The community's understanding of encapsulation has improved over the years, and consequently the related features frozen in programming languages may sometimes seem slightly outdated.

Let's consider just one encapsulation feature we all know and love — the class. A class encapsulates data (fields) and the functions (methods) that manipulate that data:

    module mylib;
    class C {
        void set(int x) {
          enforce(x >= 1 && x <== 100);
          this.x = x;
        }
        int get() {
          return x;
        }
      private:
        int x;
    }

That's about as simple as it gets. But then, we often find it reasonable to add more methods that manipulate the data:


    class C {
       ...
       int square() {
         return get() * get();
       }
    }

And an interesting thing starts to happen: We don't know when to stop adding methods. The list of required primitives tends to be rather short and stable, but the list of convenient, nice-to-have methods tends to go on forever. At the same time, such methods are quite desirable. Some usage patterns of primitives are frequent, and many consider the syntax


    a.method(b,c)

better than

    function(a,b,c).

The class tends to get larded up with all kinds of methods. After all, we cannot anticipate every way someone might use a class, so just in case, we throw in the kitchen sink. After all, someone using the class may not be able to modify the class declarations in the import files supplied.

The next stage in our thought process is "derive from class C, and add those extra methods there!":


    class D : C {
       int square() {
         return get() * get();
       }
    }

OK, but now every instance of

    new C();

must be found and replaced with:

    new D();

and of course this falls over completely when Fred derived D from C to add method square, and Bill derived E from C to add another method cube, and both methods are needed. (Of course, there's the dreaded multiple "diamond" inheritance, but I think there's a better way. Read on.)

Scott Meyers recognized this problem back in 2000, and wrote an insightful article How Non-Member Functions Improve Encapsulation about a solution — use free functions instead when they can be implemented as non-friends. (His arguments are compelling enough that I won't even try to improve on them.) Free functions in the D programming language would look like:


In fred.d:

    import mylib: C;
    int square(C c) { return c.get() * c.get(); }

In bill.d:

    import mylib: C;
    int cube(C c) { return c.get() * c.get() * c.get(); }

And it would be used like:

    import mylib: C;
    import fred: square;
    import bill: cube;

    ...
    auto c = new C;
    ...
    auto s = square(c);
    auto b = cube(c);

and that works. But there's a problem. We'd really prefer using

    c.square();

instead of


    square(c);

After all, there's the matter of consistency. When there's both:

    x.toInteger();
    toString(i);

it just seems arbitrary. Chaining methods like:

    toString(x.toInteger());

looks wonky compared with:

    x.toInteger().toString();

But it's more than just cosmetic. A template may rely on structural conformance, meaning it is looking for specific methods. An example of this would be an input range (from std.range), which looks for the methods empty, popFront, and front. There's no way to add that functionality to an existing class without cracking it open and inserting them, and input ranges don't recognize free function versions. For built-in types, most languages do not allow adding methods.

I hope you've borne with me so far, because here's the payoff:

Uniform Function Call Syntax

New to version 2.059 of D (and implemented by Kenji Hara) is the notion of uniform function call syntax. It's been around in D since the beginning in a nascent form for arrays, but it's now available for all classes and structs. If the compiler sees:

    c.square(args);

and square is not a member of class C, then it looks for a free function of the form:

    square(c,args);

That's it! Now it's easier to follow Scott Meyers' advice to minimize the number of methods in a class to just those that need to access its private state. Implement the rest as free functions. Methods can be "added" by third parties without changing the original class definition. Additions from multiple third parties can be used simultaneously. And payment will only be made for the methods that are actually used. Hopefully, this helps spell the end of "fat" and "kitchen sink" classes.

Alternatives

A different design to address this problem is called Extension Methods. Extension methods differ in that they require a special syntax to differentiate them from regular functions, and they may only be called using the infix notation; i.e., c.foo(args). The D design does not require a special syntax, and the methods may be called with either the infix notation or the prefix notation; i.e., foo(c, args).

C++ STL algorithms avoid this problem by standardizing on the non-member function call syntax. They take iterators instead of containers, so they never call container member functions, and because iterators may actually be pointers, they never invoke member functions on iterators. Function objects are invoked as if they were function pointers, which they may in fact be.

Only time and usage experience will tell whether D's approach that allows both c.foo() and foo(c) to coexist, C#'s approach to only allow c.foo(), or C++ STL's approach to only allow foo(c) is superior.

Acknowledgments

Thanks to Scott Meyers for his helpful suggestions on this, Andrei Alexandrescu for repeatedly explaining it to me, and to Kenji Hara for implementing it for D, and to Eric Neibler, Bartsoz Milewski and David Held for their insights.

Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 


Dr. Dobb's TV