I consider the question really to be more about how it looks than if it is possible.
Instead of using
S = svd(A).S # less effective
S = singvals(A) # possibly unfamiliar
one can implement
S = svd.S(A) # internally just calls singvals
Or you can even make
svd.S(A) == Diagonal(svd.s(A))
so that the notation makes sense for once.
Anyway, It is probably not really a good idea to change Base.getproperty for other than your own functions, and svd.S(A) and svd(A).S will not be exactly equal.
But for your own functions, if the only thing that changes is what is returned, I think that would actually be a nice scheme to refer some collective function name to some subfunctions.
Here is the code template with some small benchmark:
using LinearAlgebra
struct ArgoutFunction{F<:Function,sym}
ArgoutFunction(f::F, sym::Symbol) where F<:Function = new{typeof(f),sym}()
end
function my_other_fct end
Base.getproperty(f::F, sym::Symbol) where F<:Union{typeof(svd),typeof(my_other_fct)} = ArgoutFunction(f, sym) # do NOT do this for all functions
valid_argout(::F) where F<:Function = (:USVt, :S) # do NOT overload propertynames of functions
@inline function (af::ArgoutFunction{F,sym})(args...) where {F<:Function,sym}
if sym in valid_argout(F.instance)
_argout_function(af, args)
else
getproperty((F.instance(args...)), sym)
end
end
@inline function _argout_function(af::ArgoutFunction{typeof(svd),sym}, args) where sym
if sym === :USVt
svd(args...)
else
svdvals(args...)
end
end
function test(A)
GC.gc()
svd(A); svd(A) # warm up cache
println(" .sym before:")
print(".U: "); @time svd.U(A)
print(".S: "); @time svd.S(A)
print(".Vt: "); @time svd.Vt(A)
print(".USVt: "); @time (U, S, Vt) = svd.USVt(A)
println(" .sym after:")
print(".U: "); @time svd(A).U
print(".S: "); @time svd(A).S
print(".Vt: "); @time svd(A).Vt
print(".USVt: "); @time (U, S, Vt) = svd(A)
return nothing
end
A = randn(500, 20)
@code_warntype svd.S(A) # will show you constant propagation
test(A)
println(norm(svd.S(A) - svd(A).S) / norm(A))
# .sym before:
# .U: 0.000151 seconds (16 allocations: 174.906 KiB)
# .S: 0.000082 seconds (12 allocations: 90.453 KiB)
# .Vt: 0.000149 seconds (16 allocations: 174.906 KiB)
# .USVt: 0.000143 seconds (16 allocations: 174.906 KiB)
# .sym after:
# .U: 0.000153 seconds (16 allocations: 174.906 KiB)
# .S: 0.000143 seconds (16 allocations: 174.906 KiB)
# .Vt: 0.000142 seconds (16 allocations: 174.906 KiB)
# .USVt: 0.000143 seconds (16 allocations: 174.906 KiB)
There is more to consider. Optional arguments can (under bad but realistic circumstances) fail to be propagated constantly and via that design, make your function typeunstable:
function svd_part(args...; output::Symbol=:USVt)
if output === :USVt
@inline svd(args...)
else
@inline svdvals(args...)
end
end
function test_svd_part()
A = rand(Int, 2, 2)
@inline svd_part(A; output=:S)
end
@code_warntype test_svd_part()
# Body::Union{SVD{Float64, Float64, Matrix{Float64}, Vector{Float64}}, Vector{Float64}}
Using the above, the way it is or even with additional, excessive inlining, it remains typestable:
function test_svd_S()
A = rand(Int, 2, 2)
@inline svd.S(A)
end
@code_warntype test_svd_S()
# Body::Vector{Float64}