Perl 6 и `multi method new` - PullRequest
       10

Perl 6 и `multi method new`

0 голосов
/ 11 июня 2018

У меня есть класс 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

#!/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;

Выход 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

Ответы [ 2 ]

0 голосов
/ 11 июня 2018

Все написанные вами new multi-методы принимают один позиционный аргумент.

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

Вы вызываете new с именованным аргументом, хотя

:( :price($) )

Проблема в том, чтотак как вы не написали тот, который принял бы это, он использует значение по умолчанию new, которое обеспечивает Mu.


Если вы не хотите разрешать встроенный new,вы можете написать метод proto, чтобы он не мог искать в цепочке наследования.

proto method new (|) {*}

Если вы хотите, вы также можете использовать его, чтобы гарантировать, что все потенциальные подклассы также следуют правилу о наличии точноодин позиционный параметр.

proto method new ($) {*}

Если вы хотите использовать именованные параметры, используйте их.

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

Возможно, вы захотите оставить new в покое и использоватьmulti submethod 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));
}
0 голосов
/ 11 июня 2018

Методы 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 будет более естественно называться Str в Perl 6. Типы определяют, как они взаимодействуют с другими вещами путем написания метода с этим именем типа.Вызов его Str будет означать, что он будет вызываться автоматически, если экземпляр Price интерполируется в строку или используется с префиксным оператором ~.
  • Необходимо создать исключение, поэтому die(X::Price::PriceInvalid(:$price)); должно быть die(X::Price::PriceInvalid.new(:$price));.
...