|To:||rakudobug [...] perl.org|
|Date:||Fri, 29 Sep 2017 17:43:14 -0400|
|From:||Brandon Allbery <allbery.b [...] gmail.com>|
|Subject:||[LTA] file tests and Failure do not interact as expected|
This turns out to be fairly complex, and has implications that may go well beyond file tests. (Again! It only caused a syntax rethink and the redesign of smartmatching when I poked file test issues in Pugs in 2007....)
The original problem is that a fairly obvious (from shells or perl 5 or etc.) test for whether a file (as such) exists or not, can yield surprises:
pyanfar Z$ 6 '".profileX".IO.f.say'
Failed to find '/home/allbery/.profileX' while trying to do '.f'
in block <unit> at -e line 1
Naïvely, I expect this to output False, not throw.
The reason for this is fairly obvious: if I use it in a Bool context then the Failure gets coerced as I expect, But if I'm not aware that this relies on Failure getting disarmed when coerced to Bool, using it with something that accepts Any (like say) will throw instead of giving me False. Accordingly, it works as expected if I force coercion to Bool.
pyanfar Z$ 6 'say ?".profileX".IO.f'
So, the first problem is that you have to be aware of the special behavior of Failure and how it interacts with a method which is documented as producing Bool.
If it stopped there, this might not even be worth a bug report except possibly for documentation. But if you dig a little farther, things start getting more complex:
pyanfar Z$ 6 '".profileX".IO.e.say'
The .e method behaves differently, and how I expected .f to behave!
Again, there is a rational explanation: it is, logically, a different operation. In lower level terms, .e just checks whether stat() succeeded, whereas .f needs to also look at the result and gives me Failure if the stat() failed. But you need to know that this difference exists, because it's not immediately clear from the documentation.
Perl 5 had a variant of this, and "leaked" a hint of it with its magic _ parameter. As a way of exposing the difference between just calling stat() and using its result, though, its kinda the worst of all possible worlds. (Not to mention the questions of thread safety, etc. that come up when you start tossing such magic around.)
Things get deeper yet, though. Which kinds of failures of stat() result in Failure, and which if any produce harder exceptions? An EIO return from stat() is a much more fundamental failure than an ENOENT return. (tl;dr: EIO means the filesystem is hosed. For a remote filesystem it may mean the connection to the server has been lost; for a local one, it could mean someone unplugged the USB hard drive or it could mean you need to immediately shut down, fsck, and possibly dig out the backups. In all cases, it's a deeper issue than a file simply not existing.) Is this a situation where we might actually want a harder kind of Failure that doesn't get disarmed on coercion to Bool, but does if tested with .defined? Or does this justify a hard exception? And, there are likely to be intermediate cases where the right answer is even less clear.
If you go back and look at the difference between .e and .f, you also get other questions. Notably, if you decide that .f should behave like .e, do you do this explicitly (and for each operation), or do you arrange for it to be part of the signature, or do you perhaps handle any Failure return through a declared Bool return type by coercing it to Bool? All of these answers are unappealing, some moreso than others (unconditional coercion might actually be right in the general case, but it scares me --- and interacts strongly with the preceding question).
Making the return type of the file tests a coercion type to express the notion that, here, Failure should coerce to Bool (but maybe not always? gain see previous section) is tempting, but (a) I have no idea what the syntax would be (b) currently that information is ignored, or possibly throws at compile time (c) and the existing coercion machinery operates inbound to a function, not outbound for its result. And is this situation actually common enough to justify such a mechanism, especially considering that it probably makes a relatively hot path more expensive?
So there's actually a fair amount to think about here. And, depending on your early answers, this could potentially be three tickets or maybe even more.