How to convert UniformScaling with single entry to Number?

Copying a question from Zulip given that it may be useful in general to discuss in Discourse…

A good trick to perform algebraic operations with both arrays and numbers generically is to use LinearAlgebra.I instead of 1:

julia> f(x) = x*I
f (generic function with 1 method)

julia> f(1)
UniformScaling{Int64}
1*I

julia> f(ones(3,3))
3×3 Array{Float64,2}:
 1.0  1.0  1.0
 1.0  1.0  1.0
 1.0  1.0  1.0

The problem I have is that the function f is used a lot with simple numbers, and the return type of f(1) is not a Number. How do we get around this issue? Is there a function in Base that converts a UniformScaling with a single entry to a simple Number?

If I can provide a simple number in most cases, I will have less issues downstream, and the compiler will likely perform more low-level optimizations?

I know that a UniformScaling object a with a single entry has the field a.λ with the actual number it represents. I wonder how to retrieve this number elegantly, possibly without performance penalties.

maybe you can use one depending how much laziness you need:

julia> one(3)
1

julia> one(rand(3,3))
3×3 Array{Float64,2}:
 1.0  0.0  0.0
 0.0  1.0  0.0
 0.0  0.0  1.0

or you can add a method to Number (or name it something else):

julia> Base.Number(a::UniformScaling) = a.λ

julia> Number(I*3)
3

julia> Number(I*3.0)
3.0
1 Like

How to create a method that works with arrays too? In other words how to dispatch on different kinds of UniformScaling and retrieve the underlying array vs. number?

actually, if your f can return array, why does it need to be a Number? UniformScaling should act as an array in any cases.

1 Like

Because most packages out there fall into this Julia anti-pattern where the code constrains the type of functions to numbers, e.g. f(x::Number) = ... prematurely. Also, I am assuming that the compiler can do more magic with the raw numbers in general compared to a more complicated struct.

The code I am referring to is in this PR: https://github.com/JuliaEarth/Variography.jl/pull/33

I would like to be able to do g = GaussianVariogram() + SphericalVariogram() and then evaluate the function with g(1.0) and get a number.

I followed your suggestion and added:

raw(a::UniformScaling) = a.λ
raw(a) = a

to extract the underlying number when we get a uniform scaling object.

Thanks for the help.

Haven’t thought about it for too long but:

julia> (f(x::T)::T) where T = x*I
f (generic function with 1 method)

#not sure if this interferes with something else...
julia> Base.convert(::Type{T}, u::UniformScaling{T}) where T<:Number = u.λ

julia> f(1)
1

julia> f(3.0)
3.0

julia> f(rand(2,2))
2×2 Array{Float64,2}:
 0.054866  0.471527
 0.938986  0.538114

julia> g(x)= x*I
g (generic function with 1 method)

julia> using BenchmarkTools

julia> @btime g($1)
  0.037 ns (0 allocations: 0 bytes)
UniformScaling{Int64}
1*I

julia> @btime f($1)
  0.036 ns (0 allocations: 0 bytes)
1

julia> @btime f($1.0)
  0.037 ns (0 allocations: 0 bytes)
1.0

julia> @btime g($1.0)
  0.035 ns (0 allocations: 0 bytes)
UniformScaling{Float64}
1.0*I

julia> @btime g($(rand(3,3)))
  43.347 ns (1 allocation: 160 bytes)
3×3 Array{Float64,2}:
 0.543145  0.0292201  0.670174
 0.754201  0.290328   0.459809
 0.896825  0.678289   0.332216

julia> @btime f($(rand(3,3)))
  43.347 ns (1 allocation: 160 bytes)
3×3 Array{Float64,2}:
 0.32656    0.582854  0.952984
 0.0462256  0.589197  0.0384532
 0.972349   0.160872  0.807338
1 Like

Nice trick with conversion methods :+1: I will follow the simple approach above with a raw function to avoid thinking about it too much.

I think that effort is best allocated to fixing these instead of introducing a workaround.

That may not be true, especially if the struct just wraps a number.