Multiple dispatch and modules

After watching Stefan Karpinski’s talk called The Unreasonable Effectiveness of Multiple Dispatch I started wondering how multiple dispatch and modules fit together.

To put it simply, if type A is defined in module a and type B is defined in module b, then where to define a method f that takes both an A and a B as its arguments?

From the client’s perspective, is f automatically imported if A and B are in scope?

Stefan talks about the exponential expressive power of multiple dispatch. The module systems I am familiar with have a tree-like structure and, as far as I can tell from skimming through the docs, the module system of Julia is no different: a module can have multiple children but only one parent. However, it seems to me that, intuitively, I would want to put f into a module that is a child of both a and b. In other words, it seems to me that the expressive power of a tree-like module system is at odds with the exponential expressive power of multiple dispatch.

Admittedly, I haven’t read every line of the manual, just browsed through it because I felt like it contains a lot of information I already know and I did not seem to find the things I was looking for.

Either one, whichever you prefer. Something important to know is that method tables are global. That means if you have access to a function f, you also have access to all of its methods no matter where they are defined. But you do need to be able to access the function, and that follows the tree pattern you describe. Though, Base acts like a broad root, defining many functions that you can extend with your types and interoperate with other packages.

3 Likes

It’s common to make a third module FooBar that knows about both Foo and Bar. There are many examples like SampleChainsDynamicHMC that knows about SampleChains and DynamicHMC, or AbstractGPsMakie that knows about AbstractGPs and Makie.

3 Likes

Thank you both for your replies!

So how do people find the functions they need in Julia? Do they check the module Foo was defined in then check the module Bar was defined in and then look for FooBar or BarFoo?

How about functions with more than 2 arguments? Are there modules like FooBarBaz that contain functions that take a Foo, a Bar and a Baz?

methodswith is a useful way to find all methods applying to at least an argument of the provided type (regardless of where these methods are defined).

2 Likes

you can just using A and B, and then start define your f(x::a_struct, y::b_struct).
you can define such f in any place where there is A and B, which is not a child of A or B. a child module, is a module defined inside another module. And as far as i know, people don’t use submodule in julia world as much as python world. Since in python every file is a module, but in julia, people can seperate code in different files and then include them.

and for the question of how I find the proper function for object as newbie: Google it.

for example it’s hard to remember where to find a random ralated function, when you have Distrubutions, Random, StatsBase and Base.rand, and they do interact with each other.
i just guess how people would call that function, and search for it…

1 Like

The point that Eric made above means that you don’t need those. If you write generic functions, then the function doesn’t have to know about the types it will be called with. The only time when you need to know a type is for specializing functions, and that can happen anywhere.
It’s probably best with an example. Consider:

julia> module Foo
           function foo(first, second, third)
               return sum(first[second] .+ third) ^ second
           end
       end
Main.Foo

Foo.foo does not know what it’s arguments are, nor does it care. What matters is, that the parts of which foo is composed are defined. So we need to have the methods for

result1 = first[second] # getindex(first, second)
result2 = .+(result1, third)
result3 = sum(result2)
result4 = result3 ^ second # ^(result3, second)

And where these live doesn’t matter much.

julia> module Bars
           struct Bar end
           const bar = Bar()
           Base.getindex(::Bar, ind) = (2*ind)^(ind/2)
           Base.getindex(::Bar, ind, tail...) = bar[ind] + bar[tail...]
       end
Main.Bars

julia> module Baz
           abstract type AbstractBaz end
           # suppose we define our AbstractBaz interface so that every concrete Baz need getvalue(::Baz) defined
           getvalue(b::AbstractBaz) = error("The AbstractBaz interface demands that you define a specialized `getvalue(::$(typeof(b)))` method.")
           Base.:*(something::T, b::AbstractBaz) where {T} = *(something, getvalue(b))
           Base.:/(b::AbstractBaz, something::T) where {T} = /(getvalue(b), something)
           Base.:^(something::T, b::AbstractBaz) where {T} = ^(something, getvalue(b))
       end
Main.Baz

So we have Foo.foo, Bars.Bar and Baz.AbstractBaz that don’t know each other, but all are accessible from Main. We can now subtype AbstractBaz with yet another type

julia> struct MyBaz <: Baz.AbstractBaz
           value::Int
       end

and we have to respect the interface of course

julia> Bars.bar[MyBaz(2)]
ERROR: The AbstractBaz interface demands that you define a specialized `getvalue(::MyBaz)` method.
Stacktrace:
 [1] error(s::String)
   @ Base .\error.jl:33
 [2] getvalue(b::MyBaz)
   @ Main.Baz .\REPL[3]:4
 [3] *(something::Int64, b::MyBaz)
   @ Main.Baz .\REPL[3]:5
 [4] getindex(#unused#::Main.Bars.Bar, ind::MyBaz)
   @ Main.Bars .\REPL[2]:4
 [5] top-level scope
   @ REPL[7]:1

julia> Baz.getvalue(b::MyBaz) = b.value

julia> Bars.bar[MyBaz(2)]
4.0

and then those things can work together seamlessly:

julia> Foo.foo(Bars.bar, MyBaz(3), [2,4,6])
176471.77731935095

Because when the compiler encounters Foo.foo(::Bars.Bar, ::MyBaz, Vector{Int}) it can look up all the parts that make up foo and compose a specialized version of foo with those as long as they are accessible in Main.

3 Likes

Thank you all again for responding!

Let me explain my train of thougth step by step because I’m starting to think that I have made some false assumptions.

There are languages that allow developers to define functions inside of the definition of a type. Such functions are called methods.

In class-based programming, methods are defined within a class, and objects are instances of a given class.
Method (computer programming) - Wikipedia

This is how a method is defined in Python (note that f is defined inside of MyClass):

class MyClass:
    """A simple example class"""
    i = 12345

    def f(self):
        return 'hello world'

Now, there is an obvious question: What are the advantages of defining a function inside a class?

The less important here is encapsulation. In many languages, functions defined within a class are allowed to access the private fields of the class whereas functions defined elsewhere are not allowed to do so.

This is correct C++ code:

class MyClass {
	private:
		int x = 0;
	public:
		int y = 0;

	void inc() {
		++x;
		++y;
	}
};

int main() {
	MyClass mc;
	mc.inc();
}

But the following doesn’t complie:

class MyClass {
	private:
		int x = 0;
	public:
		int y = 0;
};

void inc(MyClass &mc) {
	++mc.x;
	++mc.y;
}

int main() {
	MyClass mc;
	inc(mc);
}
error: ‘int MyClass::x’ is private within this context

More importantly, if a function f is defined inside a class, then it lives inside the namespace of that class. See for instance: Python Scopes and Namespaces. Let’s return to the previous Python example and look at how f is invoked:

class MyClass:
    """A simple example class"""
    i = 12345

    def f(self):
        return 'hello world'

mc = MyClass()

assert mc.f() == 'hello world'
assert MyClass.f(mc) == 'hello world'
#assert f(mc) == 'hello world'

Clearly, f can not only be invoked using method syntax mc.f() but also using function syntax MyClass.f(mc). In this case, however, f needs to be prefixed with MyClass because that is the namespace it lives in. Try uncommenting the last line and see what happens.

This mc.f() syntax is important because it allows for dynamic dispatch. It, basically, says: Find f in the namespace of the type mc is an instance of and call f with mc. However, in Python, dynamic dispatch can also be achieved using the @singledispatch annotation. Note how using @singledispatch does away with method syntax.

Encapsulation aside, since polymorphism can be achieved by means other than defining functions inside the body of a type (at least in Python which is enough for proof of concept, I think), why do people define functions within classes?

For me, the answer seems to be convenience and habit. In particular, as the author of a package, what do I do in class-based object-oriented languages? I, mentally, assign functions to types and implement those functions within the bodies of classes. And this is what my question is about! In Julia, a language where I cannot (and also do not want to) implement fuctions inside types, I seem to have a lot of freedom that I wasn’t exposed to before. By the way, I’ve realized that I would have a similar problem in C, even though C features neither multiple dispatch nor modules… hence the title :grinning:

Even within a single file, do I write this?

struct A{};

struct B{}; 

struct C{};


void f() {}

void g(struct A a) {}
void h(struct B b) {}
void i(struct C c) {}

void j(struct A a, struct B b) {}
void k(struct A a, struct C c) {}
void l(struct B b, struct C c) {}

void m(struct A a, struct B b, struct C c) {}

or this?

void f() {}


struct A{};

void g(struct A a) {}


struct B{}; 

void h(struct B b) {}


struct C{};

void i(struct C c) {}


void j(struct A a, struct B b) {}
void k(struct A a, struct C c) {}
void l(struct B b, struct C c) {}


void m(struct A a, struct B b, struct C c) {}

maybe this? (or something similar because of the missing forward declarations)

void f() {}


struct A{};

void g(struct A a) {}
void j(struct A a, struct B b) {}
void k(struct A a, struct C c) {}
void m(struct A a, struct B b, struct C c) {}


struct B{}; 

void h(struct B b) {}
void l(struct B b, struct C c) {}


struct C{};

void i(struct C c) {}

I think, I, unconnsciously, came to the conclusion that a more powerful module system would make this question a no-brainer. Where do I define a function f that takes an A, a B and a C? Well, in the 'A-B-C' module… And how do I find functions that take exacly an A, B and C? Well, I just bring A, B and C into scope and let the module system bring in f for me automatically.

But maybe it is imprtant to consider other factors and not to let the combination of types a function takes alone dictate its location…

One last thing I missed to acknowledge is that, as far as I see, functions defined within a class are the reason why it is easy to implement an online documentation like cppreference.com where I can search for a type and immediately see what actions can be performed on that type.

I often define methods on MyFoo class in order to ensure it conforms to the AbstractFoo interface. A formal interface makes it easy to ensure I’ve done it right. It’s possible to do this with dispatch too, but Julia doesn’t provide a standard way (yet?).

1 Like