Defined vs. exists with Perl6 hash keys

if defined %hash{'key'} {
} else {
   %hash{'key'} = 1;

Use the defined routine or method. See 5to6-perlfunc -- defined

defined is a property of a value.

exists is a variation selector of the hash indexing operation.

This is true in both Perl5 and Perl6.
It's just that they way they handle it is different.

Say you have a hash like this:

my %h = ( a => 1, b => 2, c => Int );

It has three keys:

say %h.keys; # (a b c)

You can get the value associated with a key:

my $value = %h{'a'};

You can ask the value if it was defined

say defined $value; # True

Now let's try it with c:

my $value = %h{'c'};
say defined $value; # False

And with D:

my $value = %h{'D'};
say defined $value; # False

Note that even though D isn't a key in the hash, the value you get back looks the same as as looking up c.

The value doesn't know how you got ahold of it. It has no clue about the hash indexing operation.
It is the same even if you don't store it in a variable.

say defined %h{'c'}; # False
say defined %h{'D'}; # False

So we have to tell the indexing operation instead to give us that information.

my $exists = %h{'D'}:exists;
say $exists; # False

Also if you use the result of %h{'D'} as a container, it [magically] starts to exist.

say %h{'D'}:exists; # False

say %h{'D'}.perl;   # Any
say %h{'D'}:exists; # False

say %h{'D'}:exists; # True

say %h{'D'}; # 1

So then why is it different than Perl5?

In Perl5 exists and defined are keywords. In Perl6 they are not.
They are not special in any way, shape, or form in Perl6.

Here is the Perl5 version for checking exists:

use v5.12.0;
use warnings;

my %h = ( a => 1, b => 2, c => undef );

my $exists = exists $h{D};
say $exists ? 'True' : 'False'; # False

If you call the compiler with -MO=Concise you will get a listing of the opcodes that it would execute.

Here is just the part that shows that exists is an opcode:

d     <;> nextstate(main 5 -e:1) v:%,us,*,&,{,$,fea=2 ->e

g     <2> sassign vKS/2 ->h                          # <----- =

-        <1> ex-exists sK/1 ->f                      # <-\
e           <+> multideref($h{"D"}) sK/EXISTS ->f    # <--+-- exists $h{D}
-              <0> ex-padhv sR ->e                   # <-/

f        <0> padsv[$exists:5,6] sRM*/LVINTRO ->g     # <----- $exists

h     <;> nextstate(main 6 -e:1) v:%,us,*,&,{,$,fea=2 ->i

The multideref($h{"D"}) is marked with EXISTS, and there is ex-exists opcode.

The listing for defined is very similar.

One of the design goals of Perl6 was to have as few special cases as possible. This is why neither exists or defined are keywords.

If you look for where defined is in the Rakudo codebase you find this:

proto sub defined(Mu, *%) is pure {*}
multi sub defined(Mu \x) { x.defined }

defined is just a subroutine that takes one value, and calls the .defined method on that value.

Since it does a method call, the value gets a say on whether it is considered to be defined or not.

The default that all values inherit is in Mu.defined source:

proto method defined(|) {*}
multi method defined(Mu:U: --> False) { }
multi method defined(Mu:D: --> True)  { }

So the default is for the type object to be undefined, and an instance to be defined.

One notable exception is the Failure object:

multi method defined(Failure:D: --> False) { $!handled = 1 }

So that makes instances of a Failure object to be seen as not defined. It will also cause the Failure to consider itself handled.
(If a Failure isn't handled it will throw a warning when it gets garbage collected.)

So what about :exists on %h{'D'}?
In Perl6 the “normal” operators are defined as subroutines with a special name.

sub foo (%hash, $key){}

say &foo.signature.perl;
# :(%hash, $key)

say &postcircumfix:<{ }>.signature.perl;
# :($, $?, Mu $?, *%)

Note that the postcircumfix operator { } is backed by several multi-subs, but we really only need to look at one of them.

multi sub postcircumfix:<{ }>( \SELF, \key, Bool() :$exists!, *%other ) is raw {
    SLICE_ONE_HASH( SELF, key, 'exists', $exists, %other )

You can ignore most of that. The key thing to pay attention to is :$exists!.
By default named arguments are optional, and don't get a say in which multi gets selected. In order for :exists to force this one to be selected it must be marked as required !.

Whenever you see :exists you can think of it as short for :exists(True) and exists => True.
(Also :$exists is short for :exists($exists) and exists => $exists.)

So both of these lines are functionally identical:

my $exists = %h{'D'}:exists;

my $exists = postcircumfix:<{ }> %h, 'D', exists => True;

It tells the operator (subroutine) to use the exists variation.

Note that it also passes along the value of $exists, which means it can do inverted exists checks.

say %h{'c'}:exists;  # True
say %h{'c'}:!exists; # False

say %h{'D'}:exists;  # False
say %h{'D'}:!exists; # True

:!exists is short for :exists(False) and exists => False.

So the reason there isn't :defined variation of postcircumfix { } is that there doesn't need to be one. Just ask the resulting value if it was defined.

The exact code you've got can be done much simpler in Perl 6, because of autovivification:

my %foo;
say %foo.perl; # => {:thekey(1)}

Other than that, the difference between defined %foo<thekey> and %foo<thekey>:exists is that you can put an undefined value into the hash at the given key and it'll answer True to :exists but False to defined.