Function name conflict: ADL / function merging?

Yes, that makes sense (and is not a bad thing in my view).

Exactly. Also, in practice generic programming in C++ is very similar to Julia. Developers will have types inherit from structures without any member functions (or using them like traits) in order to dispatch to the correct generic function. It also uses ADL to do something much more serious than Julia for generic programming:

Think of Concepts in C++ as taking your “meaning” embedded in a function to the next level, by formally specifying the requirements of it. So, while Julia currently has convention that a set of generic functions and types inside of a namespace should have the same meaning, but C++ would actually formalize those requirements. Then, instead of just implementing * +, etc. for your type, associating it with the right namespace, and letting duck-typing do the rest, you would formally specify that it implements a particular generic interface (the concept). Then generic functions operating on those types would dispatch to functions based on the concept itself.

Think of this as a formalization of exactly what a Number or AbstractReal in Julia means in terms of generic functions, where a type (in its own namespace!) would implement all of the generic functions, and be dispatchable to any generic code (in a separate namespace!) using the Concept. Without having ADL the only way that works is if everything is in the same namespace.

I think that suggesting concepts in Julia is close to the last thing you want to do, but it is very valuable to understand how they work in C++ and how it interacts with ADL.

Great! This explains why we were having so much trouble coomunicating, and why @Tamas_Papp (correctly) told me I was ignorant and needed to understand CLOS.

If the worldviews are incompatible (and they may be!) then part of it is helping people form the correct mental model of the language. But my guess is that people from C++ and any single-dispatch languages (Java, Matlab OO, etc) will be coming in from the non-lisp worldview. In particular, anyone used to class-based object orientation thinks of the available functions as being known to the types they are calling it on.

Regardless of whether the mental model for Julia function lookups should change in v2.0, is there a value in making life easier for those coming from the alternative mental model at this stage?

2 Likes

I was told that XModule.f(x::Any) (or f<T>(T x) ?) would not be callable, but XModule.f(x::X) would be. A method becoming applicable as a result of its signature narrowing is definitely very different from the subtyping-based model of applicability.

1 Like

I am not sure… The ADL rules for generic types in C++ can get tricky, and I am out of practice. Also, C++ doesn’t have Any, which Julia does, and which might either make the rules more sane or more insane. Regardless, we are not saying that C++'s ADL approach is exactly the right one or directly transferable.

Same here; it’s possible that my statement regarding visibility of f(x::Any) was incorrect. I’m off now, but I’ll try to get up to speed on C++'s ADL rules regarding templates later.

:+1: I hadn’t really evaluated my own worldview, but now I can see how (even after years of Fortran, PL/I, Basic, etc. beforehand), 6.001 (MacLisp & Scheme) formed my conceptual view of the world for all time to come.

Seems like this would be good to have emphasized in the documentation, to help the hordes of people coming from different worldviews to Julia in the future.

1 Like

How about putting all imported/using methods into a single method table in Main? Method should have field module so it can distinguish conflicting signatures by module name. Dispatch would ignore the module name unless the signature is ambiguous. This way methods can be dispatched based on the argument type signature without having to qualify the function name. As a result it will be possible to extend methods of any module. If the argument type signature is unambiguous it will just work regardless of which module defined the method.

Ambiguous parameter type signatures require a function name qualifier. A statement like const Main.function_name = some_module.function_name could indicate the (default) module name to be used if no qualifier is specified for that function name.

Also, add the word “as” to import and using statements, for example using AdaptiveRejectionSampling as ARS. Short qualifiers make for more readable code.

Let me preface this by saying that if all that comes out of this whole discussion is a thorough understanding of why ADL does work for C++ (for some definition of ‘work’) while it doesn’t make sense for Julia, I think this whole discussion, or at least parts of it :slight_smile:, will have been very worthwhile. But I think there is hope for an ADL-like approach in Julia in the sense Jeff described:

I was definitely wrong in this comment, for any sensible interpretation of an Any type analogue in C++.

First, the interpretation of f(x::Any) as a function template. The following program compiles:

namespace XNamespace {
    struct X { };
    
    template <typename T>
    auto f(T) {return 1;}
}

int main() {
    XNamespace::X x;
    f(x);
    return 0;
}

Second, if Any is treated as the universal base class, we get

namespace Base {
    struct Any { };
}

namespace XNamespace {
    struct X : public Base::Any { };
    
    auto f(const Base::Any*) {return 1;}
}

int main()
{
    Base::Any a;
    f(&a); // error: 'f' was not declared in this scope (because Any is not defined in XNamespace)
    
    XNamespace::X x;
    f(&x); // fine
    return 0;
}

In a way this is a good thing, since I agree that the issue Jeff raised here about a function becoming applicable as a result of signature narrowing would otherwise be a showstopper.

By the way, this account by Koenig lookup’s (ADL’s) namesake makes it sound like ADL was originally added for reasons of backwards compatibility (figures…) at the time of the introduction of namespaces in C++. Just a historical note; this is not meant as an argument against ADL.

4 Likes

I’ve move this thread to #dev since it has gone well past the original usage question and into language design discussion.

Is it worth starting a new thread, to isolate the design/short term fixes from namespace shaming and “clash of civilizations” confusion?

Yes, I think this is the reason to think that it may yield some lessons for Julia. I may make a mistake here, but the story goes somewhat as follows: In the evolution of C++, it started with operator overloading but no namespaces. Of course, operators are a special type of function, because requiring namespace qualification is impractical. When namespaces were proposed, it became clear that operators would become completely unusable if they were segmented into separate namespaces unless either: (1) the user shoved everything to do with any user of a particular operator into a single, shared namespace; or (2) a way to segment operators (and, consequently, functions) in separate namespaces associated with the types they operated on, with some sort of lookup from the type to the appropriate namespace for functions. So, in order to avoid a bunch of manual hacks around the namespaces, they formalized the second approach in (2) and slowly figured out all of the insane number of specialize cases given C++'s mongrel roots.

I think this is exactly the point that some of us are at now, but luckily we don’t need to worry about Julia being a mongrel- so a cleaner set of rules is possible.

For those not used to reading “modern C++” these examples may look quite odd. It is important to understand that “Modern C++” largely avoids OO techniques such as inheritance, and instead is much closer to Julia’s generic style than you might guess. In fact, inheritance of virtual functions is completely incompatible with C++'s generic programming - so you have to choose either OO or templates. There is much disagreement, but many of the giants of the language (such as Stroustrop, Stepanov, etc.) have moved much more towards the generic programming style.

Concretely, what this means is that when you see “inheritance” in C++, it may mean old-school OO or it may mean subtyping in the Julia sense. C++ developers figured out that they can use inheritance from structures without fields or member functions as a hack to implement what is builtin to Julia. So, let me try to roughly annotate the example from @tkoolen with some Julia code, which may help understand the mapping:

namespace Base {
    struct Any { }; # julia> abstract type Any end
}

namespace XNamespace {
#julia> import Base.Any

    struct X : public Base::Any { }; #julia> abstract type X <: Base.Any end
    
    auto f(const Base::Any*) {return 1;}  #julia> f(Base.Any a) = 1
}

int main()
{
   #julia> import XNamespace.X
    XNamespace::X x; #julia> x = XNamespace.X()
    f(&x); #julia> XNamespace.f(x) #Right now need qualification in julia
    return 0;
}

Note that the sample mapping above is without any using and in particular without a using Base.

Let me add: if there is a special case for Julia to make it practical to write using-free code, it may be the Any. In particular, an implicit using Any, where the Any might better belong in a single, global namespace that is shared with unqualified REPL functions… but that sort of thing could come directly out of an attempt at an ADL-based design.

I think that C++ has something similar here in that the intrinsic C++ types (e.g. int, float, etc.) are not in a library, so they required special cases. Julia requires, at most, one special case.

2 Likes

Created a new package called ForceImport which can use the @force

This allows you to force import an external module with conflicting methods into a Module / package.

Based on the dicsussion here, maybe some of you might find it useful. Use wisely, may the force be with you

Essentially, you can safely extend conflicing functions with this, without overwriting the global methods.

3 Likes