Proper way of organizing code into subpackages

Are you proposing that the compiler disallow multiple dispatch?

Unfortunately, I don’t think FromFile really achieves the dependency management that you want it to achieve. I’m away from my computer, so I’ll just link to two old comments, here and here, that explain why I think @from doesn’t provide real dependency management.

4 Likes

I am not advocating prohibiting multiple dispatch.

What I am suggesting is that the compiler should prohibit exactly the kind of shenanigans you link to as being the “problems” with FromFile. That is to say, mutating global method tables wrt types you don’t own, etc.

Anyway, I’ll leave things there. Obviously I am no longer a Julia user anyway, so I don’t really have any stake in this any more.

The second example that I linked to uses Int and Float64, but it could just have easily used custom types that I own and it would have come to exactly the same conclusion. Besides, in that example I own the function bar, so it’s perfectly valid to define Int and Float64 methods for bar.

Would this be similar in concept to a sealed class from Kotlin or Java where the subclasses must be known at compile time?

I’ll coin the term “Sealed Module” to mean a module from which it is error to explicitly or implicitly import any of its contents into another module. You can still do using to bring them into your namespace, but you would be prevented from extending the methods.

This is why I always appreciate your thoughts when you post, you’ve got experience on both sides and that’s really useful. Especially when it comes to comparisons between Julia and JAX

5 Likes

I am starting to feel (at least, based on those comments that you linked) that you are suggesting that since all methods are “global” they should not be organized into modules—if this is not what you’re implying please correct me, as I do not mean to put words in your mouth. I very much agree with @patrick-kidger that seems like a dangerous way to live.

In your example

module DemoPackage
       
	module A
		struct S end
		foo(::S) = 1
	end
	
	module B
		import ..A: foo
		struct T end
		foo(::T) = 2
	end
	
	module C
		import ..A: foo
		struct U end
		foo(::U) = 3
	end
	
	import .A: S
	import .B: T
	import .C: foo, U
	export foo, S, T, U

I would expect foo (upon importation from DemoPackage) to be only defined on type U (and subtypes). I do not think the choice of which module to import it from should be arbitrary. If one wants to be able to call foo(S) then I think one should have to import the method from A as well.

I’m not agreeing not disagreeing, one may prefer one way or the other. But when you do import ..A: foo you are very explicit saying that you want to extend the functionality of the function defined in A, and that means what it means in Julia: adding a method to a global method table of that function.

One can always not import the function from the other module, and define a new function. And we can then, if wanting to be strict use something like:

julia> module A
           foo(x::Int) = 1
       end
Main.A

julia> module B
           foo(x::Float64) = 2
       end
Main.B

julia> import .A: foo

julia> foo(x::Float64) = B.foo(x)
foo (generic function with 2 methods)

to then only extend the functionality of the function to the type you want.

2 Likes

Gotcha, thanks for clarifying. I think I would almost always prefer something more like the latter pattern (or really, something like)

module A
    foo(x::TypeOwnedByA) = ...
end

module B
    foo(x::TypeOwnedByB) = ...
end


import .A: foo
import .B: foo

I feel pretty strongly that if I define a method in module A, and I later import that method in module B, that the behavior should only depend on how it’s defined in A (or extended in B), and not whatever extensions I or some other developer have added in a tertiary module C, unless I explicitly import the method from C as well.

1 Like

Type-piracies apart, that is what you get, as the fact that a function has a method for a type you are not aware about should not be important.

Alternatively one can simply not import at all the functions, and just use them qualified by the modules (i. e. call with B.foo(x), A.foo(x)) which is somewhat more organized, but you are giving up on the flexibility of multiple dispatch then.

1 Like