When to use `using Package: function` and when to use `import Package: function`

I have a package, which imports a couple of package, as well as a couple of extra functions from these. I am trying to figure out whether there is some “rule” for whether to use

using Package: function

or

import Package: function

From looking around, my impression is that I should use import Package: function if I want to overload function (i.e. create a dispatch for some structure that my package creates). If I only intend to use the function, I should instead use using Package: function.

Is this correct?

2 Likes

I believe you need to use import to extend an existing function, and using does not work:

struct MyType
  a::Int
end

using Base: +

(+)(x::MyType, y::MyType) = MyType(x.a + y.a)

ERROR: invalid method definition in Main: function Base.+ must be explicitly imported to be extended
Stacktrace:
 [1] top-level scope
   @ none:0
 [2] top-level scope
   @ REPL[20]:1

import Base: +
(+)(x::MyType, y::MyType) = MyType(x.a + y.a)
+ (generic function with 208 methods)

MyType(1) + MyType(2)
>  MyType(3)

Though you can also use the full name without importing:


function Base.:+(x::MyType, y::MyType)
  return MyType(x.a + y.a)
end

MyType(1) + MyType(2)
>  MyType(3)
2 Likes

Got it, and if I do not plan on extending, there is no reason to use import instead of using?

I am trying to figure out which to use in my package, and if this is the only difference I’d rather stick to using package: function for those I do not plan on extending (also a nice way to mark which functions I plan to do what with)

As far as I can tell, the other user facing functionality is exactly the same between using Statistics: cor and import Statistics: cor. So yes, it’s a nice way to denote which ones you plan to extend.

There’s also a minor issue where there used to a bug where using Statistics: cor would cause cor not to tab-complete properly in the REPL, while import Statistics: cor would. But I think that was fixed recently.

1 Like

I tend to avoid import Package: function and just extend via the following syntax.

function Package.function(args...)
   # code here
end
13 Likes

Never use import.

  • If you don’t want to extend a method, using Package: f is enough.
  • If you want to extend a method, you should do it explicitly, so using is still better.
  • If you want to bring a module into scope without any of its exports, then write using Package: Package.
11 Likes

A nice way to have such decisions made for you is to use a style guide and the associated JuliaFormatter.jl config. I usually stick to the BlueStyle, which pretty much says

Note that ExplicitImports.jl can now help you track down lonely using MyPackage statements and replace them.

4 Likes

I really would like that to be

using Package: .
3 Likes

Full agree.

Mostly agreed, but it’s not a clear cut win when the package name is very long, and it doesn’t work for macros.

No way! I’m not going to repeat myself just because an opinionated style guide thinks import is evil. (I would be happy to using MyPackage: . though if that was implemented.)

4 Likes

The reason why import Blah: foo is usually discouraged is that forcing qualification gives you added protection, albeit at runtime, from silently mixing up two same names intended to be from different modules. Consider one scenario where you start writing some other included file down the line and you forgot you imported foo way back, so you start making a bunch of methods for what you think is a new function foo to do something else. At runtime, those methods are added to the imported function foo instead, easily breaking Blah by replacing methods or affecting dispatch.

Of course the reasonable counterargument is that you should keep all your imports in one place and check the names there regularly to avoid accidental name reuse. After all, variable accesses and function calls aren’t often qualified, so you’ll have to check the list anyway to distinguish the module’s names from the imports. This incidentally is why I never bought the other argument that qualification is important for distinguishing imports at first glance; if it were so important we’d be qualifying everything like in Python.

Now consider the reverse scenario where you decide to import a new package’s function import Blah: foo at the start but you forgot you had already made an internal function foo somewhere in a buried included file. Now you’ll have to check ALL names in ALL files to avoid name reuse. A linter could do this automatically, but it’s nice if the base language also warns or stops you at runtime before anything starts running for days. This runtime protection is already there for reassignment, even for import, so it’s nice if that were consistent for adding methods:

julia> module A; x = 1; foo() = 0 end
Main.A

julia> import .A: x, foo

julia> x = 2
ERROR: cannot assign a value to imported variable Main.x
Stacktrace:
 [1] top-level scope
   @ REPL[3]:1

julia> A.x = 2 # reassignment needs qualifying
2

julia> foo(x) = 1 # adding method does not need qualifying
foo (generic function with 2 methods)

That’s not to say import Blah: foo should never be used. A small enough module won’t span enough files to run into such issues, and base Julia does import Base: ____ a lot because those symbols are more readily recognized anyway. Macros don’t have many argument types worth dispatching over, but when they do, module qualification currently does not work so you need import Blah: @foo. That’s not so bad though because extending macros from other packages is type piracy and should be avoided.

5 Likes

Worth noting here is that package extensions have added the pattern of defining an empty macro in the base package and extending it with a method in the extension module.

2 Likes

I’m not a fan of functions or macros that do nothing until a separate package is imported, but it’s definitely a lot more maintainable for the names to unconditionally exist and belong to 1 package rather than try to make them show up only when an extension is loaded.

2 Likes

Everyone, this is amazing :heart:. Thanks a lot, this is really useful to learn how these things work :slight_smile:

1 Like

The import X as Y syntax is very helpful for this:

julia> import Base as B

julia> B.length((1,2,3))
3
3 Likes

Absolutely, unless your goal is not to use import. :slight_smile:

Is there any semantic difference between using Base: Base; const B = Base and import Base as B? It seems like splitting hairs to argue that this is a bad use of import, but I also would’ve preferred using Base as B for this

E- ah, there is using Base: Base as B if we want to duck import usage

No practical difference for Base, but in general import X as Y doesn’t bring X into your namespace, which may matter for name collisions.

1 Like

Wait, this is useful. I do use

using Package
const Pk = Package

but changing that to either of

import Package as Pk
using Package: Package as Pk 

might make sense?

1 Like

Yes. It’s better to alias the package with as. If you write

using Package
const Pk = Package

Then you bring Package and its exports into scope.

However, If you write

using Package: Package as Pk 

You only bring Pk into scope.

2 Likes

A new variable is also semantically different from an imported and possibly renamed variable, though not practically if const.

A possible reason for why the import system is so complicated is that renaming wasn’t present for all of v1 (looking it up it seems to be 1.7, was it really that late?). With renames to a couple characters, even qualifying imported modules at accesses and function calls is pretty smooth in practice. I’d still rather import the names to occupy the name in the module and stop assignments of a new one, and might as well use it without qualification sometimes at that point.