>> I received email accusing me of “selling out to the OO crowd” and people whose experience and opinion I respect insisted that x.f(y) finding f(x,y) would seriously compromise their ability to design stable interfaces.
I think the concern is that a class "owns" its methods either directly or through inheritance. Therefor, the interface is expressed by this namespace and it can't be "invaded" or modified. From that perspective, having f(x,y) being callable as x.f(y) would be a violation of your interface.
But... the dot syntax does not make an interface, and neither does it make "object oriented programming". It implies that "f" is a specialized function for somehow applying "y" to "x".
If you have a generic "f" that can be compiled to a specialized "f" for your type... you are just adding composability to your language. That's a good thing! Calling "f" as x.f(y) in this case makes a lot of sense. It's just another specialized function for applying "y" to "x".
The paper "Unified Call Concerns" (linked at the end of the post) describes the issue in a bit more detail: you might write f(X x, Y y), knowing that X doesn't have a member function f, and call it with x.f(y). If a new version of the library extends X's API to include the member f(Y y), your code will suddenly start calling the member instead of the original non-member function.
As he mentions in that paper, this isn't likely to be a common issue, so I'm with the general consensus that it seems unfortunate that the feature wasn't adopted.
For a class X, all functions, including free functions, that both
(a) "mention" X, and
(b) are "supplied with" X
are logically part of X, because they form part of the interface of X.
So yes, f(x) is part of x's interface in C++, and has been considered to be so for a long time.
Please, please, please reconsider not enabling x.f(y) resolve to f(x, y).
You must explain to the detractors that they must reconsider allowing x.f(y) to resolve to f(x, y). This is not a selling out to OO, but in fact the opposite! Allowing x.f(y) to resolve as such enables us to finally get _away_ from OOP by using an alternative style called 'Data Abstraction Style'. I have written up an example of this style here - https://github.com/bryanedds/das
In PLT terms, data abstraction is the dual of OOP. In fact, I use it significantly in F# as a way to do pure functional programming where others just fall back into OOP - https://vimeo.com/128464151
Data Abstraction Style with resolution of free-standing functions to dot syntax gives us the best of both worlds - the increased modularity and extensibility of free-standing functions as well as the nice tooling and API explore-abily of the dot intellisense.
Finally, this syntax is important just to allow extension methods without a more specialized syntax that won't likely appear anyways.
Please pass along this information to the people holding out on allowing x.f(y) resolve to f(x, y) - it is not selling out to OOP - it's an elegant path to finally move beyond it. People must be made to understand this before making their final decision!
I have a pet language, in which `f.g(x)` is merely syntax sugar over `{typename of f}_g(f, x)`. It's a combination of two ideas - the colon operator for method calls in lua, and extension methods from C# &c.
I think this debate is a small part of a larger one about unified namespaces, and for functions in particular. There's no good reason for a named function to coexist with a like-named function variable. Javascript gets this a right by having `function f` largely equivalent to `var f = function`.
I'm impressed that C++ continues to evolve 37 years later. It's a stark contrast to, say, the Go 1 Compatibility Promise.
C++ tries to make the same guarantees that the Go 1 compatibility promise makes. (Or at least very similar; they are different languages after all.) Basically: old code should keep working possible, but they have the right to invent new features. The real difference is that C++ is feature-philic (new feature: want!), while Go is feature-phobic (new feature: avoid!)
Of course, if you want to make specific predictions about which features C++ or Go will adapt, then you need to look at details. For instance, Stroustrup tried hard in the early days to ensure that user-defined types are on equal footing with built-in types, while Go tried hard in the early days to make sure that very common foundational types are built in and have good support. Thus today the C++ standard library has two at least two distinct dictionary types, while Go has one built in to the language. (Someone who actually knows more about Go history than I do, please correct me if I'm wrong.) I tend to favor the C++ approach as more mathematically pleasing, but I can't deny that the Go approach leads to a simpler language. (The real question is: which one leads to better programs, and I suspect the answer is "it depends, on way too much stuff to even list".)
There are several dictionary types in C++, but each has different performance characteristics and they all have good support. Thanks to the efforts to make built-in types and UDTs equal, Qt's map was for instance implemented as a skip list and was just as usable as the standard library types. Nowadays it's a RB Tree.
Go's strategy of picking hash maps and not offering the mechanisms of building your own data structures with the same syntax affordances and performance characteristics makes it easy to jump in and hard to make progress if you hit a wall with the built-ins.
Yep. I guess they're counting on that being so rare that it's worth paying the price in occasionally having to rewrite (and uglify) large chunks of code, in exchange for having a simpler language.
In my language f.g(x) is syntactic sugar for g(f, x). Prefixing with the typename of f is not needed due to function overloading. (Which basically does the same thing I guess)
Interesting! Do you have separate symbol tables for functions and variables, then?
I've taken the major design decision that there's only one symbol table for variables, so every function is a functor. I'm not sure it's possible to reconcile this with function overloading (Go/Javascript don't)
EDIT: i suppose you could have a different symbol table for each variable type, incorporating function arities.
I don't really use symbol tables in the compiler.
But with function overloading, binding needs to be done by name and type, yes. It does complicate the binding process significantly.
Yeah, I'm really disappointed. It's one of my favorite things about D. I guess f(x,y) finding x.f(y) is better than nothing but the other way around makes for some beautifully composable code.
I like the proposal. It took me 20 years to master the 10% of C++ I understand and use. I wonder how new programmers will be able to pick up this language.
I thought of Nim, too.
To add to your comment, Nim's "Unified Call Syntax" recognises as the same len(x), x.len() and also x.len (this is similar to Ruby's poetry mode, if I am not mistaken). I find it cool, but I can also imagine reasons for which the latter would not be appreciated by the C++ community.
I have to agree with x.f(y).g(z) vs g(f(x,y),z)...jquery or d3.js would be terribly obtuse with the latter only.
But there are times when the functional notation is nicer than the OOish style. Really, having "both" is the right answer for libraries, although it might not always be possible. I suppose this proposal makes it easier for libs to do "both".
Also, can someone clarify for me, even though he mentions multimethods, these aren't really multimethods, right? Since f(x,y) becomes x.f(y), I'm assuming there is polymorphism involving the runtime type of x, but not y too, or is C++ getting true blue runtime multimethods?
I'm curious, would C++ ever consider categories like in ObjC? I.e. class extensions that unlike inheritance don't change the type.
It kind of gets close with the unified call: if you have a call f(x,y) then either x.f(y) is matched or some f(x,y) that may be defined elsewhere. But still not the same as the ability to define a new x.g(y) for an existing class.
(Edit: I can't even start to think what implications this will have on templates. They will probably become even more impenetrable.)
Their proposal to allow x.f(y) to match a free function f(x, y) would obviate the need for anything like categories. I'm puzzled and saddened that this part of the proposal didn't go through.
>C++ provides two calling syntaxes, x.f(y) and f(x,y)
I have to shamefully admit, I have no idea what this is referring to. In the second one, is this talking about the implicit "this" pointer that is passed to member functions?
I've never seen or written code like the second form -- if in fact f() is defined as taking only a single argument, I've never seen it called with 2 arguments.
My guess is that this is a discussion about the implementation of member functions, can anyone confirm?
Under the covers, `x.f(y)` is actually something like `_ZN1X1fEi(&x, y)`, where the mangled name indicates the class of X and the type of y. The implicit 'this' pointer is actually explicit in the implementation. Similarly, a call to `f(&x, y)` would actually be a call to `_Z1fP1Xi(&x, y)`, with similar reasoning.
The author is proposing that writing
f(x, y)
Which would normally only bind to the second mangled function, would bind to the first mangled function if the second is not available.
The additional proposal, that:
x.f(y)
would work in the other direction was rejected. That's a bit of a shame, as this kind of duality is powerful in languages that support it (e.g. scala).
This is also why functions can work on a null object and not crash, as long as they do not access instance data. Though at that stage they should be static or external.
AFAIK, this is still undefined behavior, so "can work" means "you got lucky this time, your next compiler upgrade may turn this into a crash, security vulnerability, or utterly confounding Heisenbug".
Ugh, I remember seeing null `this` values used on purpose in some old Windows MFC program, or maybe it was COM...
I imagine the author was an old-school C programmer who understood well how the C++ object veneer maps down to equivalent C code / assembly. From that perspective, dealing with null `this` pointers is just another way to get the compiler to spit out the assembly instructions you want, and maybe enables some elegant patterns in client code... but it's exactly the kind of thing that makes newcomers hate C++.
Based on the rest of the original paragraph, I understand he refers to the fact that you have to choose either one when desigining a library, and then you're stuck with it (and its various consequences) in your API for better or worse -- and your library users are stuck with it too.
In other words: if you design a polygon clipping library, do you design your API so that users would write `polygon1.clip(polygon2)`, or `clipper::clip(polygon1, polygon2)`?
edit: also, from reading further down, I understand the discussed issue is that after you define the API as `polygon1.clip(polygon2)`, you can't easily use the 'clip' method in places where a f(a,b) would be required, although it's effectively equivalent. And it's impossible the other way round too.
struct Foo { void Bar(int) {} };
std::function <void(Foo&, int)> f = &Foo::Bar;
int i;
Foo foo;
f(foo, i);
Which is confusing - you'd expect it to work like maybe 'foo.f(i)' or maybe 'foo.*f(i)' or something like that. Well the whole discussion (from what I understand) is what that 'something' should be (well I don't mean to imply that this is about how std::function should work; rather that this is one explicit manifestation of an issue that shows in several places).
I didn't go for the equivalence of (f x y) and x.(f y) because it's not a CLOS-like object system. It's single dispatch, and a method is actually a slot. Thus x.(f y) semantically corresponds to (call x.f y), and I don't see any value in making (f x y) a synonym for that, only harm.
It would complicate how function calls work. If global function f exists, x.(f y) still calls the one in x, unambiguously. for (f x y) to do that, the function lookup for f would first have to check that x is a struct object, and whether it has a slot called f, and then use it. Then otherwise fall back on behaving like a normal function call. That's just too much semantic cruft jammed into function calls.
The x.(f y) (f x y) equivalence only makes sense if you have a CLOS-like object system with generic functions that are separate from classes, so that f is a generic function named in the ordinary function namespace, which, when called, dispatches methods. Or a completely static language in which it is worked out at compile time whether f is an ordinary function or the member.
One of the silly things about member functions is the effect that they have on argument ordering when ordering is not supposed to matter.
For example, consider "operator +", which can have in-object or function implementations. The ridiculous thing is that, for any two types in an expression like "+", you can't tell from observation if it will compile! Is it only available as a member function? If so, then any primitive left-hand argument in this supposed-to-be-commutative expression must be swapped to the right side, or wrapped in a pointless object. The same problem can exist for non-operator functions.
At least now with "auto" and other implicit typing (and even tuples and std::get<needed_type>), there is a chance to write single functions that work around messes such as these.
> One of the silly things about member functions is the
> effect that they have on argument ordering when ordering
> is not supposed to matter.
This isn't a problem with member functions, free functions have this problem as well. And not just in C++, commutativity just isn't a first-class concept in any programming language that I've ever seen.
My main point is that there is no way for something like "2 + object" to do the right thing when member-function syntax is expected, whereas there is at least a way for a free function to do so; and the language could have avoided this problem.
I believe this is also the entire reason that recent C++ standards introduced free-function forms of things like begin(thing)/end(thing)/etc. because it was just too hard for something like a template to operate on a value when it couldn't tell whether or not object syntax would work.
I like C#'s approach where a static function can be called as a method by adding the "this" keyword to the first parameter in its declaration. This makes it opt-in, which is annoying for legacy code. All collections got a fantastic map/filter/fold system bolted on through this mechanism, and as Bjarne noted the dot notation is great for chaining calls.
So in C#, to declare an fn as an "external method" you say:
static void MyFunc(this MyClass myObj, int someParam){doStuff();}
Which can be called as
var foo = new MyClass();
foo.MyFunc(5);
The downside is that method not found errors become much more complicated and it's not obvious to the user whether a method is coming from a different package or not.
What a shocker. Allowing f(x,y) to find x.f(y) is nearly useless; and allowing x.f(y) to find f(x,y) is scrapped. Even though D can do the latter just fine.
I can already write all my class functions as f(x,y) functions, if I want my code to look like C.
The whole point of UFCS is to work around the obnoxious fact that classes are closed after their initial definitions(*). Only the latter can help with that. So when I use a string library that has a reverse function, I can say string.reverse(), but when it then is missing a lowercase function, my only option is to create lowercase(string) myself. And now my usage of the string class is a mixture of global function versus member function calls. No consistency at all. And when I start trying to chain them, it turs into a complete disaster, eg:
And this is why library writers end up trying to pack the kitchen sink into their classes. Or you end up with things like std::string that, out of the box, are basically just glorified memcpy classes (no trim, no transform, no string replace, no tokenization, no parsing [eg string->int], etc etc.); and thus you end up with countless people making their own string classes anyway.
UFCS could have even allowed extending built-in types in extremely useful ways. I could have declared "BitRef int::bits(int lo, int hi)", and allowed for eg "int x; if(x.bit(7)) x.bits(2,3) = 2;" instead of "if(x & 0x80) { x |= 0x08; x &= ~0x04; }" -- that may look a bit weird, but anyone who's done a ton of bit-twiddling will certainly appreciate the potential there for cleaner code.
I really hope D can get the GC completely out of the standard library and get a command-line switch to disable it. Then I can seriously consider switching languages.
(*) we could actually solve this problem with external class functions, but I'm sure they're even less willing to go for that. But imagine if you had:
class Integer {
...
int square() const { return x * x; }
int self() const { return x; }
private:
int x;
};
extension int Integer::cube() const { return self() * square(); }
int main() {
Integer x = 5;
print("{0}, {1}\n", x.square(), x.cube());
//prints 25,125
}
The idea would be that your extension can't access the private/protected state (unless the function is declared a friend of the class.)
You still have the ability to extend classes with useful functions as needed, and the encapsulation principles aren't violated: no internal state can be accessed, the functions can't be virtual so you won't need to modify the vtable definition, etc.
I know they don't have algorithms are part of the string class in C++ but that's because you can just treat it as any other container and run functions against it, like you would on a vector etc.
I think the concern is that a class "owns" its methods either directly or through inheritance. Therefor, the interface is expressed by this namespace and it can't be "invaded" or modified. From that perspective, having f(x,y) being callable as x.f(y) would be a violation of your interface.
But... the dot syntax does not make an interface, and neither does it make "object oriented programming". It implies that "f" is a specialized function for somehow applying "y" to "x".
If you have a generic "f" that can be compiled to a specialized "f" for your type... you are just adding composability to your language. That's a good thing! Calling "f" as x.f(y) in this case makes a lot of sense. It's just another specialized function for applying "y" to "x".