Optional output arguments

A nice thing about MATLAB is that the caller can request multiple function outputs, i.e. you can either do:

y = foo(5);

[y, info] = foo(5);

This is quite practical because you can then easily do baz(bar(foo)) if info is not too important. I guess that this cannot be done in such a straightforward way in Julia. Is this correct?

You can also do

y, _ = foo(5)

if you don’t want the second element.

and it works with slurp as well

y, _... = foo(5)

also, in slightly more cursed way, it also works if the assignment expression looks like a tuple unpacking even of a shorter length

(y,) = foo(5)

It’s correct that Julia doesn’t provide any way for a function to introspect how many output values the caller will keep.

In case the extra outputs are free or cheap to compute, just return them and let the caller discard them, as proposed by other comments.

If the extra output is expensive and something you don’t want to spend time on unless it’s wanted, my preferred way is to add an extra input with a mutable struct, vector, or Ref that the function can fill in with the desired information. E.g.

function foo(x, info = nothing)
    y = ...
    if !isnothing(info)
        # Fill in `info`.
    end
    return y
end

Something similar to

baz(bar(foo));

in Matlab can be written a little more verbosely in Julia as

baz(first(bar(foo)))

or

baz(bar(foo)[1])

Yes that’s what I was thinking. I raise this question because when translating toolboxes from Matlab it is tempting to use the exact same syntax to attract users, but evidently this is not always possible.

True, but there are lots of cases where Julia’s syntax is superior, IMO.

I couldn’t agree more with you! Just trying to offer an easy transition for potential newcomers.

See also previous discussions, which include a link to exactly this topic in the Julia manual: Implement of Matlab function "nargout" in Julia

Two other options:

  1. If you just do something that changes the input argument types, you can return an additional result by dispatching to a different method. For example, you could pass WantsFooToo() as an argument, where WantsFooToo is a singleton type, and it could dispatch to a different method that also computes and returns some data “foo”
  2. You can return an object that computes the additional data on demand.

For an exampe, Julia’s linear-algebra factorizations often return a “factorization objection” that have overloaded getproperty to compute additional data, such as the explicit Q matrix from a QR factorization (which otherwise is stored implicitly).

Another example is reverse-mode AD systems like Zygote.jl and ChainRules.jl, where the adjoint rule is returned as a pullback function object that computes a vector–Jacobian product only if/when it is actually needed.

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}