Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Except that C++ doesn't serve that role, C++ is its own whole thing. I think "C but with namespaces and a method call syntax (and templates?)" would be a great language which would occupy a completely different space than C++.


if you introduce namespaces and method calls you have to introduce name mangling, to differentiate

    namespace foo { void x(); }
and

    namespace bar { void x(); }
and then you have to rely on compiler vendors to use the same mangling everywhere otherwise you end up exactly at the C++ position where there are multiple incompatible name mangling schemes, thus C code compiled with e.g. cl.exe would not be able to call a C function compiled with gcc (and FFIs wouldn't be able to either so you loose the "easy language bindings" "feature" of the operating system ABIs)


Namespaces by themselves aren't a reason why name mangling is needed. There isn't a technical reason why foo::x couldn't be the symbol name, literally. (Not sure what ELF and PE/COFF etc would think of these names in current implementations).

Name mangling is needed if you want to overload functions and qualify them only by the types (not names) of their in and out parameters. This is where it gets ugly on the binary level.


> There isn't a technical reason why foo::x couldn't be the symbol name, literally.

how do you do when you want to access your C function from a language which is binary-compatible with C but uses :: for something else? [a-zA-Z_][a-zA-Z0-9_]* identifiers are the only thing that the whole world more-or-less standardized around.

e.g. in fortran you can directly import a C function and call it. But "::" in the middle of a function name would very likely fail (I don't know enough fortran to tell for sure but given how the syntax looks...):

     subroutine foo (a, n) bind(c)
       import
       real(kind=c_double), dimension(*) :: a
       integer (kind=c_int), value :: n
     end subroutine foo
allows to call a "void foo(double *a, int n);" function defined in C. I imagine that

     subroutine ns::foo (a, n) bind(c)
would likely not work


> how do you do when you want to access your C function from a language which is binary-compatible with C but uses :: for something else?

Typically you'd do this by picking a different internal name for the function, and putting the external symbol in quotes.


Fair point. The C naming convention with flat names is quite conservative and consequently allows the names to be used directly from most other languages without any language-aware mapping / compatibility layer.


Rust doesn't allow function overloading. Name mangling is used there to implement linking multiple versions of an external module into the final build.


Although Rust doesn't have ad-hoc polymorphism, it does have polymorphism, and so it needs to track multiple versions of the same function anyway.

Take String::contains(). The equivalent feature in C++ is an overloaded function, so there are I believe it's three, versions of this function which take different parameters: A string, a char and a pointer to chars, they do similar things in practice but the compiler has no idea, there are just three functions with the same name. However the Rust feature is polymorphic, there are N versions of this function depending on what monomorphisations are chosen at compile time. The compiler knows these are all the same function, but the parameters have different types in practice at runtime and so the generated machine code is different. If your program can String::contains(cat_photo_jpg) then the compiler will produce the code to call it with your Jpeg type or whatever and it will need to keep it distinct from the version where it takes a String or a char or whatever.

Rust does this more often than C++ because it cannot choose ad hoc polymorphism. So if there should be a foo function which can take parameters bar, baz or quux, we need to decide either that bar, baz and quux all implement some trait which foo takes, or we need three separate functions foo_with_bar, foo_with_baz and foo_with_quux.


This question is very OT considering the what TFA is about, but…

What does the term ad-hoc polymorphism mean in your comment? Are you saying Rust does not have ad-hoc polymorphism because the syntax acts like the function in parametrically polymorphic and only once monomorphisation occurs are the different functions per argument type are generated is it N different functions. Does this line of thinking also say Haskell does not have ad-hoc polymorphism?

And in case any reader wonders, I am genuinely interested in your response to these questions (i.e. this isn’t bait nor rhetorical).


Indeed; I am wondering why go does not consider traits in rust a form of ad hoc polymorphism?


Ad hoc polymorphism is distinct from parametric polymorphism in that rather than parameter type itself being a parameter, there are just an arbitrary (ad hoc determined) set of types allowed.

The C++ standard library defines contains(x) so that it'll work for a string x, a single char x or a pointer to chars x. Nothing else can work, those are the arbitrary list of types which work, that's ad hoc polymorphism.

A separate implementation is provided for each of those three cases, which is why if I've got a JPEG, I can't instead ask if the string contains the JPEG, that's not one of the three implementations provided.

The Rust standard library just defines the type of x in terms of a trait, Pattern. So, my Jpeg type can just implement Pattern and now I can ask if Strings contain the Jpeg and that works because contains delegates the matching problem to Pattern and Jpeg implements Pattern -- only the specific idea of "contains" as distinct from "begins with" or "split" or a dozen other functions is handled by the contains function.

I am not a (serious) Haskell programmer but I would argue that Haskell also lacks ad hoc polymorphism here as I understand it (and to be clear: I think this is in general a good or at worst reasonable choice)

The place where ad hoc really shines is when you're got a function that say, makes complete sense with exactly two (or maybe three, fewer than two is irrelevant and more than three seems unlikely to provide reasonable ergonomics) specific types, which otherwise have nothing useful in common.

For example suppose I've got a whole variety of bird types, Goose, Chicken, Ostrich, Penguin, Sparrow and so on, and I've got this function thunk() and I realise that, oddly it makes sense to thunk a Sparrow or an Ostrich, but literally no other birds at all. I think hard about it, but the best I can come up with to describe this property is "Thunkable" since all it really means is you can thunk() them. And there's no "content", there's no special implementation work in "Thunkable" that shouldn't live in thunk() for maintenance anyway. In this case ad hoc polymorphism is great because it saves needing to make this stupid "Thunkable" trait / type class / type-of-types / interface just to group together Sparrow and Ostrich for this single purpose.

But I'd argue the "Thunkable" case is rare, and C++ has a lot of cases where ad hoc polymorphism was the wrong choice and they fell into it.

I mentioned three types, really C++ contains() does only two things but it spells one of them two ways for 20+ year old reasons. It can do strings (as std::string, but also via C's char * type) and it can do a single character char (one code unit, so, no poop emoji).

Rust provides four Patterns, they are a string reference, a single char (a Unicode scalar, so yes a poop emoji works), a slice of chars (any of the chars matches), or a predicate which matches characters.

Now, do any of those four feel like things you'd definitely never want in C++? Because if C++ wanted all of them that's now five overloads for contains. And five overloads for find, and for every other matching function, on every string or string-like type...

I believe ad hoc polymorphism is so rarely what you really want, and yet it so often detracts from the rest of the language facilities that "We don't have that" is a sensible language design choice, same as for multiple inheritance.


I think my issue is with your definition. Type classes in Haskell are the implementation for ad-hoc polymorphism, and likewise traits in Rust. I think the definition you are using is not the commonly given one and that is where my confusion came from.

Typeclasses (in a Haskell context) we’re formalized in the Wadler and Blott paper “How to make ad-hoc polymorphism less ad hoc”. And for Rust, in [1] Traits are explicitly stated to be the method in which Rust achieves ad-hoc polymorphism.

1: Klabnik, Steve; Nichols, Carol (2019-08-12). "Chapter 10: Generic Types, Traits, and Lifetimes". The Rust Programming Language (Covers Rust 2018)


I didn't know either of these things, I guess now I have some reading to do, so thanks.

Edited to add: Hmm. Actually though, surely that second reference is just "The Book" as it's called, did it actually say it's about ad hoc polymorphism? Because I've read this section of The Book, although I hadn't in 2018, and it doesn't mention "Ad hoc polymorphism".

What's there now (I just checked) is a description of how you'd approach this problem in Rust, using traits, but doesn't claim this is ad hoc polymorphism, and sure enough doesn't involve an arbitrary set of types which is the sort of the point of why "ad hoc" is there in the name.


The standard definition of ad-hoc polymorphism imputed to Strachy, where:

Strachey chose the adjectives ad-hoc and parametric to distinguish two varieties of polymorphism [Str67]. Ad-hoc polymorphism occurs when a function is defined over several different types, acting in a dif- ferent way for each type. A typical example is overloaded multiplication: the same symbol may be used to denote multiplication of integers (as in 33) and multiplication of floating point values (as in 3.143.14).

That is from the Wadler paper where typeclasses are formalized. Typeclasses and Traits are the implementation details for those function symbols that vary in implementation for each type. The restrictions on the types (like which types implement a trait or have a type class defined for it) are the types the symbol can be used on.

You seem to be focusing on the ‘arbitrary set of types’ point, but the only connection between the types accepted by Rust’s generic functions (which are functions that accept a type provided it has some trait) are that they take types which have an impl for that trait.

I think there is a bit of ambiguity regarding the term ad-hoc polymorphism at play. You seem to think the trait/typeclass implementation of ad-hoc polymorphism (which was invented to formalize a well behaved class of ad-hoc polymorphic functions) makes it no longer ad-hoc. My position echos Wadler, it’s still ad-hoc but just less ‘ad-hoc’ (i.e. more formalized).


It comes to this phrase about a function "acting in a different way for each type".

In C++ there are literally three separate implementations of std::string's contains method for three type signatures. This is pretty clearly what is being discussed as "acting in a different way".

In Rust there's just one, here's the entire function body of contains: pat.is_contained_in(self)

OK, well that's just buck passing right? Clearly this is_contained_in() method on Pattern is really just the contains() implementation, we're passing the work to this function that as you point out needs to be implemented by each of the matching types for Pattern.

Except, wait, Pattern actually defines is_contained_in(haystack), thus: self.into_searcher(haystack).next_match().is_some()

Sure enough Pattern implementations although they're not forbidden from implementing is_contained_in themselves, do not in fact do that, they just implement into_searcher. Our hypothetical Jpeg type can provide a suitable into_searcher implementation which results in a Searcher for the Jpeg somehow, without knowing what contains() or split_once() or trim_start_matches() do, and now they will work on Jpegs.

So the "acting in a different way for each type" for contains() ends up only being because of details about the inner behaviour of that type, which is exactly parametric polymorphism so far as I can see.


Rust breaks parametricity via functions like size_of and in unstable features such as specialization.


You do realistically need a kind of name mangling yes, if you want to keep using [a-zA-Z0-9_] as the set of characters in symbol names. But without overloading or other fancy stuff, you could have a super simple name mangling scheme: separate the names by a double underscore "__".




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: