Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use abstract multi methods containing a where?

I'm working on a Perl 6 module, Pod::To::Anything, in an effort to make an easy interface to create Perl 6 Pod formatters. I'm splitting up rendering into multiple render methods, each having to deal with a given part of the Pod specification.

To ensure the Pod formatters based on this class are complete, I want to add in abstract methods that cover all the possible Pod objects. However, this requires me to use a where clause a couple of times:

multi method render (Pod::Block::Named:D $ where *.name eq "NAME" --> Str) { … }

I've tried to implement it as follows:

multi method render (Pod::Block::Named:D $pod where *.name eq "NAME" --> Str) { ".TH {self.pod-contents($pod)}\n" }

However, when I try to run the program, Perl 6 complains about a method not being implemented:

===SORRY!=== Error while compiling /home/tyil/projects/personal/perl6-Pod-To-Man/lib/Pod/To/Man.pm6 (Pod::To::Man)
Multi method 'render' with signature :(Pod::To::Man $: Pod::Block::Named:D $ where { ... }, *%_ --> Str) must be implemented by Pod::To::Man because it is required by a role at /home/tyil/projects/personal/perl6-Pod-To-Man/lib/Pod/To/Man.pm6 (Pod::To::Man):7

It's LTA that Perl 6 hides the actual contents of the where part here, but that is not my main issue. The issue I have is that it tells me a method I did implement is not implemented.

I've golfed it down a little to ensure this is not a specific problem to my current codebase:

role Foo { multi method test(%foo where *<bar>) { … } }
class Bar does Foo { multi method test(%foo where *<bar>) { "Implementation" } }

This code errors with a similar error:

===SORRY!=== Error while compiling /tmp/tmp.o2aoet3JrE/t.pl6
Multi method 'test' with signature :(Bar $: %foo where { ... }, *%_) must be implemented by Bar because it is required by a role
at /tmp/tmp.o2aoet3JrE/t.pl6:5

My question becomes: How can I use abstract multi methods containing a where clause in Perl 6?

like image 391
Tyil Avatar asked Jul 28 '18 10:07

Tyil


People also ask

Can abstract class have multiple implementation?

In ABSTRACT class,we can't extends multiple abstract classes at a time. but In INTERFACE, we can implements multiple interfaces at time. Therefore , interfaces are used to achieve multiple inheritance in java.

Can abstract class have multiple abstract methods?

An abstract class in Java is one that is declared with the abstract keyword. It may have both abstract and non-abstract methods(methods with bodies).

Can abstract methods have parameters?

Yes, we can provide parameters to abstract method but it is must to provide same type of parameters to the implemented methods we wrote in the derived classes.

How many abstracts methods should an abstract class have?

Top Features Of Abstract Class Abstract class needs to have at least one method that is abstract.


1 Answers

TL;DR The issues here relate to the fundamental nature of where clauses and P6. Rakudo's error message is LTA. You can use where clauses but you'll have to change how you're using them.

where clauses and separate compilation of modules

P6 compiles modules. It does not store source code in the modules, including the source code of the where clause. So when it goes to compare the where clause in a user's source file that uses the module containing the role, with the where clause in the role, it can't know they're the same so it conservatively decides it can't accept it.

LTA error message

As explained, the compiler does not store the source code in the compiled module. So that's why it's showing where { ... }.

What it could do is produce an awesome "you can't do that", perhaps with the actual where clause source code, when compiling the role rather than waiting to inevitably fail when compiling the class doing the role.

I've searched rt.perl.org and the github rakudo repo issues and have not found a corresponding ticket. So I've opened #2146.

P6's nominal type dispatch supports use of subsets

Routine dispatch is primarily driven by nominal (named) types.

By declaring a subset you can give a where constraint a name which you can then plug into signatures thus enabling routine dispatch to do as you wish:

subset Nominal-Type where *.key eq 'bar';
role Foo { multi method test(Nominal-Type $baz) { … } }
class Bar does Foo { multi method test(Nominal-Type $baz) { "Implementation" } }

Bar.new.test: my Nominal-Type $baz = :bar

Subsets and sigils

It's important to note that there's an aged bug that means subsets do not work with variables declared with the explicitly composite sigils (% and @).

So you must either use the scalar sigil $ or slash the sigil.

This requirement applies both to the signatures of the abstract methods you write in your roles and to the signatures of the concrete methods your users write.

Composite subsets

You can write composite subsets. The example above is a scalar subset, but you could write, say:

subset Nominal-Type-Hash of Hash where *<bar>;
role Foo { multi method test(Nominal-Type-Hash \qux) { … } }
class Bar does Foo { multi method test(Nominal-Type-Hash \qux) { "Implementation" } }

Bar.new.test: my %baz := my Nominal-Type-Hash \qux = { :bar }

Note that I've switched to slashing the sigil in the declaration with the subset type. This is because those using your subset may wish to bind to a new variable with a sigil, as I did in the last line and they might in the body of their methods.

Slashing the sigil instead of using $ ensures that a sigil'd alias is clearly distinct. Users can't, for example, accidentally write the $ sigil'd version of the variable name when they meant to write the % sigil'd version. Change the name too for extra safety points:

class Bar does Foo { multi method test(Nominal-Type-Hash \qux) { my %baz := qux; # use %baz from here on... } }

A subset for your use case

So, you can subset an existing nominal type to create a new nominal type that has a distinct name from the existing type and you can (usually will) add a where clause to that new type:

subset PTA-BN of Pod::Block::Named where *.name eq "NAME"

Now code can use PTA-BN (or whatever name you choose) as the parameter type constraint. (I think that'll be simpler and less error prone for your users than copying the where clause anyway.)

Exporting your subset

Per our discussion in comments below, you'll need to add an is export to your subsets:

subset PTA-BN is export of Pod::Block::Named where ...

and also a custom EXPORT routine (sub EXPORT { { PTA-BN => PTA-BN } }) as explained here.

Subsets of subsets

You can build up subsets of subsets of subsets etc:

subset PTA-BN2 of PTA-BN where some-other-condition;

which would ensure that not only is the underlying type of the value at run-time Pod::Block::Named and its name "NAME" but also that some-other-condition is true too.

I mention this mainly as a nice prelude to ...

User defined where clauses

While routine dispatch is primarily driven by nominal (named) types, necessitating the rest of this answer, there is an exception and it actually involves where clauses!

Routine dispatch pays attention to a where clause on a parameter inasmuch as any such clause is considered epsilon (a tiny bit) narrower than not having a clause.

In your original code the corresponding parameter of the role and class methods both had a where clause so that doesn't apply. cf Signature Introspection.

But this feature enables a nice little twist in your use case. Implementing methods can combine the subset that your role provides written on the left of a parameter with a where clause the user writes on the right and they will get dibs on matching:

use Your::Module;
class User's-Class does your-role;
multi method render (PTA-BN $pod where foo --> Str) { ... } # first dibs
multi method render (PTA-BN $pod where bar --> Str) { ... } # second dibs
multi method render (PTA-BN $pod --> Str) { ... } # default
like image 139
raiph Avatar answered Dec 27 '22 02:12

raiph