I agree that exists() throwing a warning would propably not very
DWIM-ish. Then again though I wonder if an exists() call that silently
changes data is so much more intuitive.
I think the issue is that it's a necessity for exist to take an lvalue. Why?
exists doesn't want to fully evaluate it's argument. It wants to do everything EXCEPT resolve the last reference to the data returned by that expression, which is exactly an lvalue. So like the left side of an = or += is lvalue, the parameter to exists is lvalue so it can "get at" what it wants to examine to return true/false.
And whenever hashes are evaluated in lvalue context, they autovivify. Why? Because it would be really shitty if we couldn't easily construct hash trees/data structures with {} notation willy nilly in one liners. It's to valuable a feature to leave out.
But we know that exists() is useless on checking transverse nodes in a hash lookup (for previously mentioned reasons, no matter if it has the autovivify behavior or not). We shouldn't be trying it in the first place. So you already have to implement a different kind of test function that does it if you need it... its best practices. If you don't understand how exists and operator expression evaluation works, well then you run into the pitfall, and you learn.
Here's my replacement for exists() that walks:
sub hash_path_exists () {
my $ref = shift;
if(ref $ref ne 'HASH') { return undef; }
foreach my $inner @_[0..-2] { #Don't include the last in the traversal list
if(ref $inner ne 'SCALAR') { return undef; }
if(!exists($ref->{$inner})) { return 0; }
if(ref $ref->{$inner} eq 'HASH') {
$ref = $ref->{$inner};
next;
}
return undef; #We encountered a non-hash ref in the traversal list
}
return defined($_[0]) && exists($ref->{$_[0]}); # The params array can only contain at most 1 element here.
}