Naming convention for functions when an argument is optionally mutated

I have a function f with a signature:

f(x, y, cache::Union{T1, T2})

where cache will be mutated (modified) when it’s of type T1 and not mutated when it’s of type T2. The type of cache is determined by the combinations of the types of x and y. Furthermore, the body (specific lines of code) of f does not change for different types of cache as f just passes cache to an inner function, fCore. So whether cache gets mutated is not “really” determined by f, but by cache itself.

Additionally, I would prefer not to split f into f1!(x, y, cache::T1) and f2(x, y, cache::T2) because f is used multiple times in a larger wrapper function h in which x and y do not compose a small finite number of type combinations. I think it’s too much of a hassle to manually write h with different versions of f in terms of f1! and f2 in its body, while the other parts are nearly identical.

So, after this somewhat convoluted introduction, my question is, should I redefine f’s signature to be something like f!(cache::Union{T1, T2}, x, y)?

It seems that there isn’t an apparent naming convention for a function when the mutability of an argument is directly dependent on itself rather than the function.

On the one hand, I can simply append ! to the end of the function’s name. Such a choice is under the assumption that as long as it CAN mutate the argument, it should be marked with !. On the other hand, we also have base functions like map, which does not have ! at the end but can indirectly mutate its arguments due to the lower-order function evaluated inside it, just like the relation between f and fCore in my case. To demonstrate what I meant by “indirectly”, here is an example (on Julia 1.11.2):

julia> unexpectedMutate!  = x -> x .+= 1
#1 (generic function with 1 method)

julia> v = [[1], [2], [3]]
3-element Vector{Vector{Int64}}:
 [1]
 [2]
 [3]

julia> map(unexpectedMutate!, v);

julia> v
3-element Vector{Vector{Int64}}:
 [2]
 [3]
 [4]

If we think marking the argument name instead of the function name is a better solution for this type of edge case, I would love to see a special symbol dedicated to the arguments that will be (potentially) mutated, like how ! is used for a function. However, a naive solution of appending ! to the name of an optional argument currently (Julia 1.11.2) is an invalid syntax:

julia> foo1(a!=1) = (a!) + 1
ERROR: syntax: "(a != 1)" is not a valid function argument name around 

Nevertheless, I’m interested in other people’s opinions on this edge case. Thank you!!

1 Like

You need a space:

julia> foo2(a! = 1; b! = 2) = a! + b!
foo2 (generic function with 2 methods)

julia> foo2(3; b! = 4)
7
1 Like

Oh, that’s awesome! Thanks! Is such a naming strategy encouraged in the situation I mentioned? I didn’t find any style guide in the official documentation regarding marking arguments with !.

I don’t know that any style guides (official or not) speak to it, but it is something I know I’ve seen folks do before… commonly as out! (GitHub code search), but there are even a few hits for cache!, too.

1 Like

That’s cool! Thanks! It’s a relief to see that people faced similar scenarios when trying to perform cache/buffer-based computation.

In vscode the autoformatter probably won’t respect this convention and you’ll get errors (it’ll remove the space so a! = 1 becomes a!=1 or “a not equal to 1”). One fix is to wrap the arg or kwarg in parentheses. Although that looks a little weird to me.

function func(a, b; (c!)=5)
...
end

func(1, 2; (c!)=10)
1 Like