Perl 6 multi methods never match expected signature - oop

I have a class with two multi methods (multi submit).
I call my multi like this:
$perspective.submit(:message($message.content));
Which gets shipped off to my class:
my $perspective-api = API::Perspective.new(:api-key(%*ENV<PERSPECTIVE_API_KEY>));
proto method submit (|) {*}
multi method submit(Str :$message!, MODEL :#models = TOXICITY) {
my $score = $perspective-api.analyze(:#models, :comment($message));
say #models Z=> $score<attributeScores>{#models}.map: *<summaryScore><value>;
multi method submit(Str :$name!, MODEL :#models = TOXICITY) {
my $score = $perspective-api.analyze(:#models, :comment($name));
say #models Z=> $score<attributeScores>{#models}.map: *<summaryScore><value>;
}
However I always get the following response:
Died because of the exception:
Cannot resolve caller AUTOGEN(Rose::ContentAnalysis::Perspective:D: :message(Str)); none of these signatures match:
(Rose::ContentAnalysis::Perspective: Str :$message!, MODEL :#models = MODEL::TOXICITY, *%_)
(Rose::ContentAnalysis::Perspective: Str :$name!, MODEL :#models = MODEL::TOXICITY, *%_)
Despite my named argument (:message) being a Str as required and #models having a default declared.

Multiple dispatch works in two phases:
Considering the number of positional parameters and their types
If there are any where clauses, named parameters, or sub-signatures, doing a test bind of the signature to see if it would match
The second phase will reject the candidate if it fails to bind for any reason. One such reason, and I believe the cause of the issue here, is that the default value is wrongly typed. For example, in:
multi m(:#x = "not-an-array") { }
m()
We get an error:
Cannot resolve caller m(...); none of these signatures match:
(:#x = "not-an-array")
in block <unit> at -e line 1
But changing it to:
multi m(:#x = ["an-array"]) { }
m()
Works fine. (Note that while a default value uses =, it's actually a binding, not an assignment.)
In the case in the question there's this:
MODEL :#models = TOXICITY
Looking at the module source the code is taken from, I see:
enum MODEL is export (
<TOXICITY SEVERE_TOXICITY TOXICITY_FAST IDENTITY_ATTACK
INSULT PROFANITY SEXUALLY_EXPLICIT THREAT FLIRTATION
ATTACK_ON_AUTHOR ATTACK_ON_COMMENTER INCOHERENT INFLAMMATORY
LIKELY_TO_REJECT OBSCENE SPAM UNSUBSTANTIAL>
);
Thus TOXICITY is just an Int, but what's expected is a typed array of MODEL values.
Thus, if you do this:
multi method submit(Str :$message!, MODEL :#models = Array[MODEL](TOXICITY)) {
It should work.

I see two issues.
One is that you have two methods that are identical except for the name of one named parameter.
Named parameters can have aliases:
# V--------------V
multi method submit(Str :name(:$message)!, MODEL :#models = TOXICITY) {
my $score = $perspective-api.analyze(:#models, :comment($message));
say #models Z=> $score<attributeScores>{#models}.map: *<summaryScore><value>;
}
Note that :$message is really short for :message($message)
Now on the problem which actually prevents your code from working.
#models is a Positional, but you are assigning it a singular value in the signature.
Assign it a Positional, and it works:
(In this case it has to be of type Array[MODEL] because of the MODEL type declaration.)
# V---------------------V
multi method submit(Str :name(:$message)!, MODEL :#models = Array[MODEL](TOXICITY,)) {
my $score = $perspective-api.analyze(:#models, :comment($message));
say #models Z=> $score<attributeScores>{#models}.map: *<summaryScore><value>;
}

Related

Possible to create class aliases dynamically?

Got this class:
class Mass-lb is Mass {
method new(Rat:D() $value = 1.0) {
self.bless(
:abbr('lb'),
:base_value(453.59237),
:$value,
);
}
}
I have created aliases like this:
class Mass-lbs is Mass-lb { }
class Mass-pound is Mass-lb { }
class Mass-pounds is Mass-lb { }
class Mass-pnds is Mass-lb { }
But I'd prefer to do something like this:
my #lb-syn = < lbs pounds pound pnds >;
for #lb-syn {
EVAL 'class ::("Mass-$_") is Mass-lb {}';
}
This throws an error:
Name ::("Mass-$_") is not compile-time known, and can not serve as a package name
PHP has a built-in for creating aliases: https://www.php.net/manual/en/function.class-alias.php
I couldn't find anything similar for raku.
In RakuAST there's a class that you can call to create a new type. But that the RakuAST branch hasn't landed yet.
Until then, your approach using EVAL is valid, you just need to make it a bit simpler:
class Mass-lb { }
BEGIN "constant Mass-$_ = Mass-lb".EVAL
for <lbs pounds pound pnds>;
my $mlb = Mass-lbs.new;
Make sure the aliases are created at BEGIN time.
No need to subclass, you can use a constant for aliasing.
Since constants are our by default, they're visible outside of the EVAL.
Alternatively, you could use raku Physics::Unit and Physics::Measure...
use Physics::Unit;
use Physics::Measure :ALL;
# define a new custom Unit
Unit.new( defn => 'lbm', names => <Mass-lb Mass-lbs Mass-pound Mass-pounds Mass-pnds> );
say GetUnit('Mass-lbs').names; #[Mass-lb Mass-lbs Mass-pound Mass-pounds Mass-pnds]
# use the Unit in a Measure
my $mass = ♎️'42 Mass-pnds';
say $mass; #42Mass-lb
say $mass.^name; #(..Mass) ...class name
# convert to another Unit
my $kgm = $mass.in: 'kg';
say $kgm; #19.05087954kg
# convert back
say $kgm.in: 'Mass-pound'; #42Mass-lb
# raku Rats mean that the back conversion is identical
say $kgm cmp $mass; #Same
# with % or abs Error
my $mass2 = ♎️'42 Mass-pnds ±3%';
say $mass2; #42Mass-lb ±1.26
say $mass2.in: 'kg'; #19.05087954kg ±0.5715263862
More info at github Physics::Unit and Physics::Measure...

What runtime code can I write to extract the type notations as defined by Sorbet, to display to the user?

Is there a way to inspect the type notations in ruby/sorbet?
I actually want to display those values in a form somewhere and instead of hardcoding in values like "this can be nil or a String", I want to instead use the code I've defined by the sig { ... }.
Example pseudo-code:
class User
extend T::Sig
sig { returns(T.nilable(String)) }
def foo
...
end
end
Sorbet.return_type(User, :instance_method, :foo) # returns '[nil, String]' or similar
I believe this is what I am looking for:
T::Utils.signature_for_method
In action:
T::Utils.signature_for_method(User.new.method(:daily?)).return_type
=> #<T::Types::Union:0x0000000132198da8
#types=[#<T::Types::Simple:0x0000000132198f60 #name="TrueClass", #raw_type=TrueClass>, #<T::Types::Simple:0x0000000132198e98 #name="FalseClass", #raw_type=FalseClass>]>

Get hash keys or type of value in a hash

Coming from flowtype, I'm used to having things like $Keys<obj> to say 'the union of all the keys in obj'. Also, $PropertyType<obj, 'key'> to get the type of the value of obj[key].
I'm looking for something similar in Sorbet so I can make better use of generics.
Ideally I want the key param to be T.any(<keys in the hash>) and the return value to be typeof hash[:key]
This is what I'd like to happen:
# my_rbi.rbi
class SomeClass
Elem = type_member
sig { params(key: Keys[Elem]).returns(PropertyType<Elem, key>) }
def [](key); end
end
# somewhere else
sig { returns(SomeClass[{ my_key: String }]) } # add the hash as a generic
def context
# return the context
end
context[:my_key] # works, is a String!
context[:not_my_key] # fails
Is any of this possible? what are my options here?
Context:
My use case is types for graphql-ruby, in particular GraphQL::Query::Context. It essentially delegates [] to an internal hash, which I know the shape of. I'd like to pass a generic to GraphQL::Query::Context, like GraphQL::Query::Context[{ my_key: String }]. Then, in the def [] signature, I'd like to say what the available options for key are based on the generic hash.

How to correct filter fields domain in a model?

I'm getting the following error trying to filter a field by another field value of the same model.
File
"/opt/..../tfutbol/models/partido.py",
line 29, in Partido
figura = fields.Many2one('tfutbol.jugador',domain=[('equipo_id','=',local.id)])
RuntimeError: maximum recursion depth exceeded while calling a Python
object
The line of code trying with the problem is:
figura = fields.Many2one('tfutbol.jugador',domain=[('equipo_id','=',local.id),('equipo_id','=',visitante.id)])
All the relevant code, is above:
class Partido(models.Model):
_name = 'tfutbol.partido'
local = fields.Many2one('tfutbol.equipo')
visitante = fields.Many2one('tfutbol.equipo')
figura = fields.Many2one('tfutbol.jugador',domain=[('equipo_id','=',local.id),('equipo_id','=',visitante.id)])
class Equipo(models.Model):
_name = 'tfutbol.equipo'
name = fields.Char('Nombre')
jugador_ids = fields.One2many('tfutbol.jugador', 'equipo_id', string="Jugadores")
class Jugador(models.Model):
_name = 'tfutbol.jugador'
name = fields.Char('Nombre')
equipo_id = fields.Many2one('tfutbol.equipo')
Thanks for reading!
If you read the docstring on /odoo/fields.py on the Many2one class definition you will see that:
:param domain: an optional domain to set on candidate values on the
client side (domain or string)
That means that you cannot use the dot notation (record.field) to pull in values because this has not been implemented on javascript.
So what you can do to dynamically create a domain like the one you want is:
Create an onchange method that will be invoked every time you set a value on the local and visitante fields and will change the domain on the figura field. For example:
#api.onchange('figura','visitante')
def onchange_method(self):
domain = {}
domain['figura'] = [('equipo_id','=',self.local.id),('equipo_id','=',self.visitante.id)]
return {'domain': domain}
Note: When you do not set an operator in a domain with multiple leafs (parameters) an explicit AND is added, so in your domain you are searching for the equipo_id filed to be equal to self.local.id AND self.visitante.id which will not work, you might want to add an OR like:
['|',('equipo_id','=',self.local.id),('equipo_id','=',self.visitante.id)]

Why does this documentation example fail? Is my workaround an acceptable equivalent?

While exploring the documented example raised in this perl6 question that was asked here recently, I found that the final implementation option - (my interpretation of the example is that it provides three different ways to do something) - doesn't work. Running this;
class HTTP::Header does Associative {
has %!fields handles <iterator list kv keys values>;
sub normalize-key ($key) { $key.subst(/\w+/, *.tc, :g) }
method EXISTS-KEY ($key) { %!fields{normalize-key $key}:exists }
method DELETE-KEY ($key) { %!fields{normalize-key $key}:delete }
method push (*#_) { %!fields.push: #_ }
multi method AT-KEY (::?CLASS:D: $key) is rw {
my $element := %!fields{normalize-key $key};
Proxy.new(
FETCH => method () { $element },
STORE => method ($value) {
$element = do given $value».split(/',' \s+/).flat {
when 1 { .[0] } # a single value is stored as a string
default { .Array } # multiple values are stored as an array
}
}
);
}
}
my $header = HTTP::Header.new;
say $header.WHAT; #-> (Header)
$header<Accept> = "text/plain";
$header{'Accept-' X~ <Charset Encoding Language>} = <utf-8 gzip en>;
$header.push('Accept-Language' => "fr"); # like .push on a Hash
say $header<Accept-Language>.perl; #-> $["en", "fr"]
... produces the expected output. Note that the third last line with the X meta-operator assigns a literal list (built with angle brackets) to a hash slice (given a flexible definition of "hash"). My understanding is this results in three seperate calls to method AT-KEY each with a single string argument (apart from self) and therefore does not exersise the default clause of the given statement. Is that correct?
When I invent a use case that excersises that part of the code, it appears to fail;
... as above ...
$header<Accept> = "text/plain";
$header{'Accept-' X~ <Charset Encoding Language>} = <utf-8 gzip en>;
$header{'Accept-Language'} = "en, fr, cz";
say $header<Accept-Language>.perl; #-> ["en", "fr", "cz"] ??
# outputs
(Header)
This Seq has already been iterated, and its values consumed
(you might solve this by adding .cache on usages of the Seq, or
by assigning the Seq into an array)
in block at ./hhorig.pl line 20
in method <anon> at ./hhorig.pl line 18
in block <unit> at ./hhorig.pl line 32
The error message provides an awesome explanation - the topic is a sequence produced by the split and is now spent and hence can't be referenced in the when and/or default clauses.
Have I correctly "lifted" and implemented the example? Is my invented use case of several language codes in the one string wrong or is the example code wrong/out-of-date? I say out-of-date as my recollection is that Seq's came along pretty late in the perl6 development process - so perhaps, this code used to work but doesn't now. Can anyone clarify/confirm?
Finally, taking the error message into account, the following code appears to solve the problem;
... as above ...
STORE => method ($value) {
my #values = $value».split(/',' \s+/) ;
$element = do given #values.flat {
when 1 { $value } # a single value is stored as a string
default { #values } # multiple values are stored as an array
}
}
... but is it an exact equivalent?
That code works now (Rakudo 2018.04) and prints
$["en", "fr", "cz"]
as intended. It was probably a bug which was eventually solved.