Extend `Base.+` but don't export it by default

In my module A I have defined + for symbols.

julia> module A 
           import Base: +

           +(x::Symbol, y::Symbol) = :(+($x, $y))
       end
A

I would like to use it in some module B, but since adding 2 symbols might be error prone, I don’t want it to be exported to other modules. Yet, since +(::Symbol, ::Symbol) extends Base.+, it becomes automatically available to all modules whenever A is loaded. E.g. in REPL / Main module:

julia> :x + :y
:(x + y)

Is there a way to define +(::Symbol, ::Symbol), but keep it private unless explicitly imported?

No. Base has one set of method tables for +, and all other modules that use Base’s + will see any changes you make to it. That’s why this practice is known as “type piracy” and is discouraged. Extending methods of another module’s generic function on a type signature which is all types from other modules can change the behavior of unrelated code when your module is loaded. Either use a different function/operator name, or one that’s not imported from another module, or make sure at least one of the types in the signatures you extend are types you’ve defined in your own module. You could make a simple wrapper type around Symbol to avoid changing the behavior of other code.

2 Likes

We should add a type piracy section to the style guide.

5 Likes

Fair enough. Then a follow up question: what other operators can be defined? I noticed that, for example:

++
ˆˆ

aren’t defined in base, but can be defined in user module, while:

**
--

aren’t defined and are considered invalid operators.

So is there a table of available operators or some rules to find them?

I believe that list is here: https://github.com/JuliaLang/julia/blob/7eadb55e2d43edad1fc79adf0eb01d975562880d/src/julia-parser.scm#L9-L30

\oplus may work for you. It seems free.

julia> ⊕
ERROR: UndefVarError: ⊕ not defined

julia> ⊕(x::Symbol, y::Symbol) = :(+($x, $y))
⊕ (generic function with 1 method)

julia> :a ⊕ :b
:(a + b)
1 Like

Love it! Thanks!

For reference, ⊗ (\otimes), as well as dotted versions .⊕ and .⊗, are also available for definition.

Not needed on current master

julia> ⊕(x::Symbol, y::Symbol) = :(+($x, $y))
⊕ (generic function with 1 method)

julia> :a ⊕ :b
:(a + b)

julia> [:a,:c] .⊕ [:b,:d]
2-element Array{Expr,1}:
 :(a + b)
 :(c + d)
2 Likes

In my case I need a bit different definition:

.⊕(x::Symbol, y::Symbol) = :($x .+ $y)

This might be inconsistent with general broadcasting rules, so that’s why I try to keep such things private to my module.

While it is possible to define this in 0.6 with

Base.broadcast(::typeof(⊕), x::Symbol, y::Symbol) = :($x .+ $y)

I wouldn’t recommend it. The problem is that this method will not get called in various cases, e.g. if you combine it with other dot operations, due to loop fusion. Even if you just do (:x .⊕ :y) .⊕ :z, it will fuse into broadcast((x,y,z) -> (x ⊕ y) ⊕ z, :x, :y, :z).

Basically, in 0.6 you shouldn’t be thinking of .⊕ or .+ as operators by themselves, but rather as syntactic sugar for a (fusing) broadcast call.

Ah, intersting. Will it still be possible to manually construct expression like :((x .⊕ y) .⊕ z) or :((x .+ y) .+ z)?

Yes, expressions involving dot operators are still valid expressions and parse just fine. They get converted to broadcast calls during lowering.

Actually, this is possible, and even very easy:

module A
  +(args...) = Base.:+(args...)
  x::Symbol + y::Symbol = :(+($x, $y))
end

module B
  import Main.A: +
  @show :a + :b #> :(a+b)
  @show 1 + 2   #> 3
end

:a + :b # Error
4 Likes

There are no possible performance problems with having to go through the Vararg function to reach the Base one?

That is pretty slick @MikeInnes. Could you explain the difference between

+(args...) = Base.+(args...)

and,

+(args...) = Base.:+(args...)?

I’ve never seen that second idiom before and my playing around in the repl with your example points to that being important for this thing to work.

@kristoffer.carlsson correct. In any case where Base.:+ would have been statically resolved, it’s easy for the compiler to inline A.:+, effectively eliding it completely. Forwarding functions like this sometimes runs the risk of overloading the inlining heuristic, but that’s easy to avoid with an @inline annotation.

@nsmith Base.:fft is the same as Base.fft, i.e. it gets that named object from another module. With operators Base.+ doesn’t work (I assume because it’s ambiguous with broadcast operators like x.+y) so the colon version is required.

1 Like