Perl 6 和“多方法新”
Perl 6 and `multi method new`
我有一个 class Price
封装了一个 Int
。我还希望它具有 Num
和 Str
的构造函数。我以为我可以通过使 Price::new
成为具有各种类型约束的多方法来做到这一点,但这不是我预期的行为。看起来 Price.new
完全跳过构造函数并直接进入 BUILD
,绕过了转换逻辑。
我通过查看其他 Perl 6 代码知道使用 multi method new
是可以接受的。但是,我一直无法找到具有不同类型约束的多态构造函数的示例。我如何重写此代码以强制它在构造函数中使用转换逻辑?
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 = '3.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;
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
new
方法被声明为采用位置参数:
multi method new(Int $price) {
say "Int constructor";
return self.bless(:$price);
}
但随后被调用为 Price.new(price => $priceInt)
,它正在传递一个命名参数。因此,由于所有希望额外位置参数的 multi
候选人都不适用。
最直接的解决方法是将构造函数调用更改为 Price.new($priceInt)
。
另一种选择是将 new
方法编写为 multi method new(Int :$price) { ... }
,注意 return self.new(Int($price * 100));
需要变为 return self.new(price => Int($price * 100));
以适应该更改。
关于代码的其他一些可能有用的注释:
- 通常重写
new
方法以将 接口 更改为构造(例如接受位置参数而不是命名参数),而 BUILD
和 TWEAK
用于控制值如何映射到属性。如果您选择让 new
方法采用命名参数,那么在 BUILD
. 中处理强制登录可能会更好
- 在 Perl 6 中,
Num
是一个浮点数,而 Rat
是一个有理数(存储为整数分子和分母)。文字 123.4567890
不是 Num
,而是 Rat
。 Num
文字总是有一个 e
指数部分(如 123.45e1
)。然而,由于这里的问题是处理货币,Rat
实际上是正确的选择,所以我将代码更改为使用 Rat
类型,而不是 Num
,并保留文字原样。
toString
方法在 Perl 6 中更自然地命名为 Str
。类型定义了它们如何通过编写具有该类型名称的方法来强制转换为其他内容。调用它 Str
意味着如果 Price
实例被插入到字符串中,或者与 ~
前缀运算符一起使用,它将自动被调用。
- 需要构造异常,所以
die(X::Price::PriceInvalid(:$price));
应该是die(X::Price::PriceInvalid.new(:$price));
.
您编写的所有 new
多方法都采用一个位置参数。
:( Int $ )
:( Num $ )
:( Str $ )
虽然您正在使用命名参数调用 new
:( :price($) )
问题在于,由于您没有编写接受该设置的程序,因此它使用 Mu
提供的默认值 new
。
如果您不想允许内置 new
,您可以编写一个 proto
方法来防止它向上搜索继承链。
proto method new (|) {*}
如果你愿意,你也可以使用它来确保所有潜在的子类也遵循关于只有一个位置参数的规则。
proto method new ($) {*}
如果您想使用命名参数,请使用它们。
multi method new (Int :$price!){…}
您可能希望单独使用 new
并改用多子方法 BUILD
。
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);
}
实际上我总是将输入乘以 100
,因此 1
与 "1"
和 1/1
以及 1e0
相同。
我还将输出除以 100 得到一只老鼠。
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));
}
我有一个 class Price
封装了一个 Int
。我还希望它具有 Num
和 Str
的构造函数。我以为我可以通过使 Price::new
成为具有各种类型约束的多方法来做到这一点,但这不是我预期的行为。看起来 Price.new
完全跳过构造函数并直接进入 BUILD
,绕过了转换逻辑。
我通过查看其他 Perl 6 代码知道使用 multi method new
是可以接受的。但是,我一直无法找到具有不同类型约束的多态构造函数的示例。我如何重写此代码以强制它在构造函数中使用转换逻辑?
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 = '3.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;
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
new
方法被声明为采用位置参数:
multi method new(Int $price) {
say "Int constructor";
return self.bless(:$price);
}
但随后被调用为 Price.new(price => $priceInt)
,它正在传递一个命名参数。因此,由于所有希望额外位置参数的 multi
候选人都不适用。
最直接的解决方法是将构造函数调用更改为 Price.new($priceInt)
。
另一种选择是将 new
方法编写为 multi method new(Int :$price) { ... }
,注意 return self.new(Int($price * 100));
需要变为 return self.new(price => Int($price * 100));
以适应该更改。
关于代码的其他一些可能有用的注释:
- 通常重写
new
方法以将 接口 更改为构造(例如接受位置参数而不是命名参数),而BUILD
和TWEAK
用于控制值如何映射到属性。如果您选择让new
方法采用命名参数,那么在BUILD
. 中处理强制登录可能会更好
- 在 Perl 6 中,
Num
是一个浮点数,而Rat
是一个有理数(存储为整数分子和分母)。文字123.4567890
不是Num
,而是Rat
。Num
文字总是有一个e
指数部分(如123.45e1
)。然而,由于这里的问题是处理货币,Rat
实际上是正确的选择,所以我将代码更改为使用Rat
类型,而不是Num
,并保留文字原样。 toString
方法在 Perl 6 中更自然地命名为Str
。类型定义了它们如何通过编写具有该类型名称的方法来强制转换为其他内容。调用它Str
意味着如果Price
实例被插入到字符串中,或者与~
前缀运算符一起使用,它将自动被调用。- 需要构造异常,所以
die(X::Price::PriceInvalid(:$price));
应该是die(X::Price::PriceInvalid.new(:$price));
.
您编写的所有 new
多方法都采用一个位置参数。
:( Int $ )
:( Num $ )
:( Str $ )
虽然您正在使用命名参数调用 new
:( :price($) )
问题在于,由于您没有编写接受该设置的程序,因此它使用 Mu
提供的默认值 new
。
如果您不想允许内置 new
,您可以编写一个 proto
方法来防止它向上搜索继承链。
proto method new (|) {*}
如果你愿意,你也可以使用它来确保所有潜在的子类也遵循关于只有一个位置参数的规则。
proto method new ($) {*}
如果您想使用命名参数,请使用它们。
multi method new (Int :$price!){…}
您可能希望单独使用 new
并改用多子方法 BUILD
。
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);
}
实际上我总是将输入乘以 100
,因此 1
与 "1"
和 1/1
以及 1e0
相同。
我还将输出除以 100 得到一只老鼠。
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));
}