Perl 6 and `multi method new`

  • A+
Category:Languages

I have a class Price that encapsulates an Int. I also would like it to have constructors for Num and Str. I thought I could do this by making Price::new a multi method with various type constraints, but this isn't the behavior I expected. It looks like Price.new is skipping the constructor altogether and going straight to BUILD, bypassing the casting logic.

I know from looking at other Perl 6 code that using multi method new is acceptable. However, I haven't been able to find an example of a polymorphic constructor with different type constraints. How do I rewrite this code to force it to use the casting logic in the constructor?

lib/Price.pm6

#!/usr/bin/env perl6 -w  use v6;  unit class Price:ver<0.0.1>;  class X::Price::PriceInvalid is Exception {     has $.price;      method message() {         return "Price $!price not valid"     } }  # Price is stored in cents USD has $.price;  multi method new(Int $price) {     say "Int constructor";     return self.bless(:$price); }  multi method new(Num $price) {     say "Num constructor";     return self.new(Int($price * 100)); }  multi method new(Str $price) {     say "String constructor";     $price .= trans(/<-[0..9.]>/ => '');     unless ($price ~~ m//./d**2$/) {         die(X::Price::PriceInvalid(:$price));     }     return self.new(Num($price)); }  submethod BUILD(:$!price) { say "Low-level BUILD constructor" }  method toString() {     return sprintf("%.2f", ($!price/100)); } 

t/price.t

#!/usr/bin/env perl6 -w  use v6; use Test;  use-ok 'Price', 'Module loads'; use Price;  # test constructor with Int my Int $priceInt = 12345; my $priceIntObj = Price.new(price => $priceInt); is $priceIntObj.toString(), '123.45',     'Price from Int serializes correctly';  # test constructor with Num my $priceNum = Num.new(123.45); my $priceNumObj = Price.new(price => $priceNum); is $priceNumObj.toString(), '123.45',     'Price from Num serializes correctly';  # test constructor with Num (w/ extra precision) my $priceNumExtra = 123.4567890; my $priceNumExtraObj = Price.new(price => $priceNumExtra); is $priceNumExtraObj.toString(), '123.45',     'Price from Num with extra precision serializes correctly';  # test constructor with Str my $priceStr = '$123.4567890'; my $priceStrObj = Price.new(price => $priceStr); is $priceStrObj.toString(), '123.45',     'Price from Str serializes correctly';  # test constructor with invalid Str that doesn't parse my $priceStrInvalid = 'monkey'; throws-like { my $priceStrInvalidObj = Price.new(price => $priceStrInvalid) }, X::Price::PriceInvalid,     'Invalid string does not parse';  done-testing; 

Output of PERL6LIB=lib/ perl6 t/price.t

ok 1 - Module loads Low-level BUILD constructor ok 2 - Price from Int serializes correctly Low-level BUILD constructor not ok 3 - Price from Num serializes correctly # Failed test 'Price from Num serializes correctly' # at t/price.t line 18 # expected: '123.45' #      got: '1.23' Low-level BUILD constructor not ok 4 - Price from Num with extra precision serializes correctly # Failed test 'Price from Num with extra precision serializes correctly' # at t/price.t line 24 # expected: '123.45' #      got: '1.23' Low-level BUILD constructor Cannot convert string to number: base-10 number must begin with valid digits or '.' in '⏏/$123.4567890' (indicated by ⏏)   in method toString at lib/Price.pm6 (Price) line 39   in block <unit> at t/price.t line 30 

 


All of the new multi methods that you wrote take one positional argument.

:( Int $ ) :( Num $ ) :( Str $ ) 

You are calling new with a named argument though

:( :price($) ) 

The problem is that since you didn't write one that would accept that, it uses the default new that Mu provides.


If you don't want to allow the built-in new, you could write a proto method to prevent it from searching up the inheritance chain.

proto method new (|) {*} 

If you want you could also use it to ensure that all potential sub-classes also follow the rule about having exactly one positional parameter.

proto method new ($) {*} 

If you want to use named parameters, use them.

multi method new (Int :$price!){…} 

You might want to leave new alone and use multi submethod BUILD instead.

multi submethod BUILD (Int :$!price!) {     say "Int constructor"; }  multi submethod BUILD (Num :$price!) {     say "Num constructor";     $!price = Int($price * 100);  }  multi submethod BUILD (Str :$price!) {     say "String constructor";     $price .= trans(/<-[0..9.]>/ => '');     unless ($price ~~ m//./d**2$/) {         die(X::Price::PriceInvalid(:$price));     }     $!price = Int($price * 100); } 

Actually I would always multiply the input by 100, so that 1 would be the same as "1" and 1/1 and 1e0.
I would also divide the output by 100 to get a Rat.

unit class Price:ver<0.0.1>;  class X::Price::PriceInvalid is Exception {     has $.price;      method message() {         return "Price $!price not valid"     } }  # Price is stored in cents USD has Int $.price is required;  method price () {     $!price / 100; # return a Rat }  # Real is all Numeric values except Complex multi submethod BUILD ( Real :$price ){     $!price = Int($price * 100); }  multi submethod BUILD ( Str :$price ){     $price .= trans(/<-[0..9.]>/ => '');     unless ($price ~~ m//./d**2$/) {         X::Price::PriceInvalid(:$price).throw;     }     $!price = Int($price * 100); }  method Str() {     return sprintf("%.2f", ($!price/100)); } 

Comment

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: