Variable number of arguments with different types

This might be a strange question, but I really want to know if there is a way to construct a function where the first few variables are Int, while the rest are Float64, so that I can call it using like fn(1, 2, 3.2, 3.4, 4.1), with the numbers of Int and Float64 are parametric.

I tried things like

function f(v::Vararg{Int, 2}, w::Vararg{Float64, 3})
end

which does not work.

Thanks.

Probably not. Only the last argument can be variadic. But you can wrap both groups in a tuple:

function f(v::NTuple{2, Int}, w::NTuple{3, Float64})
end
4 Likes

At first glance it seems reasonable to allow nontrailing fixed-length Vararg so that a sequence of arguments is automatically packed into tuples in the method body, but this creates a new source of ambiguity in multimethods:

# Pretend this doesn't error
function f(v::Vararg{Int, 2}, w::Int); (v, w) end
function f(v::Int, w::Vararg{Int, 2}); (v, w) end

f(1, 2, 3) # which method and result?

Method ambiguities are already possible due to multiple dispatch involving abstract types, but at least those can be resolved by adding a method with more specific argument types to cover the ambiguous cases. There’s no way to do that for this example, you must sacrifice a method and there’s no reasonable criterion besides recency. There’s no ambiguity if we write out the number of arguments and manually pack the tuples:

function f(v::Int, v2::Int, w::Int); ((v, v2), w) end
2 Likes

Sounds like an easy solution might be to separate f into a function that reads the integers and floats and the “internal” function, something like

function f(numbers...)
    ints = filter(n -> n isa Int, numbers)
    floats = filter(n -> n isa Float64, numbers)

    _f(ints, floats)
end

function _f(ints, floats)
    # ...
end

Note that this version would also recognize integers/floats that appear out of order. And it would ignore any elements of numbers that are neither Int nor Float64.

This separation you can also do before you call the function, so I’m wondering about the reason you need the inputs all splatted in the function call? It seems more intuitive to just keep them separate (like w and v in your example).

1 Like

This approach seems to work. Is the code type-stable?
I tried function f(numbers...::Tuple{Int, Float64}) but it is not allowed.

Sometimes a math function has both integer and real number input, so I’m just curious if there is a simple way to define functions like that.

Thanks for the explanation. So it looks like there is no fundamental way to do this.

1 Like

The compiler should be able to tell that the resulting ints and floats are tuples of the respective types in this example. If you check the output of, for instance, @code_warntype f(1, 3, 2.5, 1.0) you can see what Julia is able to tell about the types of the variables inside the function. Whether the whole function is type-stable depends on _f of course.

Note that once a function is called, the types of the inputs are known and a method will be compiled for this particular type signature. E.g. here when _f(integers, floats) is called, the compiler will know that integers is some tuple of a specific size containing integers etc. It can then compile a particular (fast) method of _f that deals with these inputs.

In my experience, there is generally no need (or performance benefit) to annotate the types of function arguments, unless you want to use multiple dispatch and define/extend different methods which are specialized for certain input types. If I don’t need to restrict the types for a specific reason, I tend not to do it. But it might also increase readability sometimes to clarify what objects are “intended” to be used with this function.

A Tuple{Int, Float64} is a tuple of exactly two elements (the first one of type Int and the second of type Float64. So something like (1, 2.0), but if there are more elements, like in (1, 2, 3) this would be a Tuple{Int, Int, Int} and so on.

There is the NTuple type that @HanD mentioned, which can simplify the notation a little bit.

In your example, I think two things are important to point out:

  • You can put a type restriction on a slurped argument (using the three dots ...), but it has to come before the three dots:
    f(numbers::Tuple{Int,Float64}...) would work

  • But this is just fixing the syntax and I think it still does not do what you want. This example f(numbers::Tuple{Int, Float64}...) will accept any number of “tuples of exactly two elements, the first being an int and the second a float”. So

    • f()
    • f( (1, 1.0) )
    • f( (1, 1.0), (3, 1.4) )
    • etc.

    will work, but not f(1, 2.0) or f(1, 2, 3.0), since that would require a method like f(n::Number...) or similar. And neither would f( (1, 2) ) work, since Tuple{Int, Float64} and Tuple{Int, Int} are different types…

These kind of restrictions are exactly why I feel like omitting type annotations is the better choice more often than not. In different projects I started out restricting the input types over-eagerly, only to realize that I later want the function to handle Float64 as well as Float32 and Int, etc. Just leaving the type annotation away is the solution in many of these situations.

There are few functions in math that absolutely need an integer. Most are defined for (some subset of) the real numbers, i.e. work with floats. Julia (and many other programming languages) will “promote” the numeric types according to some rules such that the computer can work out the same result (up to small errors from rounding, etc.), regardless if you compute (3.14 ^ 2), or (3.14 ^ 2.0), or (3.14 * 3.14). All the basic operations like +, *, / etc. are already defined in the proper way for all possible Number inputs, so you usually don’t need to worry about multiplying numbers of different types. The result will be of the type that “makes most sense”.

In the case you really do need integers (e.g. when using indexing), you can just manually convert the inputs by using Int(x) in the appropriate place.

More background about conversion and specifically promotion can also be found here
https://docs.julialang.org/en/v1/manual/conversion-and-promotion/


Sorry for the long-winded answer and if I mention stuff you already knew (very likely) :sweat_smile: But I had the impression that you might be running into an XY Problem down the road, since it’s still not clear to me in what situation you would actually need a function like you originally proposed.

1 Like

I think you are looking for something like this instead of the Tuple usage:

function f(numbers::Union{Int,Float64}...)
    @info numbers
end

f(1, 2, 3.3, 4.4, 5.5)
2 Likes

@Sevi Thanks for such a detailed explanation. I think then f(numbers...) is enough for efficient code. This is quite surprising but maybe it is the natural result of julia’s design.

@algunion Thanks. Indeed, this is pretty much the best we can do.

What I want originally is a generic way to specify the type of each variable in the way like parametric types. Using f(numbers::Union{Int, Float64}...) with some conditions in the function body before real evaluation is probably good enough.

Thanks all for answering this strange question.

2 Likes