Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does Perl 6 throw an X::AdHoc exception for my subset type?

This is a reported bug in Perl 6: X::AdHoc instead of X::TypeCheck::Binding with subset parameter, first reported in November 2015.


While playing with my Perl 6 module Chemisty::Elements, I've run into an Exception issue I didn't expect.

I define a type, ZInt, which limits numbers to the ordinal numbers found on the periodic chart (which I've faked a bit here). I then use that type to constrain a parameter to a subroutine. I expected to get some sort of X::TypeCheck, but I get X::AdHoc instead:

use v6;

subset ZInt of Cool is export where {
    state ( $min, $max ) = <1 120>;
    ( $_.truncate == $_ and $min <= $_ <= $max )
        or warn "Z must be between a positive whole number from $min to $max. Got <$_>."
    };

sub foo ( ZInt $Z ) { say $Z }

try {
    CATCH {
        default { .^name.say }
        }

    foo( 156 );
    }

First, I get the warning twice, which is weird:

Z must be between a positive whole number from 1 to 120. Got <156>. in block at zint.p6 line 5 Z must be between a positive whole number from 1 to 120. Got <156>. in block at zint.p6 line 5 X::AdHoc

But, I get the X::AdHoc type when I'd rather people knew it was a type error.

I checked what would happen without the warn and got X::AdHoc again:

subset ZInt of Cool is export where {
    state ( $min, $max ) = <1 120>;
    ( $_.truncate == $_ and $min <= $_ <= $max )
    };

So, I figured I could throw my own exception:

subset ZInt of Cool is export where {
    state ( $min, $max ) = <1 120>;
    ( $_.truncate == $_ and $min <= $_ <= $max )
        or X::TypeCheck.new.throw;
    };

But, I get a warning:

Use of uninitialized value of type Any in string context Any of .^name, .perl, .gist, or .say can stringify undefined things, if needed.

At this point I don't know what's complaining. I figure one of those methods expects something I'm not supplying but I don't see anything about parameters for new or throw in the docs.

How do I get the type I want without the warning, along with my custom text?

like image 377
brian d foy Avatar asked Oct 18 '16 00:10

brian d foy


2 Answers

Don't throw the exception or warn with one. Instead, you want to fail:

subset ZInt of Cool is export where {
    state ( $min, $max ) = <1 120>;
    ( $_.truncate == $_ and $min <= $_ <= $max )
        or fail "Z must be between a positive whole number from $min to $max. Got <$_>."
};

I believe that's your intent. Failing with your own exception is fine too, but X::TypeCheck has a bug in it. It should either require "operation" or provide a reasonable default as it does for "got" and "expected".

subset ZInt of Cool is export where {
    state ( $min, $max ) = <1 120>;
    ( $_.truncate == $_ and $min <= $_ <= $max )
    or fail X::TypeCheck.new(
            operation => "type check",
            expected  => ::('ZInt'),
            got       => $_,
        );
};
like image 85
zostay Avatar answered Oct 22 '22 04:10

zostay


You could pass --ll-exception and try to figure out how exactly you end up with the errors and messages you got, but I'm not sure how helpful that will be.

As to the warning about use of an uninitialzed value: You need to provide a named operation argument to X::TypeCheck.new; other arguments you may provide are got and expected, cf core/Exception.pm.

It is however a Bad Idea to throw from a subset declaration as any smartmatch against that particular type will now explode. A slightly better idea would be to .fail the exception, but that still doesn't feel right to me: Not being a member of a subset type is not an exceptional condition.

Alternatively, you could provide a multi candidate that does the dying:

subset ZInt of Cool where $_ %% 1 && $_ ~~ 1..120;

proto foo($) {*}
multi foo(ZInt $Z) { say $Z }
multi foo($Z) {
    die X::TypeCheck.new(
        operation => 'foo',
        got => $Z,
        expected => ZInt
    );
}

That still has issues if you provide an argument like "hello" that fails on numeric conversion as %% will throw instead of propagating the failure, which could be considered a defect with the Rakudo core setting.

You can work around that one via things like

subset ZInt of Cool where { try $_ %% 1 && $_ ~~ 1..120 }

or

subset ZInt of Cool where { .Numeric andthen $_ %% 1 && $_ ~~ 1..120 }

The whole interaction of argument type checking, subsets or where-clauses, failures and exceptions can be somewhat brittle, so you may want to experiment a bit until you arrive at semantics and behaviour you like.

Another approach would be doing a coercion from Cool to Int with a separate range check:

subset ZInt of Int where 1..120 ;

sub foo(Int(Cool) $Z where ZInt) {
    say $Z.perl;
}

In an ideal world, there should be some way to express this with a coercing type constraint like ZInt(Cool).

like image 42
Christoph Avatar answered Oct 22 '22 05:10

Christoph