Using symbols from parent module without naming it

Is it possible to import/using a symbol for a parent module without naming it? Eg

module Foo
const c = 1
module Bar
using ..Foo:c # like this, but without referring to Foo
f() = c
end
end
3 Likes

const c = parentmodule(@__MODULE__).c maybe…

5 Likes

How about this: @.. c (or @.. c d e - if you need to import multiple symbols).

The macro definition here:

macro (..)(s...)
    Expr(:using,
        Expr(:(:), Expr(:., :., :., Symbol(nameof(parentmodule(__module__)))),
            (Expr(:., (Symbol(x))) for x in s)...))
end

In your context:

module Foo
const c = 1
const d = 2
module Bar

macro (..)(s...)
    Expr(:using,
        Expr(:(:), Expr(:., :., :., Symbol(nameof(parentmodule(__module__)))),
            (Expr(:., (Symbol(x))) for x in s)...))
end

# works with importing multiple symbols
@.. c d

fc() = c
fd() = d

@info "happy import: " fc() fd()

end
end

Running the program would produce:

┌ Info: happy import: 
│   fc() = 1
â””   fd() = 2

Not sure if this is reliable enough for any context (limited testing) - I am sure it can be improved.

Later edit: the initial version only worked if the macro was defined inside the calling module. The current version of the macro works even if defined in module X and invoked from Y (it will still import symbols from the parent of Y).

LE2: renamed it for more convenience. You can invoke it by @.. c d e f g.

3 Likes

I was wondering the same just a few days ago. I don’t think you can. However, it should be

using ..: c

if it were to be consistent. Likewise, using ...: should be importing from the grandparent.

3 Likes

Yes, I am wondering if I should open an issue, as that would be the natural syntax for it. Could not find an existing one.

2 Likes

Issue opened:

3 Likes

Even if it was possible to do using ..: c, I would not recommend to do it when submodules are written in separate files, which seems to be the use case that @Tamas_Papp has in mind according to his comment in the github issue.

Yes, it would reduce the workload of refactoring code, as suggested. But on the other hand, it might make the code less understandable. Let’s say Foo.jl is:

module Foo
const c = 1
include("Bar.jl")
end

And Bar.jl is:

module Bar
using ..: c
f() = c
end

How do I know what c is and where does it come from, if I’m just reading Bar.jl? I should scan all the other files of the project until I find one with the code include("Bar.jl"), instead of going directly to Foo.jl, which would be the obvious choice with the explicit using ..Foo:c.

(Although admittedly, writing out the name of the module only helps if the names of the files are a good hint to look for the definitions of modules; otherwise the situation is not that different.)

2 Likes

I am not sure I understand your question, but the (proposed) syntax clearly specifies that it comes from the parent module.

What I mean is that if the parent module is in another file, I’ll have to look for that file if I want to know more details about what c is, how it is defined, etc. And explicitly naming the parent module may be helpful to do the search.

But that applies to all code. If I see a symbol that is not defined in a file, I have to find it if I want to know all the details.

Or, ideally, I can look up the docs if it is documented and that may be sufficient. But the tooling for that transcends modules, so it is fine.

Sorry, I still don’t understand what your point is, in the sense of something special going on with importing symbols from the parent module, as opposed to just importing symbols from anywhere else.

1 Like

I am not sure how much time will pass until the new syntax recommendation will be picked up (but since it is non-breaking and easy to implement, who knows).

Until then, besides the @.. c d approach above (which I prefer), I think another quick workaround can be something like this:

module Foo

const c = 1
const d = 2

module Bar

struct pm end
Base.propertynames(::Type{pm}) = names(parentmodule(@__MODULE__))
Base.getproperty(::Type{pm}, name::Symbol) = getproperty(parentmodule(@__MODULE__), name)

# Now, you can use `c` and `d` directly:
fc() = pm.c
fd() = pm.d

@info "happy: " fc() fd()

end
end

How far up the parentmodule chain would you like to go? I’d imagine something like using ........: foo to be quite confusing - can you see at a glance how many parentmodules that is?

As an aside - what’s the usecase where this is desirable to do? Or is that purely for the (unique) parent module?

Since there is no limitation on the using ... syntax for modules, I see no reason to impose one here.

You need a symbol from the parent, which is uniquely identified by being the parent, so DRY.

2 Likes

Hmm. I’m personally not a fan and I agree with the posters above - this feels too DRY, to the point of obscuring the meaning/source of the used symbol for someone who isn’t intimately familiar with the codebase in question. “DRY” doesn’t mean “write as little human-legible code as possible”, it can certainly be taken too far.

Not to mention that these long chains of dots are incredibly hard to parse visually.

2 Likes

We’ll need some rainbow effect - where each dot has a different color. :slight_smile: Or we can add inlay hints to the vs-code extension that will read 7 dots and counting.

Joke aside, I am pretty comfortable with having a unique syntax to import stuff from the parent module (especially since that is uniquely identified in the submodule context - so no ambiguity there) - as long as it is properly documented and becomes part of the knowledge base. (..: is OK).

Also, if the tooling improves and newcomers can hover ..: and have instant information about what is going on, then such additions might not feel like we are compromising in readability.

Note that

  1. this is orthogonal to the issue; we already have the ....... syntax, of arbitary length, just that it only works for modules, not all symbols,

  2. it is in practice limited by how much people nest modules. arguably if someone nests >5 layers of modules, they have much larger problems than having to write/read ......

3 Likes

An ugly solution in the vein of @kristoffer.carlsson’s solution, but using the standard using mechanism:

@eval using ..$(nameof(parentmodule(@__MODULE__))): c
3 Likes

The point was for Bar.jl to be include-able in many parent modules with the condition that those modules have a c, which I assume would be documented with more details (like type) in Bar.jl, like an interface. It’s not unlike how include-able files containing methods can be included in different modules to be in separate functions; that file may also assume certain globals exist (and should be documented).

Thing is, I agree it’s unwise to make easier, and it’s not just because it’s harder to read. It’s already very unusual to make nearly identical methods in different functions in different modules, and when it does happen, it probably should be refactored. This would make nearly identical modules in different parent modules, which should still be refactored instead of introducing new syntax. Although this seems like a DRY (don’t repeat yourself) principle because it reduces copies in the source code, include-ing identical source code in different modules to make structures that behave slightly differently depending on the global variables around is absolutely not DRY because the running process is a system too, and it has multiple tweaked copies with identical names.

Even the source code is not DRY much. A perk of DRY is that you can make a change in one place instead of several. So let’s check that, what if you had to rename c to d? Sure you can do that in one place for Bar modules, but you still have to track down every parent module. What if you had to change the various c to one new type? You can’t even do that in Bar.jl, you must find every parent module.

2 Likes

I am not sure where you got impression that this is what I am trying to do. I think you may be misunderstanding something in my question — please read the original post.

Julia already provides the ... syntax to access parent modules in various ways, what I am asking about is simply a missing corner case. I don’t think it was omitted by design, it was simply overlooked and I hope it will be fixed as some point.

1 Like

I supposed I might’ve misunderstood something from the thread. So you intend submodules in separate included files but not to include them several times in different places?

If it were intentional, it would have good reason. It is difficult to visually tell the difference between .. and ..., and a file doesn’t innately point out other relevant files, so the name makes it much easier to find parent modules. I see your point that renaming would need changes in several places, but it’s not worse than renaming other global variables, easy with a Find and Replace.

2 Likes