Output arguments of functions must be pre-defined? (Julia version 1.8.2)

  • The documentation states explicitly that “The default behavior in Julia when types are omitted is to allow values to be of any type. Thus, one can write many useful Julia functions without ever explicitly using types.” (see Types · The Julia Language, start of third paragraph).

  • Indeed, the input arguments of a function do not need to be defined explicitly before calling the function, and this lack of requirement allows the function to be used with different argument types:

julia> function f1(x, y)
       x + y
       end
f1 (generic function with 1 method)

julia> f1(1, 2)
3

julia> f1(1.2, 3.4)
4.6
  • However, this flexibility apparently does not apply to output arguments:
julia> function f2(x, y, z)
       z = x + y
       return 0   # template for an error code
       end
f2 (generic function with 1 method)

julia> f2(1, 2, z)
ERROR: UndefVarError: z not defined
Stacktrace:
 [1] top-level scope
   @ REPL[32]:1

even though it should be easy to infer the type of z from those of x an y.

  • And it looks like considering an output argument as an optional argument with a pre-assigned type and default value is not acceptable either:
julia> function f3(x, y, z::Float64 = 0.0)
       z = x + y
       return 0
       end
f3 (generic function with 2 methods)

julia> f3(1, 2, z)
ERROR: UndefVarError: z not defined
Stacktrace:
 [1] top-level scope
   @ REPL[34]:1
  • Question: Why is it mandatory to pre-define the types of output arguments since they are computed inside the function? Output arguments are frequently used in C, FORTRAN, IDL and other such languages: it is cumbersome to require users to pre-define the type of output arguments when calling external functions in those languages (specifically ccall), especially because this requires a rather intimate knowledge of the exact requirements of those external functions, as well as a translation table between types in those two languages (as described in Calling C and Fortran Code · The Julia Language)…

Thanks for clarifying those points.

In your examples z is not an output, it is an input. The error you see is because you are trying to call your functions with an input variable that you have not assigned any value to.

7 Likes

Your errors have nothing to do with output types. There is no variable named z in the global scope to pass to your functions as an input argument. You’d get the same error just typing z at the REPL.

3 Likes

@benninkrs and @PeterSimon: Thanks for taking the time to answer. I’m coming from the IDL world, where the following sequences of operations are perfectly legal (in this case on a MacBook Pro):

IDL> cd, "/Users/michel/IDLWorkspace/Default"
IDL> .RESET_SESSION
IDL> spawn, 'ls out_*.pro'
out_arg1.pro
IDL> spawn, 'cat out_arg1.pro'
FUNCTION out_arg1, x, y, z
   z = x + y
   RETURN, 0
END
IDL> .compile -v '/Users/michel/IDLWorkspace/Default/out_arg1.pro'
% Compiled module: OUT_ARG1.
IDL> res = out_arg1(1, 2, z)
IDL> print, res
       0
IDL> print, z
       3
IDL> res = out_arg1(2.2, 4, z)
IDL> print, z
      6.20000
IDL> res = out_arg1("Hello ", "World!", z)
IDL> print, z
Hello World!

IDL> spawn, 'cat out_arg2.pro'
FUNCTION out_arg2, x, y, z
   IF (y GT 0) THEN BEGIN
      z = x + y
      RETURN, 0
   ENDIF ELSE BEGIN
      z = "Second argument of out_arg2 cannot be negative."
      RETURN, 1
   ENDELSE
END
IDL> .compile -v '/Users/michel/IDLWorkspace/Default/out_arg2.pro'
% Compiled module: OUT_ARG2.
IDL> res = out_arg2(2.2, 4, z)
IDL> print, z
      6.20000
IDL> res = out_arg2(2.2, -4, z)
IDL> print, z
Second argument of out_arg2 cannot be negative.

So, in IDL, the type and value of a function argument do not need to be defined in advance, and can even change at run time…

My current focus is on understanding the underlying logic of Julia rather than fixing a particular code. Specifically, I wish to clarify rules for either properly setting up the arguments of ccalls to external libraries, or for correctly converting codes from IDL to Julia.

Looking at the definition of f2 in an earlier message, it looks like the REPL does not object to those statements. It is only when attempting to execute the f2 function that the error occurs (or is reported). So is it correct to say that

  • ALL arguments of Julia functions are considered input arguments (in the sense that they must have a specific type and value by runtime)?
  • Outputs are meant to be transferred to the calling environment through the returned value (or tuple)?
  • To the extent the values of some of the input arguments can be changed by the function, those arguments behave like output arguments in other languages?

Thanks for your thoughts on these questions. Michel.

IDL is wild LOL.

what does it do if you use z before assignement inside out_arg1? for example

FUNCTION out_arg1, x, y, z
   print, z
   z = x + y
   RETURN, 0
END

also I don’t like how Z leaks into the global scope in IDL

you should forget IDL, Julia does things very differently for variable scoping:

julia> function f(z)
           z = 3
           return z
       end
f (generic function with 1 method)

julia> f(2)
3

julia> z
ERROR: UndefVarError: z not defined

julia> z = 0
0

julia> f(z)
3

julia> z
0
1 Like

Well… it’s precisely because I know that IDL is a comparatively old language that I am learning a new one, and that I am asking those questions!

I can’t safely switch to Julia until I fully understand and master the intricacies of properly translating IDL codes to Julia… This is not a matter of liking or disliking a particular language, but rather of moving rationally and methodically, to avoid breaking tens of thousands of perfectly working lines of code through a botched translation. So my questions still stand.

2 Likes

The problem has nothing to do with f2 really. The problem is that you are trying to do something with z (passing it to a function) that is not defined. This is exactly the same error:

julia> z
ERROR: UndefVarError: z not defined

julia> z = 1
1

julia> z
1

I would recommend spending some time with the Julia manual, for example
Functions · The Julia Language and Scope of Variables · The Julia Language might particularly be useful reading.

Values vs. Bindings: The Map is Not the Territory · John Myles White is also a good post that I usually links to people being a bit confused about variables and objects in Julia.

I think those parts answer the questions you have.

2 Likes

Thanks a lot, @kristoffer.carlsson: your constructive input is very much appreciated.

If you really wanted to return something through an argument, it would need to look something this:

julia> function f2!(x, y, z)
           z[] = x + y
           return 0
       end
f2! (generic function with 1 method)

julia> z = Ref{Any}()
Base.RefValue{Any}(#undef)

julia> f2!(1, 2, z)
0

julia> z[]
3

I’m not sure why you would need to do this though.

The exclamation mark is a convention to indicate that you’re modifying one of the arguments.

If you need to output two arguments, the idiomatic way would be

function foo2(x, y)
    if y > 0
        z = x + y
        return 0, z
    else
        z = "Second argument of out_arg2 cannot be negative."
        return 1, z
    end
end

julia> foo2(1, 2)
(0, 3)

julia> status, z = foo2(1, 2)
(0, 3)

julia> status
0

julia> z
3

julia> status, z = foo2(1, -2)
(1, "Second argument of out_arg2 cannot be negative.")

julia> status
1

julia> z
"Second argument of out_arg2 cannot be negative."

Of course if you wanted to report an error, you probably should throw an ArgumentError:

julia> function bar2(x, y)
           y > 0 || throw(ArgumentError("Second argument of bar2 cannot be negative"))
           x + y
       end
bar2 (generic function with 1 method)

julia> bar2(1, 2)
3

julia> bar2(1, -2)
ERROR: ArgumentError: Second argument of bar2 cannot be negative
Stacktrace:
 [1] bar2(x::Int64, y::Int64)
   @ Main .\REPL[25]:2
 [2] top-level scope
   @ REPL[27]:1

julia> status, z = try
           0, bar2(1,2)
       catch err
           1, err
       end
(0, 3)

julia> status
0

julia> z
3

julia> status, z = try
           0, bar2(1,-2)
       catch err
           1, err
       end
(1, ArgumentError("Second argument of bar2 cannot be negative"))

julia> status
1

julia> z
ArgumentError("Second argument of bar2 cannot be negative")
1 Like

Thanks @mkitti: What I gather from this discussion is that all arguments of Julia functions must be either fully defined (type and value) or represented by an appropriate place holder like Ref{T}() before calling the function.

IDL has an implicit pass-by-reference convention.

Julia uses pass-by-sharing.

Basically, at call time, roughly yes. There is no implicit pass-by-reference in Julia.

Let me try to challenge this just so we have sufficient understanding.

julia> function f2!(x, y, z)
           z[] = x + y
           return 0
       end
f2! (generic function with 1 method)

julia> function g(x,y)
           z = Ref{promote_type(typeof(x), typeof(y))}()
           f2!(x, y, z)
           z[]
       end
g (generic function with 1 method)

julia> g(1, 2)
3

julia> g(π, 1im)
3.141592653589793 + 1.0im

In using f2! within g, we have not explicitly defined a type or value for x and y. However, when g is actually invoked, we will know the types and values of x, y, and z. That is the types can be inferred at the last minute.

The macro @code_warntype will show you how types are inferred when you the call the function g:

julia> @code_warntype g(1, 2)
MethodInstance for g(::Int64, ::Int64)
  from g(x, y) in Main at REPL[47]:1
Arguments
  #self#::Core.Const(g)
  x::Int64
  y::Int64
Locals
  z::Base.RefValue{Int64}
Body::Int64
1 ─ %1 = Main.typeof(x)::Core.Const(Int64)
│   %2 = Main.typeof(y)::Core.Const(Int64)
│   %3 = Main.promote_type(%1, %2)::Core.Const(Int64)
│   %4 = Core.apply_type(Main.Ref, %3)::Core.Const(Ref{Int64})
│        (z = (%4)())
│        Main.f2!(x, y, z)
│   %7 = Base.getindex(z)::Int64
└──      return %7


julia> @code_warntype g(π, 1im)
MethodInstance for g(::Irrational{:π}, ::Complex{Int64})
  from g(x, y) in Main at REPL[47]:1
Arguments
  #self#::Core.Const(g)
  x::Core.Const(π)
  y::Complex{Int64}
Locals
  z::Base.RefValue{ComplexF64}
Body::ComplexF64
1 ─ %1 = Main.typeof(x)::Core.Const(Irrational{:π})
│   %2 = Main.typeof(y)::Core.Const(Complex{Int64})
│   %3 = Main.promote_type(%1, %2)::Core.Const(ComplexF64)
│   %4 = Core.apply_type(Main.Ref, %3)::Core.Const(Ref{ComplexF64})
│        (z = (%4)())
│        Main.f2!(x, y, z)
│   %7 = Base.getindex(z)::ComplexF64
└──      return %7
1 Like

@mkitti: Thanks a lot; that’s interesting and enlightening.
@kristoffer.carlsson: Thanks for pointing out the web page of John Myles White, which is indeed very useful.

1 Like

There is no such thing in Julia as an “output argument”.

Therefore, everything you pass into a Julia function must have a value.

If that value is mutable, you may mutate it within the Julia function.

If you mutate the value of an argument by convention you end your function name with an exclamation point !

Hope that helps

4 Likes

@dlakelan: Yes, it does. Thanks for your input.

Note that Julia passed arguments by “sharing” but a Julia function can not change the binding of a variable that the caller has.

function outer()
   z=1
   inner!(z)
   return z
end

function inner!(z)
   z = 2 # this should compile to a noop
   return 100
end

outer() should return 1 (I’m on my phone so can’t verify). The reason is that in the inner! function the assignment just changes the binding of the totally independent variable z within the inner! function. Numbers are not mutable. When you pass a mutable struct the outer function and the inner function are both “pointing to” or “bound to” the same struct. When the inner function mutates the struct the outer function will see the mutated state of the struct because it is bound to the same object as the one the inner function sees.

1 Like

@dlakelan: Indeed:

julia> function outer()
          z=1
          inner!(z)
          return z
       end
outer (generic function with 1 method)

julia> function inner!(z)
          z = 2 # this should compile to a noop
          return 100
       end
inner! (generic function with 1 method)

julia> outer()
1

It does compile to a no-op.

julia> @code_llvm inner!(5)
;  @ REPL[1]:1 within `inner!`
define i64 @"julia_inner!_136"(i64 signext %0) #0 {
top:
  ret i64 100
}
1 Like