Using the same name for multiple items

Minimum working example of a function I tried to write is below.

function f(start,stop; length=5,collect=false)
    r=range(start,stop,length=length)
    return collect ? collect(r) : r  # Function works without this line.
end

start=0
stop=10
length=3
collect=true
println(f(start,stop,length=length,collect=collect))
MethodError: objects of type Bool are not callable

I understand the error but thought I could get away with it since having four different length’s worked okay. I thought keeping all the names the same kept things easy for the user to remember.

  1. How does the scope of length work? Apparently keyword arguments are immune from interference?
  2. Is there a better/standard Julia style for naming the keyword arguments? lengthvalue, callcollect, …?
  3. Should I be assigning my function inputs abstract types to restrict what the user can pass e.g. lengthvalue::Number, callcollect::Bool?

Thanks!

collect ? collect(r) : r

How would this work if collect is a Bool. Anyway, just call Base.collect if you have collisions.

3 Likes

The scope of length in the body of a method that defines a keyword parameter called length is the same as a if length was a positional parameter. It exists within the body of the method and shadows any previous bindings. If you are talking about the syntax in the call of the method, the length before the = in a call is not a binding, so it does not shadow others nor is shadowed.

Why it would be necessary? You mean because you shadowed the function? You can use the Base.length as @kristoffer.carlsson has pointed out.

If you think it is important, sure. Just do not believe this will have any impact on the performance.

2 Likes

So my original variable names were stylistically fine then. I just need to call the collect function from Base to get the right version. Thank you for your explanations.

In your particular example, though, I would suggest that a collect keyword argument is not a good idea because (a) it makes your function type-unstable and (b) it doesn’t save the user any typing over just calling collect on the output of f. In other words, rather than having a user do:

f(x, y, collect=true)

a user could instead do:

collect(f(x, y))

which is likely to be faster (because it’s type-stable) and also shorter.

As a general rule of thumb, I would try to avoid function arguments whose value affect the type of what is returned.

5 Likes

Okay I will try to keep that rule in mind.

In the actual function, r is used to compute three vectors (which cannot be expressed as a range) and then all four are returned like return r,x,y,z. I thought some users may want all the outputs to be the same type, and some might want the efficiency of a range. Do you have any suggestions on how to handle that?

Hm, it’s hard to say. My initial reaction is that the users who want r to be collected into a vector might just be wrong :wink:. Maybe a better question would be: Why do they need the outputs to be of the same type?

In any case, I can think of a few options:

  1. Keep doing exactly what you’re doing. Type-unstable code works just fine in Julia, it can just be a little bit slower. It’s only a problem if you are particularly concerned about the performance of your function, and only if that function is a real bottleneck.
  2. Make users call collect on r if they actually need that. This could help encourage them not to do so, since calling collect() is frequently a bad habit that new Julia programmers pick up. It’s very rarely necessary to explicitly collect a range in Julia as long as you have written your code to accept AbstractVectors instead of forcing everything to be a Vector.
  3. Return a struct holding r, x, y, and z and provide a useful method to do whatever it is your users wanted to do with all four outputs. Four separate return values is already kind of a lot to keep track of, so wrapping those values in a struct could make your code easier to use and maintain.
  4. Instead of a bool argument, pass in an argument whose type encodes the information you care about. That is, do something like:
struct Collected; end
struct NotCollected; end

f(x, y, ::Collected) = ...
f(x, y, ::NotCollected) = ...

This is a little more complicated, but it avoids type-instability because the type of the output now only depends on the type of the input. This is a very useful pattern in Julia, but I suspect it’s overkill for your particular case. One library which implements this kind of thing very nicely is Interpolations.jl, where you can do interpolate(x, Linear()) or interpolate(x, Cubic()) to return different types of interpolations.

3 Likes

Great food for thought! I will probably just go with #2 for now, but it is good to have these ideas in mind for future projects.

1 Like