Coercion types in signatures don’t work and won’t for awhile

I wanted to make a method that took an integer or a string that looked like an integer. I thought a coercion type would work nicely for that. The Int is the target type and Str is the type it will accept then coerce:

sub something ( Int:D(Str:D) $n ) { ... }

something( 37 );
something( "137" );
something( "Hello" ); # I want this to fail
something( 1.5 );     # I want this to fail too

That didn’t quite work out. If I pass it a Str it calls .Int. A string like "1.5" converts just fine because the .Int not only changes types but can change the data. We are used to int() as a way to make floating point numbers into whole numbers. So, .Int has two jobs: managing types and managing values. I don’t like that and don’t think that 3.14 is 3. I asked about it on StackOverflow and got some interesting answers. Brad Gilbert shows the long hard way which is close to what I was already doing but there’s no need for the coercion type then. I was a little disappointed.

Then I wondered what would happen if a .Int method did not return the right sort of type. I didn’t expect this to run without an error (I originally typed “expect this to work”!) but it does. I made an .Int that returned a Str. That’s silly but I can imagine myself making stupid error like this and typing should show my stupidity:

class Foo {
    method Int ( --> Str:D ) { 'Hello' }
    }

put try-it( Foo.new );

sub try-it ( Int:D() $n ) { "Got 「$n」 of type 「{$n.^name}」" }

Although the subroutine signature demanded an Int it accepted something that claimed to be able to convert but actually didn’t. The .Int method is the right thing to call but there was nothing to check that it did the right thing:

Got 「Hello」 of type 「Str」

I would have expected the runtime constraint to check the ultimate value against the type and this would have failed. But it doesn’t check that the value ends up the correct type. Assigning to a value with a type limitation works though:

class Foo {
	method Int { "Hello" }
	}

my Str $m = Foo.Int;  # works
my Int $n = Foo.Int;  # this fails as expected

I filed RT 132980 which led to some clarifications in the documentation but also a note that it’s on the back burner for a fix because the proper type check leads to a slowdown (even if you don’t use it, I take it). The motto of Rakudo had been “make it work right then make it work fast”, but sometimes there are other trade-offs that are more important.

2 comments

  1. Perhaps this could be compile-time checked: if the name of a method in a class matches an existing class, we could check if the `.returns` of the Method object actually matches the name of the method (and the class). And if not, generate a compilation error. And if there is no explicit `.returns` yet, set it to the class with the name of the method.

    That way we would get an early indication of the developer doing something wrong *and* the normal runtime type-checking.

    1. I think that might work. So far all the types I know start with an uppercase letter, but there may be situations where people accidentally define a method with the same name as a type they are unaware of. That case should as least get a warning. But people are going to make lowercase class names too, which leads to odd order-of-definition issues.

      Ideally coercion methods would know their purpose through a trait or something. That allows the syntax to signal the intent instead of inferring it from a list maintained somewhere else (and potentially not safe from future type additions).

      I think some people were recently talking about the difference between .list and .List. I know that’s something I’ve carefully noted in the book.

Leave a Reply

Your email address will not be published. Required fields are marked *