如何为类创建工厂?出现“未声明名称”错误

Ste*_*ieD 11 raku

我有这个代码:

class kg is Dimension {
    method new() {
        return self.bless(
                :type('mass'),
                :abbr('kg'),
                :multiplier(Multiplier.new(
                        numerator =>   1.0,
                        denominator => Quantity.new(1000.0, 'g')))),
    }
}

class mg is Dimension {
    method new() {
        return self.bless(
                :type('mass'),
                :abbr('mg'),
                :multiplier(Multiplier.new(
                        numerator =>   1000.0,
                        denominator => Quantity.new(1.0, 'g')))),
    }
}

Run Code Online (Sandbox Code Playgroud)

我将添加更多类似的课程。我不想单独阐明所有这些类,而是想学习如何创建一个可以从简单数据结构创建这些类的工厂。

我该怎么做呢?我阅读了元对象协议文档,但我不知道如何根据文档页面顶部和中间的示例为我的类动态指定不同的名称。

我试过:

constant A := Metamodel::ClassHOW.new_type( name => 'A' );
A.^add_method('x', my method x(A:) { say 42 });
A.^add_method('set', my method set(A: Mu \a) { A.^set_name(a) });
A.^compose;

my $bar = A;
$bar.set('Foo');
say $bar.^name;  # 
A.x;             # works
Foo.x;           # error
Run Code Online (Sandbox Code Playgroud)

但最后一行只是抛出一个错误:

Undeclared name:
    Foo used at line 13
Run Code Online (Sandbox Code Playgroud)

Eli*_*sen 10

您应该意识到的第一件事是,任何类型的元编程通常都需要在编译时完成,也就是在BEGIN块中完成。

其次:目前,Raku 具有一些用于创建代码的元编程功能,但并不是使代码尽可能轻松所需的所有功能。RakuAST 的工作将改变这一点,因为它基本上使 Raku 本身是从公共元编程 API 构建的(而不是你可能会说的,当前的引导版本使用了大量 NQP)。

我已将您的代码重写为以下内容:

sub frobnicate(Str:D $name) {
    my \A := Metamodel::ClassHOW.new_type(:$name);
    A.^add_method('x', my method x() { say 42 });
    A.^compose;
    OUR::{$name} := A;
}

BEGIN frobnicate("Foo");
say Foo.^name;  # Foo
Foo.x;          # 42
Run Code Online (Sandbox Code Playgroud)

因此,这引入了一个名为的子函数frobnicate,它使用给定名称创建一个新类型。method x向其添加 a ,并组成新类型。然后确保它our在当前编译单元中被称为 an 。

然后在编译时通过添加前缀来frobnicate调用子程序。这很重要,因为否则在编译下一行时将无法知道,因此您会收到错误。BEGINFoo

目前有一个小问题:

dd Foo.^find_method("x").signature;  # :(Mu: *%_)
Run Code Online (Sandbox Code Playgroud)

未设置调用者约束。我还没有找到一种方法(在 RakuAST 之前)使用元编程接口来设置它。但我认为这对于您给出的示例来说不会成为问题。如果这确实成为一个问题,那么当我们到达那里时,让我们跨过那座桥。

  • 惊人的。谢谢!我曾经尝试过使用 BEGIN 块,但我并没有像您那样将工厂包装在子例程中。很酷。会尝试一下。 (3认同)
  • 添加行 `A.^add_parent(Int);` 以使其继承自 `Int` ?? (2认同)

Ste*_*ieD 6

这是我为解决方案提出的完整代码:

#!/usr/bin/env raku
use v6.d;

class Dimension { }

sub dimension-attr-factory($name, Mu $type, Mu $package) {
    return Attribute.new(
            :name('$.' ~ $name),
            :type($type),
            :has_accessor(1),
            #:is_required(1),
            :package($package)
            );
}

sub dimension-factory(Str:D $name, @attributes) {
    my \A := Metamodel::ClassHOW.new_type(:$name);
    A.^add_parent(Dimension);
    for @attributes {
        my $attr = dimension-attr-factory($_[0], $_[1], A);
        A.^add_attribute($attr);
    }
    A.^compose;
    OUR::{$name} := A;
}

class Multiplier {
    has Rat $.numerator;
    has Quantity $.denominator;

    method factor() {
        return $.numerator / $.denominator.value;
    }
}


class Quantity {
    has Rat() $.value is required;
    has Dimension:D $.dimension is required;

    multi submethod BUILD(Rat:D() :$!value, Dimension:D :$!dimension) {
    }
    multi submethod BUILD(Rat:D() :$value, Str:D :$dimension) {
        $!dimension = ::($dimension).new;
    }
    multi method new(Rat:D() $value, Dimension:D $dimension) {
        return self.bless(
                :$value,
                :$dimension,
                )
    }
    multi method new(Rat:D() $value, Str:D $dimension) {
        return self.bless(
                :$value,
                :$dimension,
                )
    }
    method to(Str:D $dimension = '') {
        my $from_value = $.value;
        my $to = $dimension ?? ::($dimension).new !! ::(self.dimension.abbr).new;

        # do types match?
        if $to.type ne self.dimension.type {
            die "Cannot convert a " ~ self.dimension.type ~ " to a " ~ $to.type;
        };

        my $divisor = $.dimension.multiplier ?? $.dimension.multiplier.factor !! 1.0;
        my $dividend = $to.multiplier ?? $to.multiplier.factor !! 1;
        my $result = $dividend / $divisor * $from_value;
        return Quantity.new($result, $to);
    }
    method gist() {
        $.value ~ ' ' ~ $.dimension.abbr;
    }
}

BEGIN {
    my %dimensions = 'mass' => {
        base => {
            abbr => 'g',
        },
        derived => {
            kg => { num => 1000.0, den => 1.0, },
            mg => { num => 1.0, den => 1000.0, },
            ug => { num => 1.0, den => 1000000.0, },
        }
    };

    for %dimensions.kv -> $key, $value {
        # set up base class for dimension type
        my $base = %dimensions{$key}<base><abbr>;
            my @attributes = ['abbr', $base], ['type', $key];
            dimension-factory( $base, @attributes);

            my %derived = %dimensions{$key}<derived>;
            for %derived.kv -> $abbr, $values {
                my $numerator = %dimensions{$key}<derived>{$abbr}<num>;
                my $denominator = %dimensions{$key}<derived>{$abbr}<den>;
                my $multiplier = Multiplier.new(
                                numerator => 1.0,
                                denominator => Quantity.new(1000.0, 'g'),
                );
                @attributes = ['abbr', $abbr], ['type', $key], ['multiplier', $multiplier];
                my $dim = dimension-factory( $abbr, @attributes );
                #$dim.new(:$abbr, type => $key, :$multiplier );
            }
        }
}

my $kg = kg.new();
my $quant = Quantity.new(value => 5.0, dimension => $kg);
dd $quant;
Run Code Online (Sandbox Code Playgroud)


小智 5

我可能会使用自定义元模型创建一个维度关键字,也可能会使用未定义的维度覆盖 * 和 / 运算符,然后使用以下内容创建 kg:

dimension Gram {
    has Dimension::Type $.type = mass;
    has Str             $.abbr = "g";
}

dimension KiloGram is Gram {
    has Str                   $.abbr       = "kg";
    has Dimension::Multiplier $.multiplier = 1000 * g;
}

dimension MiliGram is Gram {
    has Str                   $.abbr       = "mg";
    has Dimension::Multiplier $.multiplier = g / 1000;
}
Run Code Online (Sandbox Code Playgroud)

但也许这太过分了...