How to know if returning an l-value when using `FALLBACK`?

This feels a bit like a X-Y question, so let's simplify the example, and see if that answers helps in your decisions.

First of all: if you return the "value" of a non-existing key in a hash, you are in fact returning a container that will auto-vivify the key in the hash when assigned to:

my %hash;
sub get($key) { return-rw %hash{$key} }
get("foo") = 42;
dd %hash;   # Hash %hash = {:foo(42)}

Please note that you need to use return-rw here to ensure the actual container is returned, rather than just the value in the container. Alternately, you can use the is raw trait, which allows you to just set the last value:

my %hash;
sub get($key) is raw { %hash{$key} }
get("foo") = 42;
dd %hash;   # Hash %hash = {:foo(42)}

Note that you should not use return in that case, as that will still de-containerize again.

To get back to your question:

I want to track if I've actually modified %!attrs or have only just read the value when FALLBACK was called.

class Foo {
    has %!attrs;
    has %!unexpected;

    method TWEAK() { %!attrs<bar> = 'bar' }

    method FALLBACK(Str:D $name, *@rest) is raw {
        if %!attrs{$name}:exists {
            %!attrs{$name}
        }
        else {
            %!unexpected{$name}++;
            Any
        }
    }
}

This would either return the container found in the hash, or record the access to the unknown key and return an immutable Any.

Regarding plan B, recording changes: for that you could use a Proxy object for that.

Hope this helps in your quest.


Liz's answer is full of useful info and you've accepted it but I thought the following might still be of interest.

How to know if returning an l-value ... ?

Let's start by ignoring the FALLBACK clause.

You would have to test the value. To deal with Scalars, you must test the .VAR of the value. (For non-Scalar values the .VAR acts like a "no op".) I think (but don't quote me) that Scalar|Array|Hash covers all the l-value super-types:

my \value       = 42;  # Int is an l-value is False
my \l-value-one = $;   # Scalar is an l-value is True
my \l-value-too = @;   # Array is an l-value is True

say "{.VAR.^name} is an l-value is {.VAR ~~ Scalar|Array|Hash}"
  for value, l-value-one, l-value-too

How to know if returning an l-value when using FALLBACK?

Adding "when using FALLBACK" makes no difference to the answer.

How can I know if I actually need to return an l-value ... ?

Again, let's start by ignoring the FALLBACK clause.

This is a completely different question than "How to know if returning an l-value ... ?". I think it's the core of your question.

Afaik, the answer is, you need to anticipate how the returned value will be used. If there's any chance it'll be used as an l-value, and you want that usage to work, then you need to return an l-value. The language/compiler can't (or at least doesn't) help you make that decision.

Consider some related scenarios:

my $baz := foo.bar;
... (100s of lines of code) ...
$baz = 42;

Unless the first line returns an l-value, the second line will fail.

But the situation is actually much more immediate than that:

routine-foo = 42;

routine-foo is evaluated first, in its entirety, before the lhs = rhs expression is evaluated.

Unless the compiler's resolution of the routine-foo call somehow incorporated the fact that the very next thing to happen would be that the lhs will be assigned to, then there would be no way for a singly or multiply dispatched routine-foo to know whether it can safely return an r-value or must return an l-value.

And the compiler's resolution does not incorporate that. Thus, for example:

multi term:<bar> is rw { ... }
multi term:<bar>       { ... }
bar = 99; # Ambiguous call to 'term:<bar>(...)'

I can imagine this one day (N years from now) being solved by a combination of allowing = to be an overloadable operator, robust macros that allow overloading of = being available, and routine resolution being modified so the above ambiguous call could do something equivalent to resolving to the is rw multi. But I doubt it will actually come to pass even with N=10. Perhaps there is another way but I can't think of one at the moment.

How can I know if I actually need to return an l-value when using FALLBACK?

Again, adding "when using FALLBACK" makes no difference to the answer.

I want to track if I've actually modified %!attrs or have only just read the value when FALLBACK was called.

When FALLBACK is called it doesn't know what context it's being called in -- r-value or l-value. Any modification comes after it has already returned.

In other words, whatever solution you come up with will being nothing to do per se with FALLBACK (even if you have to use it to implement some other aspect of whatever it is you're trying to do).

(Even if it were, I suspect trying to solve it via FALLBACK itself would just make matters worse. One can imagine writing two FALLBACK multis, one with an is rw trait, but, as explained above, my imagination doesn't stretch to that making any difference any time soon, if ever, and could only happen if the above imaginary things happened (the macros etc.) and the compiler was also modified to pay attention to the two FALLBACK multi variants, and I'm not at all meaning to suggest that that even makes sense.)

Plan B

Or (alternate plan B) can I attach a callback or something similar to my %!attrs to monitor for changes?

As Lizmat notes, that's the realm of Proxys. And thus your next SO question... :)