Reflections on an typo (getindex(::NamedTuple, ...))

I have recently finished debugging some code, with an error deep inside a calculation that wasted me a few hours. I wish to emphasize that this is not a bug in Julia. It is my error.

But, as you will see below, I can envision it happening again as it is a simple typo, and I would appreciate advice on good practices that would help me avoid or at least notice it.

For an MWE, consider

calculate_intermediate(x) = (; a = x, b = x + 1, c = x + 2, d = x + 3)

function calculate_final(x)
    (c, d) = calculate_intermediate(x) # <= ERROR HERE
    c + d
end

Now what I meant to write is (; c, d), not (c, d), which gave me (a, b) instead.

My first reaction was that “::NamedTuple should not have getindex”. But that’s like blaming a knife for cutting your hand, and from a practical perspective, now it is like that, and it remains in the language, so that discussions avoid it are pretty pointless.

A destructuring macro (like various @unpack implementations) could refuse to use getindex on a rvalue that supports has nonempty propertynames, unless some special syntax is used to signal that the user really means it. But I am still not sure that it is the right way.

Comments welcome.

1 Like

It’s definitely an easy thing to overlook. I really like that NamedTuples can be unpacked with (; ...), but it is a double-edged sword that “regular” unpacking of the ordered elements also works. For me this particular typo is right up there with forgetting a trailing comma when creating tuples with a single element ((x) instead of (x,)) and forgetting to iterate over a range instead of a single number (for i in n ... instead of for i in 1:n). They kind of look as though they mean the right thing and (mostly) don’t make the program complain immediately. But ultimately it’s something that my eyes get used to after making the mistake often enough.

Not sure if there is a way to avoid that kind of typo through some automatic checks… one heuristic for a linter might be that it’s unlikely someone wants to use the “un-named” destructuring with parentheses. E.g. in your example (c, d) = mytuple is likely a typo since it could have just been c, d = mytuple. But this criterion is probably too dependent on individual style preferences and in principle there’s nothing wrong with (c, d) = mytuple…

A macro sounds useful, but you still have to remember to use it, which to me is very similar to remembering the semicolon in the first place :person_shrugging:

PS: I wonder what NamedTuples would be without getindex – basically nothing more than anonymous structs?

2 Likes

Maybe a linter could check if the variable names exist in the named tuple but in a different position…

But that would require runtime information, right? Not sure how the linter would figure out what comes out of calculate_intermediate(x) (unless there is only one method like in the example above). But I also don’t know much about linters and how they typically operate :sweat_smile:

1 Like

yeah, you’re probably right (don’t know much about linters either :sweat_smile: )