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

We realize that, we also know that it gives warnings that you really can’t have if you are writing production software.

Because you used fun with the other meaning first.

I see. Thanks. But insisting on people not using functions in any particular order is not very practical, is it?

Why are you writing production software that uses a name with one meaning and then does further using statements? I can understand that at the REPL but why would you do it in production code?

Once a name has a fixed meaning it cannot be changed. Using it at a point where the meaning is unambiguous resolves that meaning. This is inherently temporal. The recommended pattern in programs is to put the imports at the top all in one place. Then you’ll never encounter this issue except at the REPL.

2 Likes

Oh, excellent explanation! Thanks.

2 Likes

No, I don’t, this may then have been a misunderstanding coming from running into these warnings frequently at the REPL.

A little further explanation. If a name is already ambiguous and you try to use it, then it is simply undefined. If you use a name when it’s unambiguous, that resolves its meaning. We can’t predict that you’re going to later do something that would cause it to become ambiguous, so the best we can do at that point is warn you.

The current design has the property that if you have code that works without warnings or errors and if you reorder the imports and it still works without warnings or errors then it does the exact same thing. That seems like a fairly important property to have. Combined with the requirement that Julia code be evaluated one top-level expression at a time, I don’t think there’s any other design we could have with this property. In older Julia versions the order of usings was significant which is not great because people tend to reorder them.

10 Likes

I think it would be great if this explanation plus this example was part of the documentation in https://docs.julialang.org/en/latest/manual/modules/#modules-1

Perhaps I missed it, but I couldn’t find an explanation of this and therefore I had an incorrect picture of how to resolve conflicts between imported names.

7 Likes

Yes, it would be good if some of these more in-depth explanations were added to the documentation.

This line looks rather confusing to me:

The import keyword supports the same syntax as using, but only operates on a single name at a time.

given that import module: a, b, c is correct, and it is operating on 3 names.

I for one did not know this, and agree with @PetrKryslUCSD that a documentation update would help a lot.

@rdeits made another suggestion to me in person regarding MethodErrors, and I’m hoping he’ll post it here, as it addresses another possible frustration.

Yet another part of the (at least perceived) problem I think is due to method ambiguity errors when several packages define their own ‘scalar-like’ types (and thus overload operators like +, *, etc.) that all subtype Number. But this may be solved in the near future by Cassette, JuliaCon 2017 | Mixed-Mode Automatic Differentiation in Julia | Jarrett Revels - YouTube, (or slightly earlier in the talk, JuliaCon 2017 | Mixed-Mode Automatic Differentiation in Julia | Jarrett Revels - YouTube for more context), so that there will just be less overloading of operators in general.

3 Likes

I’ve filed an issue about adding this to the docs somewhere: https://github.com/JuliaLang/julia/issues/27130.

2 Likes

Yup, I wrote it up here: Making method errors more helpful by indicating the defining module

1 Like

I didn’t follow all of this thread, so apologies if the following is off. But one thing that I often wish we had is a syntax that allows me to import a package and assign a different local name to it in one go. So essentially a shortcut for import Foo; const bar = Foo.

Typescript has something similar (import * as bar from "Foo") and Python of course has the alias syntax (import Foo as bar). I find both of these extremely useful.

I think it would allow us to a) keep the long and descriptive package names we seem to tend towards (I like that!) and b) generally use using less, but still have relatively concise code.

And yes, obviously one can use the syntax I showed above, but I think it would be nice if there was official syntax for this.

1 Like

Maybe not “official” syntax, but I’m adding that (and many other things) to APITools.jl.
The syntax I had been thinking of was something like:
@api use Foo => bar
and also have things like:
@api use Foo(!thismethod, isalpha => is_letter)
to exclude and/or rename things, instead of just importing all exported symbols.

See #1255: module and import aliasing · Issue #1255 · JuliaLang/julia · GitHub

Also see the GitHub - fredrikekre/ImportMacros.jl module.

2 Likes

@jlperla was interested in having a macro for merging function definitions automatically.

https://github.com/chakravala/ForceImport.jl/issues/1

this is actually not that difficult to write a macro for so I created the @merge macro, but note that macros cannot be used for code generation, so the eval method must be called on the output of the macro.

Here is an example

julia> using ForceImport

julia> module Mod
           export fun
           fun(x) = x
       end
Mod

julia> using Mod

julia> eval(@merge fun(x::String) = String)
fun (generic function with 2 methods)

julia> fun(1)
1

julia> fun("1")
String

So with the eval(@merge fun...) macro, the Mod.fun method is automatically imported if necessary.

Here is the macro definition

macro merge(expr)
    if !( (expr.head == :function) | ( (expr.head == :(=)) &&
            (typeof(expr.args[1]) == Expr) && (expr.args[1].head == :call) ) )
        throw(error("ForceImport: $expr is not a function definition"))
    end
    fun = expr.args[1].args[1]
    return Expr(:quote,quote
        for name in names(current_module())
            try
                eval(ForceImport.imp($(string(fun)),name))
            end
        end
        eval($(Expr(:quote,expr)))
    end)
end

function imp(fun::String,name::Symbol)
    :(Symbol($fun) ∈ names($name) && (import $name.$(Symbol(fun))))
end
1 Like

https://github.com/MichaelHatherly/MergedMethods.jl

It’s not that difficult to do in a crude way, but basic merging isn’t necessarily correct. If package A creates a f(::Number) and package B creates f(::Float64), then automatic merging can accidentally change the internal dispatching of f in package A since it just added a dispatch that is more concrete than the one that package A expects to be using. So you have to actually check subtyping relations in the methods table and deduce when it’s okay to call the merged method in a given scope and when to ignore the merge because it would cause a subtle form of piracy (in this example, the internals of A should ignore the new f(::Float64) but in the scope of B it should have both definitions since it can do so safely, which should also be the case when both A and B are used if both export f).

This is what cannot be handled by a single global dispatch table and would need a lot more details to be worked out, if it can be effectively worked out. If you have package C introduce a new f(::Float64), you should merge so that way you have two versions of f(::Float64) such that one is used in the scope of C, another is used in the scope of B, and both of them don’t exist in the methods table in the scope of A to prevent breaking its internals through accidental piracy. And if you using all 3, then in the scope of Main in the REPL you should only get one method, since f(::Number) makes sense but there’s two f(::Float64) so that’s ambiguous so you discard it, but actually you want to error on Float64 still (? Do you?)? Now add in having multiple arguments and you can see that the number of checks + dispatches you actually need to be working with grows exponentially.

And then you have to take into account #265 fixes:

So if you add f(::Float64) to the module Main in the REPL, now you merge it in everywhere that you can, what functions do you now have to recompile? It’s again dependent on the subtyping relations and scopes in which functions are defined which would have to be satisfied to not “accidentally pirate”, which is kind of odd because you will want A.f(::Float64) to act differently so that way you can force pirating packages at will (for example right now you can currently modify dispatches internal to packages at will. It’s a great debugging tool so we wouldn’t want to drop this feature just because of how auto-merging has to not let those dispatches get added automatically). So you’ll need a pretty complex to workout how to handle each new change to the method table(s) and propogate recompilation.

And I’m not even a core dev, so I’m sure that Jameson would look at this description and tell me that I missed the hardest part…

Maybe it is possible to work it out (thought I cannot see how dynamic dispatch wouldn’t be even slower than now), but this leads to what @StefanKarpinski was saying with:

Even if all of this does get worked out, the question of what methods exist in a given scope will be fairly non-trivial if this exists. If you pull in 4 packages which share names, nobody could tell you in advance what dispatches exist in given scopes. You’d have to run this complex algorithm and check in given scopes to know what methods ended up being where.

After running a few examples in my head like this to try and see how it can be possible yet not accidentally pirate and break code… is this really easier than just telling someone to use import? Of course, this doesn’t need to be worked in a macro with a disclaimer “use wisely”, but to be a language feature this would have to all be worked out.

2 Likes

My takeaway from all this is that I just want tools (macros) to handle the common cases that I run into all the time, not automatic merging, but easier ways of manually explicitly saying how it should be merged, in as DRY a fashion as possible.