Good template for defining a custom type with an addition method

Hello,

I’m implementing for the first time a custom type with a custom addition method (i.e. the + operator). Unless I missed something in the manual, I didn’t find a paragraph focused on the topic.

After several iterations, I arrived at this result which seems to work (re-expressed for a dummy custom Point type)

module MyModule
    import Base
    export Point, +

    struct Point{T<:Real}
        x::T
        y::T
    end

    function Base.:+(a::Point{<:Real}, b::Point{<:Real})
        return Point(a.x + b.x, a.y + b.y)
    end
end

using .MyModule

a = Point(1.0, 2.0)
b = Point(3.0, 3.0)

print(a+b)

My questions are:

  1. is it indeed the correct/recommended approach to define a custom type in a module?
  2. is there a dedicated section of the manual that I missed?

If indeed there is no dedicated section in the manual, here is a description of what non trivial pieces of information I think I had to gather using the current state of the documentation.

To arrive at the above result, I needed to merge elements from:

  • the Methods chapter, which explains methods and redefinition, but says nothing about redefining base methods
  • the Interfaces chapter, which focuses a lot on container types (for indexing…) but doesn’t mention the “simple algebraic operations” interface: +,-,*,/…)
  • plus a bit of Parametric Composite Types (e.g. for the correct method signature to accept all arguments of type Point{T} where T is a subtype of Real)

also, some background knowledge about modules is needed. In particular, unlike methods of object oriented languages like Python, there is a need to export both the type and each of its methods (unless I missed something).

For comparison, although the concept of special methods is not as strong in Julia, there is a dedicated section in the Python language reference 3 Data model / 3.3. Special method names which covers addition, comparison, indexing…

To summarize, I think the elements I missed (in the process of finding a solution that works) are:

  • the explanation about the need to import Base and the need to redefine Base.:+ (insteadi of just implementing function +(...) and also, as a detail, not forgetting the : prefix… which is not needed when defining function +(...))
  • and then the need to export + (but not Base.+).

I agree that this example is not explained well in the manual. At least I did not find a very clear explanation by searching briefly.

The important thing to realize is that + does not universally denote the same function. Function names are no different than variable names - they share the same namespace if you wish. For example you could extend your code with

let - = +
    println(a-b)
end

and it would just work and do the same as your a+b.

What does that mean for your example?

  1. When you want to add a method to + you really mean + from Base, which needs to be written as Base.:+ in a function definition (I think this syntax is explained somewhere but I didn’t find it quickly - somewhat confusingly when importing something both import Base.+ and import Base.:+ work).
  2. To add a method to an existing function from a package you are using, you need to use its full name in the definition function Base.:+(...) as function +(...) would introduce a new local function named + and shadow Base.:+ in the local scope, meaning you could not add things anymore.
  3. On the other hand, if you import the other module, then function +(...) would NOT define a new function and just add a new method to the existing function! This a crucial difference between using and import, that’s explained in the FAQ but perhaps could use an example for illustration.
  4. So generally you need to either import modules to add methods to their functions or use the long form of the method’s name (like Module.function) in the function definition. I personally would always recommend the latter as that makes it more clear to the reader that an external function is extended (imo).
  5. You don’t need to (re)export + from MyModule as it is identical with Base.:+.

So you can simplify your code to:

module MyModule
    export Point

    struct Point{T<:Real}
        x::T
        y::T
    end

    function Base.:+(a::Point, b::Point) # no need to repeat the type parameter if unused
        return Point(a.x + b.x, a.y + b.y)
    end
end

using .MyModule

a = Point(1.0, 2.0)
b = Point(3.0, 3.0)

print(a+b)
4 Likes

That’s only a question of how you want to organize your code.

Thanks a lot for the explanations. In particular the reference to the FAQ What is the difference between “using” and “import”?, because I was now wondering how come I had been using the + operator all these years without first import/using Base…

(As a side note, I cannot find the reference which explains how Base is automatically “used” in environment.)

In the end, I agree with your preference (item 4.) to use the qualified name Base.:+ for adding a method. This is clearer saves the need to add an extra import statement which, in a complex code base, appears in another file…

(As a side note, I cannot find the reference which explains how Base is automatically “used” in environment.)

help?> baremodule
search: baremodule

  baremodule

  baremodule declares a module that does not contain using Base or local 
  definitions of eval and include. It does still import Core. In other words,

  module Mod
  ...
  end

  is equivalent to

  baremodule Mod

  using Base

  eval(x) = Core.eval(Mod, x)
  include(p) = Base.include(Mod, p)
  ...
  end

Ah yes baremodule, thanks.

The exact reference is indeed in the Manual in Modules / Namespace management / Default top-level definitions and bare modules:

Modules automatically contain using Core, using Base, and definitions of the eval and include functions, …