Julep: Taking multiple dispatch,export,import,binary compilation seriously

This is roughly the equivalent of going onto a Julia forum and insisting that extensible generic functions (so that a method added in one module can be used in a completely independent module) make it impossible to write practical code.

This is not at all what is discussed. On the opposite what is discussed is a (controlled) more
general merging of methods.

1 Like

The current thread began with the proposal to “remove the notion ‘extending’ functions in Base or any other Module.”

You are apparently referring to the function merging thread, which made the opposite proposal of merging distinct functions into a single generic function, changing the fundamental semantics of namespaces.

I would love to get your feedback on APITools.jl.
It’s my own attempt to get out of using/import/export ambiguity hell.

Actually both proposals (taken in all their implications) have fundamentally the same effect on the language/use of namespaces.

Yes. But are you sure that the fundamental pain that drives the start of this thread (even if the proposed design may take the wrong approach) is not the same issue that drives so many arguments here? I am pretty sure it is part of the same general issue.

Separating methods into different generic functions for each module (so that different modules can’t see one another’s methods) is basically the opposite of merging distinct functions from different modules into a single generic function.

This thread is contentious enough without trying to rehash a separate 200-comment thread here.

Scott,

I’ve had a look, but I’m not sure what the intention is. Is this to replace plain old using and import with macros that under the hood do the usual stuff? Also: How would one use what you wrote? The test is a bit sparse, and I believe it only represents the developer half, not the user half.

There are many issues people have that fit this analogy, but this is not one of them. With all due respect, at the end of the ADL Stefan and Jeff finally understood why so many people have been confused here, but I recall you giving up earlier. Please look again around the part Function name conflict: ADL / function merging? - #209 by tkoolen and if things are unclear, there is plenty of patience to clarify because it is so essential. Understanding does not mean anyone expects any changes to the language.

This is a real issue, at the heart of many problems people have with using the language… Is it solvable? Semantically, possibly with no downsides. Implementable? Perhaps it is simply unfeasible to do ADL, and certainly not for a while. But until everyone understands the confusion for people coming from single dispatch languages and those with function overloading, it is difficult to communicate. In the short term, it also helps sharpen thinking on what sorts of macro solutions are less offensive in the long run.

I saw the end of the thread. I don’t find it particularly confusing — in single-dispatch languages, the methods are attached to the object types, but in Julia a function is a separate entity. I’m not aware of any real interest among the core developers in changing the central semantics of the language here.

Certainly there is an education issue in explaining dispatch and generic functions to people like yourself who were confused by a mental model grounded in C++ and similar languages. But saying that it is a different programming model than what some people are used to (I agree!) is not the same thing as saying that it is a fundamental “problem” in the language that may or may not be “solvable” but makes it impossible to write “practical code”.

3 Likes

The intention is to have a lot more control over what the published API of a module or package is, to allow for a distinct “developer” API, and to keep separate the things that are expected to be extended as part of the API, and things that either cannot, or should not, be extended.
I also want to allow for inspection of the API(s), and also for renaming things as they are imported.

I might also add optional “linting” operations, i.e. have the @api macro give a warning, if you put something in your public API that doesn’t have a docstring :grinning:

That sounds interesting. I have tried to address that for myself (https://github.com/PetrKryslUCSD/FinEtoolsUseCase). I have to admit that I still don’t see how what you propose will be used. Any examples?

I know you think that I am bringing up the ADL thing completely out of context, but I am not. Reread the “problems this addresses” at the beginning of the thread, and you will see that these are exactly the same issues that keep popping up. In particular, the problems (1) and (2) are exactly the typical issue of namespaces and type piracy, which I hope I don’t need to repeat.

Problem (3) the poster points out (the difficulty of compiling due to all sorts of merging into the Base namespace) is exactly what is fixed by an ADL based approach. With ADL, noone ever has to stick anything in the Base namespace. In fact, you could even ban it if you wanted for compilable code and it would be completely practical. Furthermore, it would be completely practical for all packages to be written without any usings at all, which would make them easier to coexist. This is how things work in the best example of a compiled programming language with multi-argument generic programming (i.e. C++) that has proper namespaces.

Now, I personally don’t agree with the proposed approach to solving the issues, but this comes down to the same ultimate problem which keeps rearing its head and causing immeasurable “ink” to be spilled. Please take the persistence of people’s questions, the fact that so many posts on discourse end up with the same basic problem, and the fact that there are all sorts of hacky macros proposed as a signal that there is something deeper to be considered between major versions. Don’t assume this is just about educating people unused to working with generic programming and multi-argument functions.

No, that is not the essence of the issue, and I think it is much more subtle and pervasive than you realize. The way that Julia does lookups is not a natural consequence (in any way) of multiple dispatch. If multiple-dispatch is the natural generalization of single-dispatch, then what Julia does is not the natural generalization of namespaces under multi-dispatch. It may have its own self-consistent logic (which apparently is a lisp worldview), but it has nothing to do with generic programming with multi-argument functions.

In single-dispatch languages, the basic lookup rule is that whatever the namespace of the type you use as the first argument determines the relevant set of methods for a given function. The natural generalization of this is that with multi-dispatch, is that the namespaces of the types you have as the arguments determines the relevant set of methods for a given function. This is exactly what ADL does. With that, you never need to mess around inside of the namespaces of other types or the base library, unless you are doing something truly dynamic and odd.

Would ADL break a bunch of existing code? Maybe none. Can ADL this be implemented with a dynamic language and first-class functions? I am not sure, and implementation details matter. But there is a big difference between telling people “this is how just you do things with multi-dispatch” vs. “there are reasons that the obvious generalization of single-dispatch (i.e. ADL) cannot be implemented here” vs. “we understand the perspective and the pain, we will revisit whether alternative lookup rules can fix pain points between major versions, and here is a well-documented macro approach to achieve merging in a way that is a manual way in the meantime”.

3 Likes

Take a look at the code in some of the packages in juliastring.org, such as StrAPI.jl, which sets up the base public and development API for the other packages to use or extend. The usage is of this form:

"""My wonderful module"""
module MyModule

using APITools
@api init   # this initializes the information about this module's API
@api use Format  # this pulls in the public API of `Format` (i.e. what you would have exported)
@api extend StrAPI # this imports or uses both the public and develop APIs of `StrAPI`

@api develop check_string, write_utf8 # these are part of the development/low-level API
@api define_public Str, Chr # these are part of the public API (but can't be extended)
@api public is_unicode, is_bmp, is_latin # These are functions that can be extended for other types

...

"""
Checks if the string is valid (if not already validated)
and only contains characters in the BMP (i.e. 0x0-0xffff)
"""
function is_bmp end

...

@api freeze # freeze API information in tuples, free up temp vectors

end # module MyModule

It’s not merely a “lisp worldview”. Evaluating the function as an object separate from evaluating the arguments, and then applying the function to the arguments, is a property of nearly all languages with first-class functions. (This excludes many traditional static OOP languages like Java and most of C++. C++ and Java have made steps in the direction of first-class functions with limited lambda expressions and () overloading, but e.g. new methods cannot be added to C++ functor objects after they are created.) Take a step back and realize that you are insisting that everyone else is confused because your unfamiliarity with first-class functions made it difficult to communicate with you.

As @jeff.bezanson said in the other thread, even if you have a kind of “ADL object”, you would still need a global namespace of such objects, and furthermore having a method become applicable because its signature narrowed (and thus entered a new type’s namespace) is totally different from the subtyping model of dispatch. I don’t think you grasped the implications of these remarks, especially the latter. What you are suggesting is a complete upheaval in how the language works, not merely an incremental change (excluding add-on packages that emulate ADL by implementing their own lookup tables).

It may be interesting to think about as an academic exercise, but I think you’ve confused people understanding how to better explain the difference between Julia’s dispatch and the ADL you are used to with agreeing that there is something wrong with Julia’s approach (which is common to all all other dynamic multiple-dispatch languages AFAIK) that we need to plan to “fix” in Julia 2.0. Also realize that Julia 2.0 is probably many years away at this point, and planning complete language upheavals (as opposed to new features, optimizer improvements, static compilation, better multi-threading, etcetera) is very far down on everyone’s priority list.

6 Likes

We established later in the thread that this is not the case in C++.

By the way, I think it’s unhelpful (in either ‘direction’) to try to figure out who was more confused.

1 Like

I would be nice if somebody updated that Wiki page and added Julia!
(I’d do it myself, but I’m not always the best person at explaining things :nerd:)

1 Like

Haven’t had time to really try it out, but it looks like a good concept. Don’t think I would ever need to use it personally, but I get where you’re coming from and why you might want it. For me, it would get too confusing after a while to worry about having so many different API’s for the same package. This might be useful for a very large package with a complex API that is used differently in different situations, but I don’t really see a need / use case for it with the average typical Julia package that I am working on. The main issue I see with it is that users / developers will have to make even more choices about what package to use and what API to use, which will require them to research a lot of additional information and make more decisions about what to do. This might be needed in some situations, which is why you probably needed it, but it might also over complicate some things, at least for my own use cases. That’s just my personal take on it, but I will keep an eye on it in the future.

The feature for importing a method with a different name, that’s the most interesting part to me, and might also be one of the most useful from those ideas, to me at least.

It’s really just 2 APIs, the “public” one (what people now export), and the “develop” one, which are things that currently people simply don’t export, and you have to either use/import them explicitly, or put the package name as a qualifier every time you use them.

A good example of that are things like: Base.HasLength, and Base.IteratorSize, they are not public, but if you are developing an iterator, you need to use them, so they are part of the “develop” API.

I’m just trying to make that more explicit, and add tools that let you easily find the API for a package, see what functions, types, constants, etc. that are involved, and choose for yourself if you want to to import those names, avoid conflicts (by renaming, something I plan on adding soon), or simply always use them qualified.

No , to call sum from module B (eval(B,:(sum(V)) you first need to construct an AST to compile, sum is a function(multi method) in B because every module does using Base it is statically resolved to Base.sum and inference continues…

it is then encounters Base.start which is an exported function therefore according the rules I layed out(which may be that better ones exist…) it is resolved to function in its scope (Base) and in the calling scope which is B (remember we called B.sum which has Base.sum as one of its methods)

I hope it is clearer now…

It’s solving none issue. In fact, it makes binary compilation completely useless

I trust your knowledge and experience , but I also trust mine … so in the words of big lebovsky “thats like your’e opinion man”

so you’ll have to compile everything when the user calls it.

yes once, but only once … unless something change in the module it is defined in, just like any .so .dylib .dll out there.

Something you cannot do today with Julia
for example:


julia> baremodule N #just for verbosity
           using Base
           f(x,y) = x+y
           export f
       end
N

julia> using N

julia> f(2.0,3.0)
5.0

julia> baremodule M
           using Base
           import Base.+
           (+)(x::Float64,y::Float64) = 0.0
       end
WARNING: Method definition +(Float64, Float64) in module Base at float.jl:375 overwritten in module M at REPL[4]:4.
M

julia> f(2.0,3.0)
0.0

Regarding your example , yes if you are going to use an implementation, you’ll have to specifically import it.
I know this may seem added verbosity , but it feels more predictable to the end user.