11

I seem to be having difficulty understanding how to correctly work with classes in Raku.

I am trying to create a 'Database' class that I will use throughout my cro application. However I don't seem to understand how to deal with setting instance variables at object construction (BUILD?) time.

The class below shows an example of the problem I am having when calling it with:

Calling:

my $db = Database.new(dsn => 'update me!');
say "what value is foo: " ~ $db.what-is-foo(); # prints "what value is foo: 0" but expecting 123

My Class:

use DB::Pg;

class Database {
    has Str    $.dsn is required;
    has DB::Pg $!dbh = Nil;
    has Int    $!foo = 0;

    method new (:$dsn) {
        my $dbh = DB::Pg.new(conninfo => :$dsn);
        my $foo = 123;
        return self.bless(:$dsn, dbh => $dbh, foo => $foo);
    }

    method what-is-foo() {
        return $!foo;
    }
}

So in my class constructor I would like to pass in a named arg for the database dsn. In the new method I plan to connect, and set an instance variable to the connection handle.

In this example I am using a simple integer (foo) as a test.

So far I have found the docs here to not include an example of this type of pattern, except pehaps:

use DB::Pg;

class Database {
    has Str    $.dsn is required;
    has DB::Pg $!dbh = Nil;
    has Int    $!foo = 0;

    submethod BUILD(:$dsn) {
        $!dbh = DB::Pg.new(conninfo => :$dsn);
        $!foo = 123;
    }

    method what-is-foo() {
        return $!foo;
    }
}

But that gives me:

The attribute '$!dsn' is required, but you did not provide a value for it.

Any help would be greatly appreciated!

Elizabeth Mattijsen
  • 25,654
  • 3
  • 75
  • 105
camstuart
  • 623
  • 3
  • 13

1 Answers1

11

Only attributes with public accessors (that is, the . twigil) are set automatically with bless.

You have two ways to handle this. You can set them at TWEAK or BUILD, or you can add the attribute is built to the attribute to have this done automatically for you. The attribute is built is probably the easiest in this case, just say has Int $!foo is built = 0. But it's worth expanding on the other options:

If you include a BUILD method, you are responsible for all of the set up on your own. This means both public and private attributes. But you can make your life easier by smartly naming the parameters:

method BUILD (:$!dsn, :$!dbh, :$!foo) { }

That's actually it. The signature binds the incoming values to $!dsn, etc., which of course sets them for the whole object instance. Of course, you can do fancier things here too. In any case, after BUILD, there are some additional checks done. (1) if you don't set $!dsn, because you have is required, there will be an error. (2) if you don't ultimately set $!foo or $!dbh, they will receive their default value.

With TWEAK, you get passed the same arguments as you would with BUILD, but all initial set up is done (BUILD or the auto-binding to public attributes, and all default values, plus guarantee that required values are there). You just get a chance to do some last second adjustments.

user0721090601
  • 5,276
  • 24
  • 41
  • 4
    That's an excellent answer and explains a lot. Thankyou! In this case I went down the is built route. But I have a better understanding which leads to be believe I will undoubtable use TWEAK at some time in the near future – camstuart Feb 17 '21 at 05:57
  • 3
    I think there is a push to update the docs to use TWEAK more than BUILD. The idea being that 9 times out of 10 it's want you want. And of that other time you probably want BUILD and THEN you might want to write a new method. – Scimon Proctor Feb 17 '21 at 10:27
  • 4
    Conversely, if you want an attribute to have an accessor but *not* have it settable at object creation, you can say `has $.foo is built(False)`. Sadly there is **no** syntactic sugar yet for allowing `has $.foo is !built`. – Elizabeth Mattijsen Feb 17 '21 at 11:21