Interface for `Number`

I recently ran into an issue overriding zero for my own Number:

julia> immutable Infinity <: Number end

julia> Base.zero(::Infinity) = 0

julia> import Base: +

julia> +(::Infinity,::Int) = Infinity()
+ (generic function with 181 methods)

julia> reduce(+,[Infinity(),Infinity()])
ERROR: MethodError: Cannot `convert` an object of type Int64 to an object of type Infinity
This may have arisen from a call to the constructor Infinity(...),
since type constructors fall back to convert methods.
Stacktrace:
 [1] _mapreduce(::Base.#identity, ::Base.#+, ::IndexLinear, ::Array{Infinity,1}) at ./reduce.jl:260
 [2] reduce(::Function, ::Array{Infinity,1}) at ./reduce.jl:321

The issue appears to be that zero{T<:Number}(::T) is expected to return a T.

Is this a bug, or is this part of the Number “Interface”? Is the Number “Interface” written down anywhere?

1 Like

I’d say it’s part of the interface. If you can’t expect that zero(::T) returns a type T, then lots of codes would be potentially type-unstable.

I don’t think this interface is written down anywhere, but it should get documented better.

OK that’s reasonable. Yes, if built in types have an interface that Base Julia assumes (like for Number) it should be written down.

Even better, there should be a testnumberinterface(Infinity) to test that all the contracts that Base expects are satisfied for my type. (Also, testiterator, testabstractarray, etc.)

3 Likes

Is this new in v0.6?

For v0.5, the following works:

immutable Infinity <: Number end

Base.zero(::Infinity) = 0

import Base: +
+(::Infinity,::Int) = Infinity()
+(::Infinity,::Infinity) = Infinity()

reduce(+,[Infinity(),Infinity()])

Yes it’s 0.6 (sorrry, should have mentioned this was a surprising new behaviour of something that worked fine in 0.5)

Note that the precise problem is not that zero() returns a different type, it’s that the value returned by zero() cannot be converted to Infinity. Defining this (incorrect) method fixes the issue:
Base.convert(::Type{Infinity}, x::Integer) = Infinity().

This sounds like an unintentional restriction to me. It could be worth filing a bug. Ideally what should happen is that either the error would only be thrown when the collection is empty (i.e. when zero must be used), or we would accept a type instability by returning 0 disregarding its type. But these are complex issues.

Created an issue: https://github.com/JuliaLang/julia/issues/21097

1 Like

I was suggested this topic when I was about to start a new development discussion titled “What is a Number”

In general, interfaces and abstract types in Julia are pretty well documented and defined, (e.g. AbstractArray, iterator interface, …). It is suprising to see so little documentation about what it constitutes to be a <:Number, both mathematically and in terms of interface. With currently many active PR’s about making Base code consistent with various number types that are not <:Real or <:Complex, this would probably be good to have.

So first the mathematics, what are the properties of a type in order to be a subtype of Number? Is it supposed to represent elements from a ring , a division ring (I think that this is still fine for Integers, as long as they get promoted to another type under division), a normed division ring, a field (I guess not since non-commutativity of e.g. quaternions is the cause for some recent PRs).

2 Likes

I run into a related problem frequently: if I get a container with element type T <: Number, and create a result S, which I need to precompute, what is the best way to do this? Think of T eg as a ForwardDiff.Dual, or a similar construct related to a tape from one of the reverse AD packages.

Eg if I know I will be using something that can be based on the four basic arithmetic ops (ie a field), I frequently calculate

S = typeof(one(T)/one(T))

If I have multiple inputs,

S = typeof(one(T1)/one(T2)/one(T3))

also seems to work, but always feels like a hack. A fieldclosure(T1, T2, ...) would be nice.

4 Likes

So first the mathematics

This is the wrong approach I think. I doubt there’s any useful mathematical definition that includes everything that needs to be <: Number, e.g., floating point arithmetic, interval arithmetic, quaternions, dual numbers, etc.

It’s much better to define Number as an interface, and anything that implements the Number interface is acceptable. So it’s about overriding +, -, *, /, etc., not about mathematical consistency.

2 Likes

Yes I was exaggerating, I know it’s not a good idea to try to map mathematical structures onto the type hierarchy for various reasons. But I do mean establishing the kind of mathematical properties that are expected of a Number, i.e. I guess commutative addition operator, associative but not necessarily commutative multiplication operator, is norm part of the required interface, …

See here for a variant of fieldclosure in the wild. I also implemented such a thing in more then one place…

3 Likes

It would be great to either (in order of my preferences)

  1. make this part of Base.
  2. package it separately,
  3. or at least export as supported API.

I end up coding a variant of this many times.

1 Like

There is also this package available

Be careful that numbers also include dimensionful quantities, ala Unitful.jl, so ideally generic code should do type computations in a dimensionfully correct way. For example one(T) returns the type of the multiplicative identity for T, which strips away any units.

Writing truly type-generic numeric code, especially handling the dimensionful case, is tricky and takes testing, though.

2 Likes

Which is why existing best practices should be shared, possibly in a package.

1 Like

In my new upcoming Grassmann.jl package, I am providing a generalized number type for which a product algebra is defined. The parametric VectorSpace type system with direct-sum capability helps encode the mathematical properties of each mathematical space, making it possible for many of the computations to be entirely pre-allocated in memory or cached by input. By default, it is defined over a scalar field taking Number types, but I have decided to provide an additional convenient feature which can make extending the entire product algebra to different number types a breeze.

Due to the abstract generality of the code generation of the Grassmann product algebra, it is possible to extend the entire set of operations to other kinds of scalar coefficient types. By default, the coefficients are required to be <:Number . However, if this does not suit your needs, an alternative scalar product algebra can be specified with

generate_product_algebra(SymField,:(Sym.:*),:(Sym.:+),:(Sym.:-),:svec)

where SymField is the desired scalar field and Sym is the scope which contains the scalar field algebra for SymField . Currently, it is feasible to enable symbolic scalar computation, by specifying the operations. It should be straight-forward to substitute any other extended algebraic operations and for extended fields.

My hope is that it will be extensible enough to handle different promotion situations, such as propagating dimensionful quantities, which I have not given much thought to yet.