How to define many variables in few lines?

I don’t know much about Fortran, but as far as I am aware global mutable variables are considered bad in general, in every language, it’s not just a Julia thing.

If you google “why are global variables bad?” you will get a lot of hits, none of them related to Julia. One of the top hits I get is even “Why (non-const) global variables are evil”(!!)

4 Likes

Thank you very much.

I was asked to Translate my Fortran to Julia. Because we want to develop a package which is easy to install on users’ computer. From this point, Julia might be better. Especially if we plan to use differential equations in Julia.
It is difficult to ask a user (assuming some users are just doctors and they just need a button to click and install) to install some Fortran libraries.

On the other hand, personally I am willing to learning Julia a little bit, too.

You have a point here. However, did you encounter this issue in real code so far? Or are you just thinking up front that you could be making this mistake? Also, IMO, the “bigger issue” here is not that the type of a “changes” but that you’re overshadowing (“overwriting”) your function argument a with a local definition which is not want you want to do irrespective of types. I.e.

function f(a::Int64)
  a=3 
  println(a) 
end

is just “as bad” IMO.

2 Likes

I apologize again. I am not proud of using global variables.
But your guys help is very valuable.
Since those global variables in my Julia code now has all been marked as Ref and [ ] symbol, it is trivial to create a struct to include all of them within a type, then put this type object in the arguments of the functions.

No need to apologize! We’ve all been there and have made mistakes when writing code, in particular in a new language. (Actually, I still make them way to often :sweat_smile:)

2 Likes

Translating code is hard, especially if the original code doesn’t use good programming practice. My sympathies.

But it’s also an opportunity to clean up the code and make it better, faster, safer and easier to understand. Getting rid of global variables is an important step on that road. Good luck!

4 Likes

You are right, I just thinking up front.
That function is just a stupid as hell illustration. Your points are all right.
My point is just to say that, sometimes, perhaps in Julia we might accidently changed the type of a variable which should not be really change. If Julia can throw a warning it might be nicer (perhaps Julia can throw a warning but I do not know how to let Julia make such warnings).

It doesn’t throw a warning but there are nice introspection tools, like @code_warntype (or Cthulhu.jl), which highlight type instabilities in red:

However, note that not every instability is a real issue (in the sense that it degrades performance).

3 Likes

Note that this is not really a type instability. You are just using assigning the label a to a new value. It has no relationship with the “previous” a. If you were mutating an array of integers and tried to assign a float to it, you would get an error.

(I completely understand that this dissociation between label and value is strange coming from Fortran. I have also a note about that: Assignment and mutation · JuliaNotes.jl)

3 Likes

IMPLICIT NONE and declaring all variables at the top is typical in Fortran, but not Julia.

In Julia, you can add type declarations to variables and functions as they are defined, but in many cases there is no benefit. I suggest initially translating your code without thinking about concrete types at all; just let Julia figure them out on its own. Then go back at the end with @code_warntype if you notice something is slow. You can write fast, type-stable code without any type declarations in Julia.

Try to enjoy removing your Fortran shackles :wink: . In Julia, you can write what you want the code to do without all the computer science details.

10 Likes

One more comment:

There is some truth about that statement. For small arrays that is the place to use StaticArrays, and pass the size of these small arrays as one parameter of your input, this is a fundamental strategy in Julia to keep generic, avoid global variables (even the size of array):

julia> using StaticArrays

julia> function f(x::SVector{N,T}) where {N,T}
           s = zero(T)
           for i in 1:N
               s += x[i]
           end
           return s
       end
f (generic function with 1 method)

julia> x = SVector{3,Float64}(1.0,2.0,3.0);

julia> f(x)
6.0

julia> x = SVector{5,Int}(1,2,3,4,5);

julia> f(x)
15

f(x) above known at compile time the size of x and the type of elements of x. And it specializes to these sizes and types.

These small arrays (to about ~100 elements) are stack allocated and that is very fast. For larger arrays it is just better to just pass the array as a parameter to avoid internal allocations, or just allocate the array inside the function with the size you need. Fortran is better at stack allocating large arrays than Julia (there is a PR somewhere aiming to improve this in Julia, but for now preallocating is the best solution).

1 Like

Better yet, OP should get used to the style of

function my_operation(params...)
    x = Vector{Float64}(undef, 100)
    _my_operation!(x, params...)
end

where the outer function allocates the array, and is only ever called once, and the inner function operates on the array, without copying, as needed. Then all other functions get passed the array.

5 Likes

@pdeffebach, could you please provide an example of usage of that code style?
Thank you.

1 Like

Here is an example with median! and median from Statistics.jl

It seems to be more efficient (memory-speed tradeoff) to sort a vector vector in-place, so the function makes a copy and then sorts in-place.

3 Likes

One could also expose both functions to the user:

function my_operation!(x,params...) #mutates x, and thus has the ! to indicate it
  ...
end
function my_operation(params...) # non-mutating version
     x = Vector{Float64}(undef,100) 
     return my_operation!(x,params...)
end

such that it even has the option to choose one or the other depnding if, for example, that function is called in an inner loop an you want to avoid the allocations by passing an auxiliary preallocated array.

(@pdeffebach, there a typo in your x = Vector{.... definition above, the parenthesis are missing).

3 Likes

Thank you very much. One more question, we see that there is 3 there inside the ntuple(), for the a, b, c.
Now, is there a way to automatically set the number, instead of manually count how many variables on the left hand side?

I know from you all that perhaps I can manually set a big number like 1000 such that

a, b, c = ntuple(i -> Vector{Float64}(undef,0), 1000)

might also work. But I wonder, is there a more ‘automatically’ way to set the number of ntuple elements? Like

a, b, c = ntuple(i -> Vector{Float64}(undef,0), <automatic size based on how many variables on the left hand side>  )

Or do I have to count how many variables on the left hand side, and set this number manually?

as far as I know you cannot avoid counting the left side by hand (the large number option is fine, though).

1 Like

It’s not optimal to use ntuple if you don’t know the number of outputs, since the tuple on the right hand side will actually be allocated. So ntuple(..., 6) will allocate twice as much memory as ntuple(..., 3):

jl> @btime a, b, c = ntuple(i -> Float64[], 3);
  91.516 ns (3 allocations: 240 bytes)

jl> @btime a, b, c = ntuple(i -> Float64[], 6);
  175.777 ns (6 allocations: 480 bytes)

jl> @btime a, b, c = ntuple(i -> Float64[], 100);
  4.743 μs (102 allocations: 9.48 KiB)

If you use a generator, on the other hand this problem disappears, and only the needed amount of memory is allocated:

jl> @btime a, b, c = (Float64[] for _ in 1:3);
  88.689 ns (3 allocations: 240 bytes)

jl> @btime a, b, c = (Float64[] for _ in 1:6);
  88.303 ns (3 allocations: 240 bytes)

So you can use arbitrarily large numbers:

jl> @btime a, b, c = (Float64[] for _ in 1:1000);
  91.401 ns (3 allocations: 240 bytes)

Or, the solution to your question (and to contradict @lmiq :wink: ):

jl> @btime a, b, c = (Float64[] for _ in Iterators.countfrom(1));
  91.649 ns (3 allocations: 240 bytes)
3 Likes

if you really need this for some reason:

julia> macro blah(ex1, ex2)
           rs = [quote $x = $ex1 end for x in ex2.args]
           esc(Expr(:block, rs...))
       end
@blah (macro with 1 method)

julia> @blah Float64[] a,b,c
Float64[]

julia> a
Float64[]

julia> b
Float64[]

julia> c
Float64[]

julia> push!(a, 1)
1-element Vector{Float64}:
 1.0

julia> b
Float64[]
2 Likes

Here’s a function that’s a bit neater:

jl> gimme(fun) = (fun() for _ in Iterators.countfrom(1))

jl> @btime a,b,c = gimme(()->Float64[]);
  91.410 ns (3 allocations: 240 bytes)

jl> @btime a,b,c,d = gimme(()->Float64[]);
  120.270 ns (4 allocations: 320 bytes)
2 Likes