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) 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.