tags:

views:

118

answers:

3

Hi,

This code smells... how do I rewrite it better?

my $record;

eval {
    while (
        # undef $record here, so if getRecord() failed, nothing will be written
        # in the reject file
        do { undef $record; defined( $record = $dataFile->getRecord ) }
    ) {
        $LT_DataFile->encode($record);
    } 
    1;
};

if ( my $error = $@ ) {
    $rejectFile->writeRecord( $error, $record );
}

Thanks.

+8  A: 

You do not need a variable in the catch part because when execution arrives there its content will always be undef. Replacing this with a literal value lets us restrict $record to a smaller scope.

use Try::Tiny;
try {
    while (defined(my $record = $dataFile->getRecord)) {
        $LT_DataFile->encode($record);
    }
} catch {
    $rejectFile->writeRecord($_, undef);    # T::T puts error in $_
}
daxim
+1 for Try::Tiny. To @est, doing the `eval` and then separately testing `$@` can cause problems (missed errors, incorrect error messages) in some cases due to `$@` being global. Use Try::Tiny instead because it handles all those details for you.
Dave Sherohman
Why do you say `$record` would always be `undef` if there's an error? Presumably the `encode` method is capable of throwing errors, and in that case `$record` would be the record that caused the encoding error. It's only `undef` if `getRecord` is the method that failed.
cjm
If $record is an object, then you can drop the `defined`, and just check for truthiness: `while (my $record = ...)) }`
Ether
@Ether: Remember that objects can overload `bool`, so a defined object can evaluate to false. (Or, if the object overloads conversion to string or number, those will be used to determine truthiness.)
cjm
@cjm: objects *could*, but one would hope that they would do so intelligently and return a logical result -- e.g. a defined object evaluating to false in a boolean context could indicate an invalid or corrupt dataset, so checking this value might do exactly what the caller needs.
Ether
+1 for @cjm, encode() still needs to capture the $record when it throws exception. The solution from Robert P below works the best for my situation.
est
+2  A: 

Ok, reworked my answer.

I think the REAL problem here is how you handle the errors. It's confusing at first glance to see a single error handler when you have multiple places where things could go wrong. I see two alternatives.

First, keep it mostly the same as you have now, but specifically check for each type of error:

my $record;
eval {
    while (defined( $record = $dataFile->getRecord )) {
        $LT_DataFile->encode($record);
    } 
};
if (my $error = $@) {
    given ($error) {
        when (/get record error/) {  $rejectFile->writeRecord($_, undef);   }
        when (/encode error/)     {  $rejectFile->writeRecord($_, $record); }
    }
}

This way, you're explicit in how you handle your errors. Of course, with Try::Tiny, this simplifies into the following

my $record;
try {
    while (defined( $record = $dataFile->getRecord )) {
        $LT_DataFile->encode($record);
    } 
} catch {
    when (/get record error/) {  $rejectFile->writeRecord($_, undef);   }
    when (/encode error/)     {  $rejectFile->writeRecord($_, $record); }
}

Alternatively, you could add the lexical record per Daxim's answer. This requires a second eval or try, closer to the problem and adding a last call:

eval {
    while (defined( my $record = $dataFile->getRecord )) {
        eval { $LT_DataFile->encode($record) };
        if (my $error = $@) { $rejectFile->writeRecord($error, $record); last }
    } 
};
if (my $error = $@) {
    $rejectFile->writeRecord($error, undef);
}

Unfortunately this method won't work with Try::Tiny, because the blocks passed to try are actually subrefs.

Robert P
But you've ignored the comment explaining why `$record` needs to be `undef`. If `getRecord` throws an error, you'll be reporting the previous (non-erroneous) record.
cjm
Derp, indeed. Fixed.
Robert P
And now you have the opposite problem. If `encode` throws an error, you'll be reporting `undef` instead of the record that caused the error.
cjm
Damn, it was confusing code. Cleaned up in its entirety now.
Robert P
If this were my code, I'd write unit tests that exercised every code path.
Ether
+2  A: 

I would refactor the while loop to

while (defined( $record = $dataFile->getRecord )) {
    $LT_DataFile->encode($record);
} continue {
    undef $record;
}

The continue block is executed after each iteration before conditional is tested. It will still be called if your code uses next to start the next iteration early. If your not likely to call next then simplifying the code to

while (defined( $record = $dataFile->getRecord )) {
    $LT_DataFile->encode($record);
    undef $record;
}

would work as well.

Ven'Tatsu
This will not work as expected.... when $dataFile->getRecord() throws exception, $record already has value from previous iteration, so $rejectFile->writeRecord() gets the wrong information.
est
@est how so? The call do `undef $record;` will always be called before the conditional in the next iteration of the loop. If `$dataFile->getRecord()` dies then `$record` is undef. Any action that will skip the `continue` block will either end the loop or skip the conditional. Assuming that `$record` is not defined before the first iteration then `$record` will always be undefined when `$dataFile->getRecord` is called and always defined when `$LT_DataFile->encode($record);` is called.
Ven'Tatsu