Home > Raku > Coercive bits

Coercive bits

Altreus was looking for a way to convert a list of bitmask names, provided as a string by the user, to a bitmask. He wished for BUILDARGS, as provided by Perl’s Moose but was given good advise how to solve the problem without making object instantiation even more complex. Nobody can stop me from doing just that.

With Moose, BUILDARGS allows to modify values before they are bound to attributes of a class. We can do the same by using the COERCE-protocol, not just for Routine-arguments.

role BitMask[@names] {
    has int $.mask;
    sub names-to-bits(@a) {
        my int $mask;
        for @a -> $s {
            $mask = $mask +^ (1 +< @names.first($s, :k) // fail('bad‼'))
        }

        $mask
    }
    sub bits-to-names($mask) {
        ($mask.base(2).comb.reverse Z @names).map: -> [Int() $is-it, $name] { $is-it ?? $name !! Empty }
    }

    multi method COERCE(Str $s) { self.COERCE: $s.split(' ').list }
    multi method COERCE(List $l) { self.new: :mask($l.&names-to-bits) }

    method raku { "BitMask[<@names[]>](<{$!mask.&bits-to-names}>)" }
    method bits(--> Str) { $.mask.fmt('%b') }
}

class C {
    has BitMask[<ENODOC ENOSPEC LTA SEGV>]() $.attr is required;
}

my $c = C.new: :attr<ENODOC ENOSPEC SEGV>;
say $c;
say $c.attr.bits;
# OUTOUT: C.new(attr => BitMask[<ENODOC ENOSPEC LTA SEGV>](<ENOSPEC LTA SEGV>))
#         1011 

Here I build a role that carries the names of bits in a bit-field and will create the bit-field when give a Str or List containing those names. Since I use a parametrised role, my bitfield is type-safe and the type is tied to the actual names of the bits. As a consequence a user of the module that exports C can extend the accepted types by using that specific role-candidate.

enum IssueTypes ( ENODOC => 0b0001, ENOSPEC => 0b0010, LTA => 0b0100, SEGV => 0b1001 );

class IssueTypesBits does BitMask[<ENODOC ENOSPEC LTA SEGV>] {
    method new(*@a where .all ~~ IssueTypes) {
        self.bless: mask => [+|] @a
    }
}

sub bitmask(*@a where .all ~~ IssueTypes) {
    IssueTypesBits.new: |@a
}

my $c3 = C.new: attr => BEGIN bitmask(ENODOC, LTA, SEGV);
say $c3;
say $c3.attr.bits;
# OUTPUT: C.new(attr => BitMask[<ENODOC ENOSPEC LTA SEGV>](<ENODOC LTA SEGV>))
          1101

Here I define an enum and provide the same names in the same order as in does BitMask. With the shim bitmask I create the type-safety bridge between IssueTypes and BitMask[<ENODOC ENOSPEC LTA SEGV>].

It is very pleasing how well the new COERCE-protocol ties into the rest of the language, because we can use a coercing-type at any place in the source code which takes a normal type as well. What is not pleasing are the LTA-messages X::Coerce::Impossible is producing.

my $c4 = C.new: attr => 42;
# OUTPUT: Impossible coercion from 'Int' into 'BitMask[List]': method new returned a type object NQPMu
            in block <unit> at parameterised-attribute.raku line 60

This is surpring because we can get hold of the candidates for COERCE at runtime.

CATCH {
    when X::Coerce::Impossible && .target-type.HOW === Metamodel::CurriedRoleHOW {
        put "Sadly, COERCE has failed for {.from-type.^name}. Available candidates are: ", .target-type.new.^lookup('COERCE').candidates».signature».params[*;1]».type.map(*.^name)
    }
}

# OUTPUT: Sadly, COERCE has failed for Int. Available candidates are: Str List

What we can’t see are the candidates of IssueTypesBits, because that is hidden behind the constructor new. I tried to use the MOP to add another multi candidate but failed. When parametrising the underlying type-object gets cloned. Any change to the multi-chain wont propagate to any specialised types.

The COERCE-protocol is quite useful indeed. Maybe it’s time to document it.

Categories: Raku