What does `export Foo` do when there are multiple `Foo`'s?

export works on names, but sometimes there are several objects with the same name. Which of them are exported? I’m wondering about this because I have struct Foo and a constructor function Foo.

The discussion of keywords says

export is used within modules to tell Julia which functions should be made available to the user.

Taken literally, this means only the constructor is exported.

But the Modules documentation says

Names (referring to functions, types, global variables, and constants) can be added to the export list of a module with export.

and the subsequent examples make clear that export is not limited to functions.

My guess is that even if I only define struct Foo, a default constructor is created anyway, and so the function/type distinction would remain relevant.

Further, one function may have many methods, raising again the issue of what is exported. Although the documentation in Modules mentions only functions, not methods, and so maybe thinking about exporting individual methods is a mistake.

My guess is that everything with the indicated names is exported. Can anyone confirm that, or tell me what the truth is if it’s something else?

BTW, if I want to explicitly get a particular binding of a name, how would I do that? This is just general curiosity; currently all I need is that Foo(a, b, c) gets the function Foo, and that seems to be working fine.

Thanks.

1 Like

I believe it’s the name to be exported, so all the object that share that name.

The first two sections below contain my first attempt at an explanation, and the third section contains a correction which I think gives a better and more correct explanation.

1) Semantics

Functions

If you have export foo, where foo is a function with multiple methods, you are exporting one object: the generic function foo. When you declare new methods for foo, you are extending the definition of the function foo—you are not adding new objects called “methods”. In other words, new extensions to a function are called “methods”, but they are not new objects, they are extensions to the object foo.

Evidence for this point of view can be seen from the output of typeof(foo):

julia> typeof(foo)
typeof(foo) (singleton type of function foo, subtype of Function)

The type typeof(foo) is a singleton type, of which foo is the only instance. In other words, the various methods do not have their own object instances (except perhaps as hidden implementation details).

Constructors

If you have export A, where A is a type, you are exporting two objects:

  • The type A.
  • The (constructor) function A.

2) Implementation Details

The following details may provide some insight, but I think they are considered to be implementation details rather than semantic guarantees of the language.

The constructors for a type are implemented by making the type object callable. You can even do this by hand. For example:

julia> struct A end

julia> (::Type{A})(x) = A()

julia> methods(A)
# 2 methods for type constructor:
[1] A() in Main at REPL[1]:1
[2] A(x) in Main at REPL[2]:1

julia> A(42)
A()

So when you write export A, in practice you’re really just exporting the type object A, and the constructors come along for free, since the type object A is callable.

The situation for functions is similar. We can add methods to foo by making objects of type typeof(foo) callable:

julia> foo(x) = 2x
foo (generic function with 1 method)

julia> (::typeof(foo))(x, y) = x + y

julia> methods(foo)
# 2 methods for generic function "foo":
[1] foo(x) in Main at REPL[1]:1
[2] foo(x, y) in Main at REPL[2]:1

julia> foo(1, 2)
3

So when you write export foo, in practice you’re really just exporting the function object foo, and the methods come along for free, since the function object foo is callable.

3) Corrections / clarifications

I think my explanation above is more of a post-hoc justification. It really comes down to the passage from the docs that you quoted:

So, all that export really does is take a variable binding in a module and make it available in another module when you using the first module. A variable can only be bound to one value (object). In the case of types and functions, there are multiple ways of calling that object, but it is still only one object. And the fact that types are callable really is a language guarantee, rather than an implementation detail as I wrote above.

3 Likes

I appreciate your taking the time to respond, but your response has lots of statements that sound like statements of fact that I think are only speculation (and you kind of flag them as such). Quite a number of them seem incorrect. For example,

When you declare new methods for foo, you are extending the definition of the function foo—you are not adding new objects called “methods”. In other words, new extensions to a function are called “methods”, but they are not new objects, they are extensions to the object foo.

But the julia docs say

all values in Julia are true objects

and, as you show in part 2, one can retrieve the individual methods. The documentation for which even refers explicitly to Method objects. So method objects exist and they can extend existing function objects.

In part 3 you say

all that export really does is take a variable binding

That differs from the quoted passage from the Modules documentation, which refers to names not variable bindings. The same name may be bound to different things depending on usage, e.g., Foo(a, b, c) binds Foo to my constructor function, while new in an inner constructor implicitly references the type. The reason for my question was uncertainty about how such situations are handled.

Part of the justification for this is the argument that the type and the constructor are the same thing, perhaps buttressed by identification in part 2 of a way to make a constructor from the type. Aside from my doubts that you know how (implicit) constructors are “really” created (I certainly don’t know), even the mechanism you outline does create a function or method, which is not the same as a type even if it has the same name.

The confusion here is because much of Julia is implemented in Julia. You might have overlooked this part of my comment:

[emphasis added]

Yes, Method objects exist, but only as an implementation detail—they are not part of the Base Julia interface, because they do not have a docstring (and a docstring entry in the manual):

help?> Method
search: Method methods MethodError methodswith hasmethod

  No documentation found.

Furthermore, look at what happens if we look at the value of a constructor definition:

julia> struct A end

julia> a = function A(x)
           @show x
           A()
       end
A

julia> a
A

julia> typeof(a)
DataType

So, we see that the value returned by a constructor definition for the type A is literally just the type A. So a constructor really just is a callable type. And this is documented in the manual (albeit sparsely) in the section on Function-like objects:

This mechanism is also the key to how type constructors and closures (inner functions that refer to their surrounding environment) work in Julia.

There is a little more detail on this in the old v0.6 docs:

Constructors T(args...) in Julia are implemented like other callable objects: methods are added to their types. The type of a type is Type, so all constructor methods are stored in the method table for the Type type. This means that you can declare more flexible constructors, e.g. constructors for abstract types, by explicitly defining methods for the appropriate types.

These same arguments carry over to methods of generic functions. Consider this example where we look at the values of method definitions:

julia> f = function foo(x::Int)
           1
       end
foo (generic function with 1 method)

julia> g = function foo(x::Float64)
           2
       end
foo (generic function with 2 methods)

julia> typeof(f)
typeof(foo) (singleton type of function foo, subtype of Function)

julia> typeof(g)
typeof(foo) (singleton type of function foo, subtype of Function)

julia> f === g
true

We see here that the value of a method definition for the function foo is simply the function foo itself. In other words, the concept of a Method object does not exist in Julia unless you reach into the Julia internals. (Don’t do that.)

I was a little loose with my language there. My reasoning was that within a particular scope, a name/variable is always bound to exactly one value/object at a time. Hence, I used “variable binding” interchangeably with “name”. I’ll think about how to rewrite that in a more accurate way, since there is some distinction between “names” and “variables”.

No, only the assignment operator, =, binds variables to values, and the value associated with a name does not change based on your usage of that name.

I don’t see how the special new function is relevant—it has nothing to do with names or bindings.

I don’t know why you are mystifying something simple: methods are objects. Neither you nor I originally said anything about them being part of Base or having docstrings, or being implemented in julia or C. None of those things matter. So explaining how they work by saying they are not objects or not “real” objects is not going to work.

Any time a name occurs in a program the program must associate it with a value. That is a binding too, albeit not necessarily a long-lived one.

new instantiates a type, and so it needs to know what type it is instantiating. That is the type within which the new appears, and so is an implicit reference to the type and its name. Functions with the same name may exist, but I presume new will not call them.

It’s not clear to me if you understand the difference between the semantics of a language and the implementation details of a language. The semantics of a language are the logical rules for what the language means and how to use it. As a user of a language, you should only be concerned with the semantics, not the implementation details. In fact, you should actively ignore the implementation details, since they can change at any time.

Parts of the Julia runtime are implemented in C. Does that mean that the intermediate C objects have a semantic meaning in the Julia language? No, it does not. The Julia parser is written in femtolisp. Does that mean that the intermediate femtolisp objects have a semantic meaning in the Julia language? No, it does not. Parts of the Julia language are implemented in Julia itself. Does that mean that the intermediate Julia objects have a semantic meaning in the Julia language? No, it does not.

The FAQ has the following to say about the public API for Julia:

How does Julia define its public API?

The only interfaces that are stable with respect to SemVer of julia version are the Julia Base and standard libraries interfaces described in the documentation and not marked as unstable (e.g., experimental and internal). Functions, types, and constants are not part of the public API if they are not included in the documentation, even if they have docstrings.

The Method type is not in the documentation, therefore it is not part of the public API for Julia. This is really not a disputable fact. There is no way to access or create a Method object without relying on internal implementation details of the Julia language.

In my previous post, I gave an example that demonstrated that the value of a method definition expression for the function foo is not a Method object, it is simply the generic function foo. If you can’t believe the evidence of your own eyes, then there is nothing I can do for you.