Problem making two custom number types work together

I’ve defined a type of symbolic number, FastDifferentiation.Node,

struct Node <: Real
    ...
end

that causes this error when used with Quaternions.jl:

julia> using FastDifferentiation,Quaternions

julia> @variables x y z w #creates 4 FastDifferentiation.Node instances
w

julia> q = Quaternion(x,y,z,w)
Quaternion{FastDifferentiation.Node}(x, y, z, w)

julia> a = q/w
ERROR: MethodError: /(::Quaternion{FastDifferentiation.Node}, ::FastDifferentiation.Node) is ambiguous.

Candidates:
  /(q::Quaternion, x::Real)
    @ Quaternions ~/.julia/packages/Quaternions/UZIvR/src/Quaternion.jl:153
  /(a::Number, b::FastDifferentiation.Node)
    @ FastDifferentiation ~/.julia/packages/FastDifferentiation/TzEqR/src/Methods.jl:58

Possible fix, define
  /(::Quaternion, ::FastDifferentiation.Node)

The suggested solution

Possible fix, define
  /(::Quaternion, ::FastDifferentiation.Node)

is not practical since it would force FastDifferentiation to take a dependency on Quaternions or vice versa.

Arithmetic methods for FastDifferentiation.Node are defined using the number_methods macro, cribbed from SymbolicUtils.jl. This creates a definition for / in the FastDifferentiation.jl package:

/(a::Number, b::FastDifferentiation.Node)

Quaternion.jl also defines a method for /:

/(q::Quaternion, x::Real)

These two methods appear to be the source of the ambiguity. I want the method defined in Quaternion.jl to be called but am having trouble figuring out how.

What is the best way to fix this?

  • You could modify number_methods to not define / on FastDifferentiation.Node
  • You could use invoke at the call site to force a specific method of /: Essentials · The Julia Language
  • You could use isdefined to conditionally define the more specific / as Oscar mentions below, this is a bad idea, and it’s better to use package extensions: 5. Creating Packages · Pkg.jl

This last suggestion is a pretty bad one. Hacking this based on symbols is very brittle. The better version of this would be a package extension.

5 Likes

Without knowing more about your Node, it’s hard to say whether the following is practical and reasonable. But if it is, an idiomatic solution is to remove the definition of /(::Number, ::Node) (and likewise for other operators) and instead define a set of promote_rule to make this work. In this version, you would likely only define /(::Node, ::Node) and a variety of promote_rule.

1 Like

Julia is quite good at having different numerical types interoperate.
What you are seeing is the generality of Number; it is the most abstract supertype for all things that are in some manner “computationally algebraic”.

One sort is numerically valued
e.g. {Int8, Float32, Rational{Int64}, Complex{Float64}, Quaternion{Float32}}

      Integer, Rational, AbstractFloat ∈ subtypes(Real)
      `Real`, Complex, Quaternion ∈ subtypes(Number)
 
      Complex and Quaternions are operationally available
          through algebras over 2-tuples and 4-tuples.
          [and in other more mathy ways]

The other sort is implicitly evaluable, while carrying no explicit value.
These are the symbolic-numeric types. They may be symbols or structured pairs/triples…, or evaluation trees with symbolic or symbolic + guiding adornments as leaves. e.g. FastDifferentiation.Node. See:

====

Quaternion division by a scalar is definable using the operations
abs2, complement, reciprocal, and quaternion construction.

While Nodes are not traditional (numerical) scalars, Real values are scalars.
We have supertype( FastDifferention.Node ) == Real.

for illustrative purposes only way

using Quaternions
using FastDifferentiation

const Q = Quaternion
const Node = FastDifferentiation.Node

sq(q::Q) = q.s; 
xq(q::Q) = q.v1;
yq(q::Q) = q.v2; 
zq(q::Q) = q.v3;

complement(q::Q) =  Q(sq(q), -xq(q), -yq(q), -zq(q))

function reciprocal(q::Q)
   a = abs2(q) # sum of |element|^2
   c = complement(q)
   Q( sq(c) / a, xq(c) / a, yq(c) / a, zq(c) / a )
end

Base.:(/)(q::Q, n::Node) =
    Q( sq(q) / n, xq(q) / n, yq(q) / n, zq(q) / n )

@variables a b c d
q = Quaternion(a, b, c, d)
qoa = q / a
# Quaternion{FastDifferentiation.Node}((a / a), (b / a), (c / a), (d / a))
qod = q / d
# Quaternion{FastDifferentiation.Node}((a / d), (b / d), (c / d), (d / d))

difference = qoa - qod
#=
Quaternion{FastDifferentiation.Node}(
((a / a) - (a / d)), ((b / a) - (b / d)), ((c / a) - (c / d)), ((d / a) - (d / d))
)
=#

product = qoa * qod
#=
Quaternion{FastDifferentiation.Node}(
((((a / a) * (a / d)) - ((c / a) * (c / d))) - (((b / a) * (b / d)) + ((d / a) * (d / d)))), 
((((a / a) * (b / d)) + ((b / a) * (a / d))) + (((c / a) * (d / d)) - ((d / a) * (c / d)))), 
((((a / a) * (c / d)) + ((c / a) * (a / d))) + (((d / a) * (b / d)) - ((b / a) * (d / d)))), 
((((a / a) * (d / d)) + ((d / a) * (a / d))) + (((b / a) * (c / d)) - ((c / a) * (b / d))))
)
=#

The problem does seem to be with the methods defined with Number arguments. I cribbed the number_methods macro from SymbolicUtils.jl to automatically generate arithmetic, trigonometric, etc., methods for the Node type.

The two lines marked problematic in this code snippet from number_methods are causing the problem:

  expr = quote
            (f::$(typeof(f)))(a::$T, b::$T) = $rhs2
            (f::$(typeof(f)))(a::$T, b::Real) = $rhs2
            (f::$(typeof(f)))(a::Real, b::$T) = $rhs2
            (f::$(typeof(f)))(a::$T, b::Number) = $rhs2 #This is a problematic line
            (f::$(typeof(f)))(a::Number, b::$T) = $rhs2 #This is a problematic line
        end

Are the two lines that generate functions with Number arguments necessary? I commented them out and all my tests pass. Maybe there are obscure cases where you need them? Does anybody know why they are there?

For sure they cause exceptions to be thrown for Quaternion{Node} and Complex{Node} types and if they are removed the errors go away.

I would err on the side of not including the ::Number methods until you have an obvious use case for them.

::Number methods give a default implementation for how your type should interact with all other custom number types. I think this makes sense if your type is e.g. a simple wrapper over an Int or a Float, but if it’s anything more complex you probably want the user to make a deliberate decision.

You can also see that the number_methods macro has options skip_basics and only_basics. So the authors clearly expected some situations where you wouldn’t want to define the full set of methods.