Functions from different modules with the same name

Hi !

I have a problem which I thought should never happen with Julia. The result is even worse when using Pluto.

I use two modules, which have a function with the same name, but taking parameters of different types. I thought that using them at the same time would create several methods for the same function, and that all would be good… not so.

Specifically, consider

using AbstractAlgebra # I use the permutations defined there
p= perm"(1,2,3)"
cycles(p)  # this works, so far

So this works. But suppose that I try

using Permutations  # also defines permutations,
                    # and also has a function 'cycles'
                    # so this issues a warning
q= Permutation([3,2,1])
cycles(q) # fails

This fails with ERROR: MethodError: no method matching cycles(::Permutation). Why doesn’t the second using statement merely add a method to the function cycles ?

This seems to work routinely in other contexts, like the * operator having more and more methods as we import more modules. In fact, these two modules above do define * for their respective permutations, and all works fine.

(One guess: people write function Base.* or something, letting Julia know explicitly what they want to achieve. But surely this cannot be the only way to add methods to a function, this would bring many questions, that I save for later…)

When i try all this under Pluto, things are actually worse : after the second using statement, even the first cycles function is “broken” – that is, calling cycles(p) fails with UndefVarError: cycles not defined.

So weird… ideas, anyone?

thanks!
Pierre

Usually you should have received a WARNING, didn’t you:

julia> module A
         export f
         f(x) = 1
       end
Main.A

julia> module B
         export f
         f(x) = 2
       end
Main.B

julia> using .A, .B

julia> f(1)
WARNING: both B and A export "f"; uses of it in module Main must be qualified
ERROR: UndefVarError: f not defined

This is because if A or B do not explicitly use the other and add a method to the function defined to the other, there is no reason to think that both functions are the same instead of just two different functions which happened to have the same name.

For that to work as different methods of the same function one of the modules should have imported the function from the other first:

julia> module A
         export f
         f(x::Float64) = 1
       end
Main.A

julia> module B
         import ..A: f
         export f
         f(x::Int) = 2
       end
Main.B

julia> using .A, .B

julia> f(1)
2

julia> f(1.0)
1

Then Julia would know that the two methods are methods of the same function.

If that was not done, you need to qualify the use of the functions:

julia> A.f(1)
2

julia> B.f(1)
2

and if you really need a convenience syntax, you can add your own methods:

julia> module A
         export f
         f(x) = 1
       end
Main.A

julia> module B
         export f
         f(x) = 2
       end
Main.B

julia> using .A, .B

julia> f(1)
WARNING: both B and A export "f"; uses of it in module Main must be qualified
ERROR: UndefVarError: f not defined
Stacktrace:
 [1] top-level scope
   @ REPL[4]:1

julia> f(x::Int) = A.f(x)
f (generic function with 1 method)

julia> f(x::Float64) = B.f(x)
f (generic function with 2 methods)

julia> f(1)
1

julia> f(1.0)
2


5 Likes

I guess there is bound to be a problem if you have two functions with the same name and accepting the same arguments and argument types.

Does your code work if you do:

using AbstractAlgebra # I use the permutations defined there
p= perm"(1,2,3)"
AbstractAlgebra.cycles(p)  # this works, so far

and

using Permutations  # also defines permutations,
                    # and also has a function 'cycles'
                    # so this issues a warning
q= Permutation([3,2,1])
Permutations.cycles(q) # fails

Since Julia 1.6.0, you can also introduce aliases for import, so you can try:

import AbstractAlgebra as aal # I use the permutations defined there
p= aal.perm"(1,2,3)"    # is `perm` a function from `AbstractAlgebra`?
aal.cycles(p)  # this works, so far

and

import Permutations  as prm # also defines permutations,
                    # and also has a function 'cycles'
                    # so this issues a warning
q= prm.Permutation([3,2,1])
prm.cycles(q) # fails

I’m not sure whether the Julia designers have come up with some “standards” for aliases???

Complementing the previous answers with the specific example:

julia> using AbstractAlgebra, Permutations

julia> p = perm"(1,2,3)"
(1,2,3)

julia> cycles(p)
WARNING: both Permutations and AbstractAlgebra export "cycles"; uses of it in module Main must be qualified
ERROR: UndefVarError: cycles not defined
Stacktrace:
 [1] top-level scope

Does not work, use must be qualified:

julia> AbstractAlgebra.cycles(p)
Cycle Decomposition: (1,2,3)


julia> q = Permutation([3,2,1])
(1,3)(2)

julia> Permutations.cycles(q)
2-element Vector{Vector{Int64}}:
 [1, 3]
 [2]

And you can define your methods explicitly:

julia> cycles(p::typeof(p)) = AbstractAlgebra.cycles(p)
cycles (generic function with 1 method)

julia> cycles(q::typeof(q)) = Permutations.cycles(q)
cycles (generic function with 2 methods)

julia> cycles(p)
Cycle Decomposition: (1,2,3)

julia> cycles(q)
2-element Vector{Vector{Int64}}:
 [1, 3]
 [2]

1 Like

Thanks all for your answers! I had prepared a workaround similar to what @lmiq suggested at the end. (And yes @BLI, if my imports are qualified everything works.)

So I guess when you want to add a method to a function you really always have to be explicit about it, giving its full name Module.f if necessary. This is disappointing though – what would you do in the following situation?

You have some code, say some function f(x) that will work as soon as some other function, call it cycles(x) while we’re at it, exists with some prescribed behaviour. So say

function f(x)
    # bla bla
    s= cycles(x)
    # bla bla involving s
end

And then you want people to use your function f , after they have created a version of cycles that suits their needs – does something specific to their particular data type. How would people use it? if they merely create a function called cycles, without explicitly adding a method to an existing one, then name clashes will happen rapidly. There’s an example in my original post.

Perhaps I should say more appropriately that I would like to use f on some data types that have a function cycles but written by people who have no knowledge of my little function f:slight_smile: So asking them to add a method to MyPersonalModule.cycles is not a serious option. And writing a workaround for each data type, as I will end up doing for my permutation things, is not great either.

In OOP, say in Python, I would do things like this all the time, writing code calling x.cycles() that will work for any object with a method called cycles.

1 Like

Unfortunately, this is one the pain points which will not go away (you may read Function name conflict: ADL / function merging? to get the standing point of the Julia creators on this issue).

You can use external packages though, which are trying to mitigate it, for example UsingMerge.jl. Unfortunately, it’s not in the general Registry yet, so one need to install it with

pkg> add git@github.com:jmichel7/UsingMerge.jl.git

Then you can add necessary functions through the @usingmerge macro

using UsingMerge
using AbstractAlgebra
@usingmerge Permutations: cycles

julia> p = perm"(1,2,3)"
(1,2,3)

julia> cycles(p)
Cycle Decomposition: (1,2,3)
4 Likes

The fundamental problem here is that two packages could define two functions with the same name that do different things with the same type of variable. Then what to do?

Your function is perfectly fine. But if one user wants to define a cycles function and use a package that also defines a cycles function, he/she will have to qualify the use of the cycle function of that package, or, if that is what he/she wants, extend the methods of the functions of that package.

ps: In python you would be qualifying the names all the time, with np.something(), etc… To feel safe, you can do that in Julia as well, using import instead of using. But generic programming sort of gets lost there.

Example with python is misleading actually. Most of the time people use OOP in python, and class instances do not qualify methods, which is contrast to Julia with it’s multiple dispatch.

Compare

class A:
   def __init__(self):
      self.a = 1

   def foo(self):
      return self.a

if it is a part of some module utils.py, then usage will be

import utils

a = utils.A()
a.foo()

Similar code in Julia is

module Utils

struct A
  a
end
A() = A(1)

foo(x::A) = x.a

end # module

and usage

using Utils

a = Utils.A()
Utils.foo(a)

You can see, that method call is different. Roughly speaking, in python fully qualified name of the method is inferred from the class. So, python equivalent of Julia code in this case would be

a.utils.foo()

which is redundant.

1 Like

Indeed, but adding that A.foo there in python is a method that can only be applied to objects of type A, thus a new class of objects need a complete new implementation of foo. (that is the trade-off for never having name clashes)

Except of inheritance and mixins :slight_smile:

But surely, each approach has its advantages and disadvantages. Fact that you need to almost always use fully qualified names of methods in Julia is one of such disadvantages (in my opinion). And it seems that it is impossible to solve it on a general level, due to the type piracy and other issues. Yet on the other hand UsingMerge.jl approach seems promising.

1 Like

Do you think that happens? I feel exactly the opposite, always never having to do that, the disadvantage being sometimes having to deal with these name clashes explicitly.

If you mean that in package development the addition of methods needs qualifying (explicit import), agreed, but from a user perspective, those name clashes are not common, imo. It is much more common to see “qualified” names in the module model of python (which means really all the time).

Do you think that happens?

We have this topic, so it happens.

1 Like

Not “almost always” :slight_smile:

Thanks, i’m going through the very long discussion following your link, this is quite a vast subject, it seems.

By the way, I don’t think my python example is misleading. I’m sure you already know what I’m going to write, but I mean that you can have in one file:

class Foo:
    def value(self):
        return 1

and then in some other file, written by someone else in a galaxy far, far away:

class Bar:
    def value(self):
        return -1
    # and please imagine that the rest of Foo and Bar are wildly different

Then one can write anywhere

print("the value of x is", x.value())

and it just works, whether x was created as a Foo or Bar or whatever. It’s always the right method.

1 Like

I think he meant my example.

1 Like

Oh, OK. My last post was a waste of time for everyone, sorry :slight_smile:

1 Like

You can probably use methods(A.f) and methods(B.f) to programmatically create glue methods.