[ANN] CallableModules.jl – Make Julia modules callable

Hi,

I am happy to announce CallableModules.jl. By making your module callable, you can, e.g., share the same name as a shell command line interface and as a Julia interface.

So if your app accepts the following arguments when the process is started (typically in the shell):

MyApp int1 int2 [--verbose]

you can now also have

MyApp(int1, int2; verbose)

within Julia.

There are probably other use cases for callable modules. You can play with it to find them. The package is rather small, but it needs to be a package due to the implementation doing type piracy which would clash with a redefine method error if multiple packages would be doing it, but works when they all use CallableModules.jl.

To use it, you need to

]add https://github.com/PatrickHaecker/CallableModules.jl
using CallableModules

You can then tag the method in you module (package), which should be called when the Module is being called, e.g. main, with @module_main to get the interface as shown above.

The implementation uses the design workaround proposed by @Keno in #61256 where Val is used to dispatch on modules to then make (all) Modules callable to then dispatch on the ones which implement something useful.

The package is not yet registered, because I want to find a suitable Github organisation for it. Maybe JuliaCollections could be a home for the package? I’d be happy if someone would offer to have it in their Github organisation and provide me with permissions so that I can maintain it there.

1 Like

Quite clearly not appropriate for the General registry. Do type piracy in a personal script if you must, but don’t try to register it in General. My two cents, but seems clear.

I really appreciate your opinion, @nsajko. Thanks for stating it clearly.

In most of the cases I agree that we should avoid type piracy. However, in this case, I have the following reasons why I currently think it is the best available option:

  • Julia uses the same type for all modules. I argued in #61256 why I think it would be better to have a separate type for each module. However, this does not seem to have any traction and even if this might be a good idea, we just don’t have it and at least short-term it does not seem to be in reach.
  • If Core implemented the relevant method, it would no longer be type piracy. But again, it does not seem that there is any intention to do so. And I see that there are arguments for opt-in here, at least to explore the problem space a bit better by having a package first.
  • For callable objects type piracy is harder to avoid in general, as there is no function name to distinguish during dispatch.
  • The package’s purpose is exactly to avoid type piracy problems by being the coordinating instance.
  • Every package using CallableModules.jl is actively opting in, so there should be no surprises. But please mention if you have an idea how this should to made even more transparent.
  • The chances of a true type piracy problem, i.e. that a module call will be used in the future for something else than, well, a module call, seem exceedingly low (is that logically even possible?).
  • The functionality that an app shares the same name for its module, its command line interface and its Julia interface is useful and quite intuitive from a user’s perspective.

But you made me curios, as I know you often come up with really elegant solutions: What do you suggest? How could the problem of name sharing be solved more elegantly?

1 Like

Quite a few packages in General do type-piracy, both relatively-old and relatively-new. This cannot realistically be a strict requirement.

For this package specifically – imo its piracy seems to make sense, unifying this piracy in a single place is its whole point.

1 Like
# This is type piracy, but is fully generic. If every package which wants to have its
# module callable uses this package, no problems with multiple definitions should occur.
(x::Module)(varargs...; kwargs...) = Val(x)(varargs...; kwargs...)

Nope, not only is it unconditionally type piracy, all packages using CallableModules would interfere with the potential Core/Base implementation and likely break something. This gets worse if CallableModules gets versions that aren’t strictly aligned with Julia versions. These sorts of piracy packages are tolerable as personal Base hacks, but not as widespread dependencies.

Secondly, this does not work as stated.

  1. The premise that the shell command and the underlying module share a name is false. The documented examples ALL specify names in the [apps] section of Project.toml that differ from the names of the package or submodules. Commands also do not have a one-to-one correspondence with modules because they also configure default command-line flags for the Julia process.
  2. Apps are documented to require the macro call @main, which resolves to a function name main after some background registration for Apps and automatic Main.main calls. @module_main does not do any of that and explicitly supports names other than main.

I really don’t understand why you claim this package has anything to do with Apps when it already does exactly what it’s named: make modules callable by forwarding to an annotated function.

The above method is unambiguously type piracy, by any definition. There ought to be nothing special to consider here, independently of any intended application of the package or whatever else. It is really a “textbook example” of the worst kind of type piracy.

To spell it out:

  • Suppose your package gets registered.

  • Suppose the next day some other package registers the same method.

  • Both packages get dependencies.

  • The ecosystem is now fractured!

Alternatively, suppose JuliaLang/julia itself defines the same method.

This does not make sense. Functions are merely an example of a callable object. It is not even clear when should a callable object be (or not be) considered a function. Basically, “function” is not formally defined, sometimes it means “value of type Function”, but largely I would say “function” is just a synonym for “callable object”.

That is like shooting yourself in the foot to be safe from foot guns.

One way to look at this: if your package gets registered, to be fair and consistent, other packages that define the same method ought to be allowed to be registered. So there is really no attempt at “coordination” here. What do you mean by “coordination”?

To achieve actual coordination, you would have to make a PR to JuliaLang/julia and get it merged and released.

I feel with you here. The current solution regarding apps seems overly complicated from the UX perspective. As far as I gather, a design goal for apps is that a single package should be allowed to have multiple entry points, thus the app names were decoupled from the package name. But I agree with you about this making things more complicated. I feel some piece may still be missing regarding the apps design.

Thank you.

I feel I would need a clear picture of a concrete application (or intended user interface) in my head before I could start thinking about this. I guess I have not used or created enough apps yet.