11

If I have a Role R defined as:

role R { method answer { 42 } }

What is the difference (if any) between these two lines:

my $a = 'question' does R;
my $b = 'question' but  R;

They appear very similar:

say $a.answer;  # OUTPUT: «42»
say $b.answer;  # OUTPUT: «42»

say $a.WHAT;    # OUTPUT: «(Str+{R})»
say $b.WHAT;    # OUTPUT: «(Str+{R})»

Is this a case of there being More Than One Way To Do It™, and these both mean the same thing? Or is there a subtle difference that I'm missing?

note:
I understand that does is both an operator and a trait and thus can be used when for compile-time mixins (e.g., class C does R {}) whereas but is only for runtime mixins. I also understand that but can be used with an object (e.g., my $c = 'question' but False) whereas does can only be used with a Role. I'm not asking about either of those differences; my only question is about whether there's a difference when both are used at runtime with a Role. I have read the documentation section on mixing in Role, but didn't see an answer.

codesections
  • 8,900
  • 16
  • 50

1 Answers1

11

Put simply:

  • does modifies an object in place (and should be used with caution with value types, see note below)

  • but returns a new object.

When created off of a literal, it's probably not as evident, but when used with another object, it's pretty clear I think:

role R { method answer { 42 } }

my $question = 'question';

my $but  = $question but  R;
my $does = $question does R;

say $question.WHAT;   # (Str+{R})
say $but.WHAT;        # (Str+{R})
say $does.WHAT;       # (Str+{R})

say $question.WHERE;  # 129371492039210
say $but.WHERE;       # 913912490323923
say $does.WHERE;      # 129371492039210 <-- same as $question's

Notice I cheated a bit and swapped the order of does and but. If I had preserved the order you had, the does would modify $question in place, applying the role, meaning that but would clone $question (with its role) and apply the role (again!):

my $does = $question does R;
my $but  = $question but  R;

say $does.WHAT;  # (Str+{R})
say $but.WHAT;   # (Str+{R}+{R})

This is because does as an operator is conceptually akin to ++ or +=, that is, designed to be used in a standalone context, for instance

my $foo = …;
given $bar {
   when 'a' { $foo does A }
   when 'b' { $foo does B }
   when 'c' { $foo does B }
}

Using but is conceptually closer to using $foo + 1 — mostly meaningless unless assigned to or passed to something else.

A warning for does and value types

If you use does on a value type (strings, numbers mainly), there is an extremely high likelihood that you will cause unintended side effects. This is because value types (which, e.g., strings are) are supposed to be immutable and substitutable for one other. Note the following:

role Fooish { }

my $foo = 'foo';
$foo does Fooish;

say 'foo'.WHAT; # (Str+{Fooish})

This is a substitution that's happening at compile time (so it won't affect, e.g, 'foobar'.substr(0,3), that happens at runtime), but can cause some truly weird effects if you toss them in a loop:

role Fooish { }

my @a;
@a.push('foo' does Fooish) for ^10;

say @a[0].WHAT; # (Str+{Fooish}+{Fooish}+{Fooish}+{Fooish}+{Fooish}
                      +{Fooish}+{Fooish}+{Fooish}+{Fooish}+{Fooish})

Applying multiple rolls takes longer and longer the more you do it, so if you change that to ^100000, be ready to wait a while. OTOH, doing but gives you nice constant time and doesn't pollute the literal. This behavior seems, AFAICT, to be perfectly valid, but definitely something that can catch you unexpectedly.

user0721090601
  • 5,276
  • 24
  • 41
  • Thanks! That is a big (and surprising!) difference. In particular, I would **not** have expected `$question.answer == 42` just because I did `my $does = $question does R`. I can see now why that happens, but I'm glad I learned that here rather than when debugging! – codesections Feb 28 '21 at 17:35
  • In fact, that strikes me as surprising enough that it might be worth adding that explicitly to your answer. It's clear from what you wrote, but it's also something someone could miss if skimming. – codesections Feb 28 '21 at 17:37
  • 2
    @codesections yeah, the idea was that you could have a line, all by itself, that says `$foo does Bar` to apply the role, rather than needing to say `$foo = $foo but Bar`. I guess it's *roughly* to using `++$foo` versus `$foo + 1`. – user0721090601 Feb 28 '21 at 17:43
  • Also, FWIW, I used to always use `but` for runtime stuff. Once I realized the difference, I started using `does` (I should update my Advent post on `DateTime::Timezones` to reflect that) – user0721090601 Feb 28 '21 at 17:48
  • @user0721090601 "If you use `does` on a value type (strings, numbers mainly), there is an extremely high likelihood that you will cause unintended side effects." So why isn't this forbidden? – jubilatious1 Mar 03 '21 at 02:06
  • 1
    @jubilatious1 It has recently been made forbidden. – Brad Gilbert Mar 04 '21 at 14:48