Is an explicit "export" a good thing?

I am not sure how this is better than simply

import ThatModule: thisfunction, thatfunction, etc

If it is always the same list, a module would save some work, but at the same time it also adds overhead, and generally it loses the specificity that comes from a tailored import approach (I may use a specific subset for a project, and import allows me to be explicit about it).

I guess one could put the difference succinctly as who decides what is made available for implicit access:
(1) the developer, or (2) the user.

In the first case, the module mmm exports certain symbols. In the second case, the user is free to write a module that provides that implicit access, but using the module mmm itself does not lead to the problem of magically appearing symbols.

I am not sure I understand; with an explicit import list it is also the user who decides.

Maintaining a separate package-specific API module for some other package looks cumbersome, but perhaps this is because I have not seen it in practice. Can you link an example of a package that employs this technique?

You are absolutely correct, that is of course possible. My point was that if the user wished to do using to get the entire API available at the REPL, employing a separate module to do that is in my opinion preferable to the developer exporting the stuff once and for all.

BTW: I am not trying to persuade anyone to do this. I would like to figure this out for my own package: no obligation for anyone else. :wink:

Here is the MWE:

module mmm  # DEVELOPER WRITTEN: note NO explicit exports
"""
The "exported" function from this module is `publicfun`.
"""
function publicfun()
    println("in publicfun()")
end
function _privatefun()
    println("in _privatefun()")
end
end
module mmm_API # USER WRITTEN
# As one of the users who use `mmm` a lot from the REPL, I want to do `using mmm_API` 
# to get implicit access to both the public function 
# and the private function from the module `mmm`.
using mmm: publicfun, _privatefun
export publicfun, _privatefun
end

Yes, of course, but it is easier too see what is intended to be externally used and what is considered private.

Just in case someone is interested: I figured out how to deal with exports for my package. It addresses the problem of control of conflicting exports by the USER of the package (instead of the DEVELOPER). Should you be interested, drop me a message. Or, have a look at GitHub - PetrKryslUCSD/FinEtools.jl: Finite Element tools in Julia

2 Likes

It would be great if you could provide a brief summary here. Or perhaps a blog post.

2 Likes

I hear you: I am working on it.

2 Likes

The design was described in the FinEtools documentation (https://petrkryslucsd.github.io/FinEtools.jl/modules.html), and in the use-case package (https://github.com/PetrKryslUCSD/FinEtoolsUseCase) which details two methods in which the control of the public interface of FinEtools can be exercised by the USER of the package.

The key is to concentrate all exporting into the top-level file of the package.

I would appreciate any and all constructive comments!

1 Like

Thanks for writing it up. It is great that you document your intended interface, and provide examples.

I still think that for one-off usage, it is best to just import explicitly from packages that choose not to export their API. However, if a user is consistently using the same subset of some API, creating some module that imports and reexports could be worthwhile, especially if we are talking about a lot of symbols.

The downside is the extra administrivia of creating a local module. I like to keep my code (even scripts) independent of the current working directory, so I try not to use include in scripts, or alternatively cd to some path I can calculate (eg from @__DIR__ in some module file). Ideally, the interface module which reexports would be available in LOAD_PATH.

Finally, if you stick to this interface, consider writing a macro that imports and reexports, along the lines of
https://github.com/simonster/Reexport.jl
but just for a subset of symbols (explicitly specified).

Thanks for starting this topic. Even if I don’t grasp the motivation for this interface module (possibly because I have not encountered the use case), it made me think about my own export choices.

If I understand you correctly, your workflow consists of writing lots of short scripts? If so, it will be quite different from mine. I like to put my code in modules so the workspace can be kept clean and manageable.

You mentioned LOAD_PATH: I believe I saw somewhere recently that the new package manager would get rid of this entirely.

And, thanks for the pointer. Reexport looks interesting.

Can you not use a macro to essentially do the same thing? The macro can be something like FinEtools.@import LinearElasticity Meshing which would then import all the parts of the package related to linear elasticity and meshing for example. These could be the API identifiers from one or more submodules of FinEtools. Would that serve the purpose without introducing a new API module?

Sorry, I’m not following: which part of the process would this macro facilitate? Are you referring to the use-case package?

Yes to essentially replace the use-case package. So as a user, I can still tell FinEtools which parts I am interested in without adding them to another myFinEtools package. Not sure if that will serve the purpose in your mind?

I see (I think). How would you organize the exports in the FinEtools package into groups then? One thing I was not happy about was that the exports hhave been scattered across the ~50 modules. It seemed to me much cleaner to organize the exports so that all the export statements are in a single file (the top level package file). For the macro you propose, these exports would have to be organized into groups… Not sure how to do that.

Hmm, not really sure I haven’t dealt with submodules before, but if I think of something I will let you know.

Something along these lines perhaps.

module Outer
module Inner
f() = 1
g() = 2
end

import .Inner: f, g
macro simport(x...)
    expr = Expr(:toplevel)
    for _x in x
        if _x == :(Funcs)
            push!(expr.args, Expr(:import, :Outer, :f))
            push!(expr.args, Expr(:import, :Outer, :g))
        end
    end
    return esc(expr)
end
end

Outer.@simport Funcs

f()
g()

Notice all the symbols can be imported first into the top level module and then grouped together in the macro, all in one place. Then I can tell the Outer module what is that I want, and the macro will take care of the shopping for me.

Or more neatly:

module Outer
module Inner
f() = 1
g() = 2
end

import .Inner: f, g
groups = Dict(:Funcs => [:f, :g])

macro simport(x...)
    expr = Expr(:toplevel)
    for _x in x
        if haskey(groups, _x)
            for _f in groups[_x]
                push!(expr.args, Expr(:import, :Outer, _f))
            end
        end
    end
    return esc(expr)
end
end

Only 2 lines have to be modified to include the functions and groups from your 50 submodules instead of the single Inner module.

Cool! I will ponder this a bit… Thanks.

Petr