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.
 

Comments:

ubm_techweb_disqus_sso_-4ec011f0d46bf5b98431572f26bac3e0
2014-01-05T23:21:13

The bigger issue would be if something like auto-complete were ever to be implemented. With a string literal you'd type . and then see a list of functions that don't make sense as member calls (like files that download from a url, or read from a file, or really anything that takes a string to specify something rather than operate on it)


Permalink
ubm_techweb_disqus_sso_-4ec011f0d46bf5b98431572f26bac3e0
2014-01-05T23:18:18

C# 6 will have free functions in the sense that you can use a static class and you can then use the static methods as if they were free functions.

Really the only difference between C# and D's implementation is that C# requires you to explicitly say that a function supports the member function syntax, which is better IMO as it doesn't cloud the suggestion list with tons of functions that don't make sense to use as member functions (I'm thinking something like File.ReadAllText, which would be weird to see after a string)


Permalink
ubm_techweb_disqus_sso_-71b657111f3a5deebf4dce3ee24d797e
2012-04-06T01:51:11

Have you considered implementing it the other way around as well for fully uniform notation? I.e.

a.f(5);

should call

void f(A, int);

if no f() method matches the call.


Permalink
ubm_techweb_disqus_sso_-929e8af566e8b3bbbe3deaee8fff3288
2012-04-04T14:47:49

How does this cooperate with a hierarchy of classes? Can it do virtual invocation?


Permalink
ubm_techweb_disqus_sso_-90855f7f659097331b7b710ce8232f9a
2012-03-29T17:28:24

Extension methods in C#/VB.NET can be called either with infix or the standard style for a static member function. Since C# doesn't have free functions it is essentially equivalent to the D syntax (or as close as it could be without free functions).


Permalink
ubm_techweb_disqus_sso_-2556466c8ce184cdf24445ef81266396
2012-03-28T16:34:51

Well, the function has to match the object type in its first argument as well, so to me it is not that different from dispelling your method and calling another method already in the class.


Permalink
ubm_techweb_disqus_sso_-826ad3d60cb32366c17900d34c93db85
2012-03-28T15:52:47

I see a pitfall: If you misspell a method, and your misspelling happens to match the name of a free function, you get a surprise.


Permalink


Video