rakudo / rakudo

🦋 Rakudo – Raku on MoarVM, JVM, and JS
https://rakudo.org/
Artistic License 2.0
1.73k stars 375 forks source link

New postfix call operator that calls method only if the invocant is defined #1732

Open jkramer opened 6 years ago

jkramer commented 6 years ago

Proposal for a new operator similar to .?, .* and so on. It should behave like .? but instead of checking if the method exists or not, it should check if the invocant is defined:

my Str $s;
say $s./trim; # => Nil
$s = ' foo ';
say $s./trim; # => foo

(./ or even .// because it's remotely related to the defined-or operator //, but that's just an idea).

PS: I don't know if this is the right place for discussing new language features, I thought I'd just try and maybe someone can point me to the correct place if this doesn't belong here.

lizmat commented 6 years ago

FWIW, I find the current semantics of .? less than useful and also would like to see a way to only call a method if the invocant is defined.

smls commented 6 years ago

I feel the same as lizmat; in particular, the fact that several methods exists on undefined invocants too, sometime or even on Any, just to print a warning, makes the current behavior much less useful in practice:

➜  say Any.?match(/a/);
Use of uninitialized value of type Any in string context.
Methods .^name, .perl, .gist, or .say can be used to stringify it to something meaningful.
   block <unit> at <unknown file> line 1

Nil
> say Str.?uc;
Use of uninitialized value of type Str in string context.
Methods .^name, .perl, .gist, or .say can be used to stringify it to something meaningful.
  in block <unit> at <unknown file> line 1
zoffixznet commented 6 years ago

my Str $s; say $s./trim; # => Nil

What's the real-life usecase for this? Both how would you end up with an undefined object in the first place and why would returning a Nil be useful behaviour. Also, what's the behaviour if the method doesn't exist on the object; a throw?

We already have two ways to achieve similar behaviour:

my Str $s;
$s andthen .trim.say;
.trim.say with $s;

So so far I'm voting 👎 YAGNI

zoffixznet commented 6 years ago

saw in the #perl6 logs where this idea was incepted. The original syntax you used works fine if you put the whole colonpair behind a with, not just the value:

<Zoffix__> m: m: my $x; my Str %h = :foo('lol'), |(:bar(.trim) with $x); dd %h
<camelia> rakudo-moar 5ef056122: OUTPUT: «Hash[Str] %h = (my Str % = :foo("lol"))␤»
jkramer commented 6 years ago

It would really just be convenient syntactic sugar. Here's one use-case:

class Foo {
  has Str $.a-string;
}

my Str $some-input;

my $foo = Foo.new(:a-string($some-input./trim));

Both Foo.new(:a-string($some-input andthen .trim)) and Foo.new(:a-string(.trim with $some-input)) won't work because they return Empty (Slip), so the type check against Str fails.

EDIT: Your last example seems to work though.

smls commented 6 years ago

What's the real-life usecase for this? Both how would you end up with an undefined object in the first place

Lots of ways!

Say you're looking up a hash entry that may not exist:

sub negate-verb(Str $verb --> Str) {
    state Str %opposite = 'GOOD' => 'BAD', #`[more loaded from database here];

    return %opposite{$verb.uc}.?lc // "not $verb";
}

say negate-verb 'good';  # bad
say negate-verb 'big';   # not big

Or you're doing something with an object attribute that the users of the class may leave uninitialized:

class Person {
    #| Name of the person, or (Str) if unknown
    has Str $.name;

    #| Initials of the person, or (Str) if unknown
    has Str $.initials = $!name.?comb(/«./).?join;
}

my $p1 = Person.new(name => "John Fitzgerald Kennedy");
my $p2 = Person.new();

say $p1.initials;  # JFK
say $p2.initials;  # (Str)

In a language where variables, attributes, and data structure entries default to type objects, and the default type constraints for parameters and even method invocants allow both defined and undefined values to be passed in from down-stream code, there are really lots of situations where you can end up with a value that is usually .defined but sometimes not.

and why would returning a Nil be useful behaviour.

Nil is automatically cast to the type object of whatever type it needs to be, when assigned to a variable or similar.

So whatever type the method would have returned in the "defined invocant" case, the Nil return value for the "undefined invocant" case is compatible with it.

zoffixznet commented 6 years ago

Lots of ways!

I think this is going backwards about it: you thought up a feature and now making up examples that might use it. Rather than having a bunch of real world code and going: "man, this code would be a lot simpler if this feature existed."

When adding a new feature, you shouldn't think of all the ways to use it. Rather think of all the ways to avoid its use, to find cases that actually require it.


Say you're looking up a hash entry that may not exist:

So just use

    return .lc with %opposite{$verb.uc};
    "not $verb";

or

    return %opposite{$verb.uc} andthen .lc orelse "not $verb";

Or you're doing something with an object attribute that the users

This example makes no sense to me: why would I want an undefined $.name or undefined $.initials? But regardless, ($!name andthen .comb(/«./).join orelse Nil) is only 20 characters longer.


It's also worth noting we currently have mirroring consistency between methodops and regexes:

The ./ /.// breaks it (and also looks awful).

Also, don't we already have like 60-80 various methodcalls? Adding 6 more to that list just to save 20 characters of typing makes little sense to me.

The LTAness of .? was mentioned above, and I can agree with it, but I don't see ./ as a solution to it, because (a) it (as currently proposed) doesn't actually check if the method can be called on the object and (b) that the method can be called with the given args.

I'd normally suggest making a module first, but in this case, I don't think even that is worth the time. We already have plenty of options to deal with definedness of objects.

vendethiel commented 6 years ago

I think this is going backwards about it: you thought up a feature and now making up examples that might use it. Rather than having a bunch of real world code and going: "man, this code would be a lot simpler if this feature existed."

I disagree. The operator proposed here exists in Groovy, exists also in C# and CoffeeScript (is currently a stage 1 proposal for JS and in PHP as well IIRC). Ruby does it with .tap, which is more similar to andthen. The current .?, .+, .* we have are not features that exist in any other languages that I'm aware of (except maybe for Common Lisp's method combinators).

It's useful in plenty of circumstances:

method scope-lookup($name) {
  %.v{$name} // $.parent.??lookup($name);
}

my $name = get-logged-in-user().??profile??.username // "Anonymous";

$node.as-a-thing.??process;

The ./ /.// breaks it (and also looks awful).

Solid +1 to both points you have here. Though it mirrors our coalescing operator //...

FCO commented 6 years ago

It would be great if it short-circuits, as Swift’s and kotlin’s ‘?.’, where this:

$a.??uc.subst(“a”, “i”).say;

Would print “BLI” if $a == “bla” and would do nothing if its not defined... Em sex, 13 de abr de 2018 às 16:33, ven notifications@github.com escreveu:

I think this is going backwards about it: you thought up a feature and now making up examples that might use it. Rather than having a bunch of real world code and going: "man, this code would be a lot simpler if this feature existed."

I disagree. The operator proposed here exists in Groovy, exists also in C# and CoffeeScript (is currently a stage 1 proposal for JS https://github.com/tc39/proposal-optional-chaining and in PHP as well IIRC). Ruby does it with .tap, which is more similar to andthen. The current .?, .+, .* we have are not features that exist in any other languages that I'm aware of (except maybe for Common Lisp's method combinators).

It's useful in plenty of circumstances:

method scope-lookup($name) { %.v{$name} // $.parent.??lookup($name); } my $name = get-logged-in-user().??profile??.username // "Anonymous"; $node.as-a-thing.??process;

The ./ /.// breaks it (and also looks awful).

Solid +1 to both points you have here. Though it mirrors our coalescing operator //...

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/rakudo/rakudo/issues/1732#issuecomment-381239917, or mute the thread https://github.com/notifications/unsubscribe-auth/AAGF-mHLdb7frK2VHaST1IEOwSWJXQi4ks5toP2RgaJpZM4TTad0 .

FCO commented 6 years ago

Im not sure if I agree with myself…

$ perl6 -e '

my $a; $a = "bla";

$a andthen .uc.subst("A", "I").say

' BLI

Em 13 de abr de 2018, à(s) 22:27, Fernando Oliveira fernandocorrea@gmail.com escreveu:

It would be great if it short-circuits, as Swift’s and kotlin’s ‘?.’, where this:

$a.??uc.subst(“a”, “i”).say;

Would print “BLI” if $a == “bla” and would do nothing if its not defined... Em sex, 13 de abr de 2018 às 16:33, ven <notifications@github.com mailto:notifications@github.com> escreveu: I think this is going backwards about it: you thought up a feature and now making up examples that might use it. Rather than having a bunch of real world code and going: "man, this code would be a lot simpler if this feature existed."

I disagree. The operator proposed here exists in Groovy, exists also in C# and CoffeeScript (is currently a stage 1 proposal for JS https://github.com/tc39/proposal-optional-chaining and in PHP as well IIRC). Ruby does it with .tap, which is more similar to andthen. The current .?, .+, .* we have are not features that exist in any other languages that I'm aware of (except maybe for Common Lisp's method combinators).

It's useful in plenty of circumstances:

method scope-lookup($name) { %.v{$name} // $.parent.??lookup($name); }

my $name = get-logged-in-user().??profile??.username // "Anonymous";

$node.as-a-thing.??process; The ./ /.// breaks it (and also looks awful).

Solid +1 to both points you have here. Though it mirrors our coalescing operator //...

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/rakudo/rakudo/issues/1732#issuecomment-381239917, or mute the thread https://github.com/notifications/unsubscribe-auth/AAGF-mHLdb7frK2VHaST1IEOwSWJXQi4ks5toP2RgaJpZM4TTad0.