Search code examples
signatureraku

How can I convert a Str to an Int only when it represents an integer?


In Perl 6 you can specify a type that a type can be coerced to. For example, you need an Int but get something else that can convert to an Int. This is handy when you don't want separate candidates for Int and Str where the string represents an integer value.

But, it seems that the conversion is a bit aggressive because the conversion not only changes the type but is willing to change the data. It's partly a problem of the conflation of changing types and an expected operation to truncate a number to an integer. Conceptually those are different ideas but they are intertwined in Str.Int (actually sandwiching a side trip to Numeric):

sub foo ( Int:D() $n ) { put "Got <$n> of type {$n.^name}" }
foo( 80 );     # Got <80> of type Int
foo( '99' );   # Got <99> of type Int
foo( 1.5 );    # Got <1> of type Int
foo( '1.5' );  # Got <1> of type Int

Trying to limit this to Str isn't any better:

sub foo ( Int:D(Str:D) $n ) { put "Got <$n> of type {$n.^name}" }
foo( '1.5' );  # Got <1> of type Int

I could make some adapters which seems the easiest to understand:

multi foo ( Int:D $n ) {
    put "Got <$n> of type {$n.^name}"
    }
multi foo ( Str:D $n where { $^n.Int == $^n.Numeric } ) {
    foo( $n.Int );
    }

foo( '1.5' );  # Cannot resolve ...

And I can probably come up with some subsets but that's not any more satisfying. So the trick is, can I coerce like this without changing the value (even if it changes representation)?


It turns out that this feature is broken and doesn't have a timeline for repair: RT 132980. Basically, the target type is not enforced. The docs are updated. My advice is to not use this at all.


Solution

  • The way Int:D(Any) works in Rakudo is by creating a multi candidate that accepts Any, converts it to an Int, and uses the result to call your original subroutine.

    If you instead do that yourself, you can have more control over how it works.

    proto sub foo ( Int:D() $n ) {*}
    
    multi sub foo ( Any:D $n ) {
      my $i = try $n.Numeric.narrow;
    
      if $i ~~ Int:D {
    
        samewith $i
    
      } else {
    
        X::TypeCheck::Binding::Parameter.new(
    
          # there are more arguments that should be added here
          got => $n,
          expected => Int:D(),
    
        ).throw
    
      }
    }
    
    multi sub foo ( Int:D $n ) { put "Got <$n> of type {$n.^name}" }