8

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.

brian d foy
  • 129,424
  • 31
  • 207
  • 592
  • 1
    It seems like this would be one of the use cases for `IntStr`, but perhaps I am mistaken. – callyalater Mar 13 '18 at 18:58
  • I expanded this idea in https://www.learningperl6.com/2018/03/14/coercion-types-in-signatures-dont-work-and-wont-for-awhile/ – brian d foy Mar 15 '18 at 06:02
  • 1
    "It turns out that this feature is broken and doesn't have a timeline for repair" No, it works as designed and the return type check is on a roadmap for future language versions. –  Mar 16 '18 at 12:04
  • "It turns out that this feature is broken" Coercion types are used throughout Perl 6 and work fine. Saying the current implementation of them is broken is like saying [duck typing](https://en.wikipedia.org/wiki/Duck_typing) is broken. It's not strong, but it's not broken. The only way a coercion routine can ever get called is that its name matches the target type. All built in coercion routines make sure their return values match their target type. Anyone writing a custom coercer could easily do the same. In the meantime, Christoph's answer shows how to achieve exactly what your question asks. – raiph Mar 27 '18 at 10:34
  • If a type doesn't actually enforce the type, it's not doing what it's meant to do. That's "broken". – brian d foy Mar 27 '18 at 14:01

2 Answers2

8

One possible signature would be

Numeric() $ where Int

or, restricting to strings,

Numeric(Str:D) $ where Int
Christoph
  • 164,997
  • 36
  • 182
  • 240
  • These do prevent the subroutine from being called with the wrong types but it gives a worse error message. I'd rather have the `X::TypeCheck::Binding::Parameter` exception. – brian d foy Mar 14 '18 at 21:01
  • 1
    @briandfoy Using a recent compiler, `sub something ( Numeric() $ where Int ) { } ; something( "Hello" ) ; CATCH { .^name.say } ` displays `X::TypeCheck::Binding::Parameter`. Of course the default error message is human readable rather than just the exception type name and it doesn't just say "type check failed in binding to parameter" but adds more detail -- "cannot convert string to number" for 'hello' and "got Rat (1.5)" for "1.5". If that's what you're getting, I think that's great and that Christoph's answer succinctly, elegantly, clearly, and completely answers your original question. – raiph Mar 16 '18 at 16:09
7

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}" }
Brad Gilbert
  • 33,846
  • 11
  • 78
  • 129
  • Great answer although I'm sad that the coercion feature is broken: https://rt.perl.org/Ticket/Display.html?id=132980 – brian d foy Mar 15 '18 at 06:02