Constant function arguments

question
proposal

#1

Is there a way to declare function arguments as constants to prevent their accidental modification in the function body?

For example,

function foo(x::const Int, y::Int)
    return x + y
end

or

function foo(x::immutable Int, y::Int)
    return x + y
end

Or even, some method like in FORTRAN, via intent keyword; e.g.:

function func(i) result(j)
    integer, intent(in) :: i ! input
    integer             :: j ! output
    j = i**2 + i**3
 end function func

#2

Julia is pass-by-value, so you can’t modify the caller’s x::Int (unlike Fortran which is pass-by-reference). There is no way to a make a local variable const, though (see also https://github.com/JuliaLang/julia/issues/5148).


#3

I think there is a need for a uniform declaration of an immutable variable in global and local scopes. This helps a lot for correct programming and makes Julia syntax consistent, imo, since one can now declare an immutable struct in Julia.
Making immutability explicit is always a good programming practice, I believe.


#4

I believe a closure might be the best alternative in many instances. See for example https://julianlsolvers.github.io/Optim.jl/latest/user/tipsandtricks/ section on dealing with constant parameters.


#5

Thanks; I’ll look into that.
Still, I believe declaring an immutable variable should be possible in Julia.


#6

A pull request implementing constant local variables would be a welcomed contribution to the language!

Note on terminology: const and immutable are not the same – const refers to a binding which cannot be reassigned; immutable refers to a type whose value cannot be modified. These are orthogonal: one can have a const binding to a mutable value or a non-const binding to an immutable value.


#7

thanks for the clarification. I am only a layman in that respect :sweat_smile:
My idea is simply to have compile-time constants, as in C++ for instance.
But having explicit immutable declaration is also a nice thing; we have now for structs.


#8

I am now confused. I thought you wanted function arguments to be “constant” (ie presumably error on attempted assignment within the function scope) at runtime, with their values, of course, depending on the caller, so definitely not constant at compile time.

I would really like an orthogonal feature: make const redefinable, via a mechanism similar to what fixed #265. It would be a hint to the compiler so that it can assume unchanged type and value, but redefining would trigger recompilation. (I know I can get this already via myconstant() = value though.)


#9

You’re right, @Tamas_Papp . I am not using the precise wording. I’ll right up something more precise to convey what I mean/expect.


#10

@StefanKarpinski
I wrote up an extensive new topic about this:
see https://discourse.julialang.org/t/8686
I hope I’ve got the idea correctly.


#11

Regarding your suggestion to use closures – if I have understood that correctly --, consider the following example:

# original function
function foo(x, y)
    return x+y
end

# generator for closures on `f`, fixes 2nd argument
function closure_gen(y)
    cl(x) = foo(x, y)
    return cl
end

# closure
bar = closure_gen(2)

# equivalent function to `bar`
baz(x) = x + 2

println( bar(1) == baz(1) )  # prints 'true': the results match

Let’s compare the underlying native codes:

@code_native bar(1)
#=
	.text
Filename: REPL[2]
	pushq	%rbp
	movq	%rsp, %rbp
Source line: 2
	addq	(%rdi), %rsi
	movq	%rsi, %rax
	popq	%rbp
	retq
	nopl	(%rax)
=#

and

@code_native baz(1)
#=
	.text
Filename: REPL[7]
	pushq	%rbp
	movq	%rsp, %rbp
Source line: 1
	leaq	2(%rdi), %rax
	popq	%rbp
	retq
	nopw	(%rax,%rax)
=#

Why are the generated codes different?


#12

I believe in your code the value 2 for y is not known at compile time. Only the types are known at compile time. This works as you expected:

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

julia> # generator for closures on `f`, fixes 2nd argument
       function closure_gen(y::Val{n}) where n
           cl(x) = foo(x, n)
           return cl
       end

julia> bar = closure_gen(Val{2}())
(::cl) (generic function with 1 method)

julia> bar(1)
3

julia> # equivalent function to `bar`
       baz(x) = x + 2
baz (generic function with 1 method)

julia> baz(1)
3

julia> @code_native bar(1)
	.section	__TEXT,__text,regular,pure_instructions
Filename: REPL[15]
	pushq	%rbp
	movq	%rsp, %rbp
Source line: 3
	leaq	2(%rdi), %rax
	popq	%rbp
	retq
	nopw	(%rax,%rax)

julia> @code_native baz(1)
	.section	__TEXT,__text,regular,pure_instructions
Filename: REPL[6]
	pushq	%rbp
	movq	%rsp, %rbp
Source line: 2
	leaq	2(%rdi), %rax
	popq	%rbp
	retq
	nopw	(%rax,%rax)

#13

Could you explain the meaning of Val{n} and function closure_gen(y::Val{n}) where n in your example?
What should one do, if y is an Array or a user-defined type?


#14

This works with any isbits type. It will not work with a common array, but will with a StaticArray.

You can find more information here:
https://docs.julialang.org/en/stable/manual/performance-tips/#Types-with-values-as-parameters-1


#15

I just noticed that in my code I broke the convention stated in the manual:

“For consistency across Julia, the call site should always pass a Valtype rather than creating an instance, i.e., use foo(Val{:bar}) rather than foo(Val{:bar}()).”