11

I have this code:

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')))),
    }
}

I'll be adding many more similar classes. Rather than spell out all these classes separately, I'd like to learn how to create a factory that can create these classes from simple data structures.

How do I do this? I read the Metaobject Protocol doc but I couldn't figure out how to give my classes different names on the fly based on the examples at the top and middle of the doc page.

I tried:

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

But the last line just throws an error:

Undeclared name:
    Foo used at line 13
TylerH
  • 20,799
  • 66
  • 75
  • 101
StevieD
  • 6,925
  • 2
  • 25
  • 45

3 Answers3

10

The first thing you should realize, that any kind of meta-programmming usually will need to be done at compile time, aka in a BEGIN block.

Secondly: at the moment, Raku has some meta-programming features for creating code, but not all features needed to make this as painless as possible. The work on RakuAST will change that, as it basically makes Raku itself being built from a public meta-programming API (rather than you could argue, the current bootstrap version using a lot of NQP).

I've rewritten your code to the following:

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

So, this introduces a sub called frobnicate that creates a new type with the given name. Adds a method x to it, and composes the new type. And then makes sure it is known as an our in the current compilation unit.

Then the frobnicate sub is called at compile time by prefixing a BEGIN. This is important, because otherwise Foo won't be known when the next line is compiled, so you'd get errors.

There is currently a small catch:

dd Foo.^find_method("x").signature;  # :(Mu: *%_)

The invocant constraint is not set. I haven't found a way (before RakuAST) to set that using an meta-programming interface. But I don't think that's going to be an issue for the example you've given. If it does become an issue, then let's cross that bridge when we get there.

Elizabeth Mattijsen
  • 25,654
  • 3
  • 75
  • 105
  • 3
    Awesome. Thanks! I had tried to use a BEGIN block at one point but it hadn’t struck me to wrap the factory in a subroutine like you did. Very cool. Will give this a shot. – StevieD Jun 09 '22 at 11:54
  • 1
    Ok, ran into a problem. How do I make the `A` metaobject a child of another class? – StevieD Jun 09 '22 at 16:11
  • 1
    Looks like I need this: https://docs.raku.org/routine/add_parent but it's not clear how I should implement it. – StevieD Jun 09 '22 at 16:44
  • 2
    Add the line `A.^add_parent(Int);` to make it inherit from `Int` ?? – Elizabeth Mattijsen Jun 09 '22 at 18:04
6

Here is the entire code that I came up with for a solution:

#!/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;
StevieD
  • 6,925
  • 2
  • 25
  • 45
  • I would get rid of the `BUILD` methods in the `Quantity` class and do the conversion of `Str:D` dimension to `Dimension:D` inside the `new` candidate that accepts the `Str:D` dimension: `:dimension(::($dimension).new)`. Also, more stylistically, I would not use `return` at the end of a sub/method. To me, `return` indicates an early return from a sub/method, so something special :-) – Elizabeth Mattijsen Jun 10 '22 at 08:05
5

I would probably create a dimension keyword with a custom metamodel, would probably also override * and / operators using undefined dimensions and then would create kg with something like:

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;
}

but maybe that's too much...

SmokeMachine
  • 426
  • 2
  • 5