The associative role and associative classes§
The Associative
role underlies hashes and maps, as well as other classes such as MixHash
. It defines the two types that will be used in associative classes; by default, you can use anything (literally, since any class that subclasses Any
can be used) as a key, although it will be coerced to a string, and any object as value. You can access these types using the of
and keyof
methods.
By default, any object declared with the %
sigil will get the Associative role, and will by default behave like a hash, but this role will only provide the two methods above, as well as the default Hash behavior.
say (%).^name ; # OUTPUT: «Hash»
Inversely, you cannot use the %
sigil if the Associative
role is not mixed in, but since this role does not have any associated properties, you will have to redefine the behavior of the hash subscript operator. In order to do that, there are several functions you will have to override:
does Associative[Cool,DateTime]say Logger.of; # OUTPUT: «(Cool)»my := Logger.new;say .of; # OUTPUT: «(Cool)».log( "Stuff" );.log( "More stuff");say <2018-05-26>; # OUTPUT: «(More stuff Stuff)»say <2018-04-22>:exists; # OUTPUT: «False»
In this case, we are defining a logger with Associative semantics that will be able to use dates (or a part of them) as keys. Since we are parameterizing Associative
to those particular classes, of
will return the value type we have used, Cool
in this case (we can log lists or strings only). Mixing the Associative
role gives it the right to use the %
sigil; binding is needed in the definition since %
-sigiled variables get by default the Hash
type.
This log is going to be append-only, which is why we escape the associative array metaphor to use a log
method to add new events to the log. Once they have been added, however, we can retrieve them by date or check if they exist. For the first we have to override the AT-KEY
multi method, for the latter EXIST-KEY
. In the last two statements, we show how the subscript operation invokes AT-KEY
, while the :exists
adverb invokes EXISTS-KEY
.
We override DELETE-KEY
, ASSIGN-KEY
and BIND-KEY
, but only to throw an exception. Attempting to assign, delete, or bind a value to a key will result in a Cannot modify an immutable Str (value)
exception thrown.
Making classes associative provides a very convenient way of using and working with them using hashes; an example can be seen in Cro, which uses it extensively for the convenience of using hashes to define structured requests and express its response.
Mutable hashes and immutable maps§
A Hash
is a mutable mapping from keys to values (called dictionary, hash table or map in other programming languages). The values are all scalar containers, which means you can assign to them. Map
s are, on the other hand, immutable. Once a key has been paired with a value, this pairing cannot be changed.
Maps and hashes are usually stored in variables with the percent %
sigil, which is used to indicate they are Associative.
Hash and map elements are accessed by key via the { }
postcircumfix operator:
say .raku;# OUTPUT: «("/home/camelia", "/usr/bin:/sbin:/bin")»
The general Subscript rules apply providing shortcuts for lists of literal strings, with and without interpolation.
my = oranges => 'round', bananas => 'bendy';say <oranges bananas>;# OUTPUT: «(round bendy)»my = 'bananas';say «oranges "$fruit"»;# OUTPUT: «(round bendy)»
You can add new pairs simply by assigning to an unused key:
my ;= 'new value';
Hash assignment§
Assigning a list of elements to a hash variable first empties the variable, and then iterates the elements of the right-hand side (which must contain an even number of elements). Note that hash keys are always coerced to be strings even if they are unquoted, but keys with spaces in their names must be quoted. For example, using the common Pair
syntax:
my = 1 => 'a', b => 2, '1 2' => 3;say .keys.sort.raku; # OUTPUT: «("1", "1 2", "b").Seq»say .values.sort.raku # OUTPUT: «(2, 3, "a").Seq»
The stringification of the keys can lead to surprising results. For example, the Raku module CSV::Parser
in one mode returns a CSV data line as a hash of {UInt => 'value'}
pairs which can look like this fragment of a 20-field line:
my = '2' => 'a', '10' => 'b';say .keys.sort.raku; # OUTPUT: «("10", "2").Seq»
The sort result is not what one would normally want. We can use the power of Raku to coerce the string values into integers for the sort. There are several ways that can be done but this method may present the least "line noise" to novices or non-Raku viewers:
say .keys.map(+*).sort.raku; # OUTPUT: «(2, 10).Seq»
In a hash constructor list the Pair
syntax doesn't have to be used, or it may be intermixed with ordinary list values as long as the list has an even number of elements in total as well as between Pair
s. If an element is a Pair
type (e.g., 'a => 1'), its key is taken as a new hash key, and its value as the new hash value for that key. Otherwise the value is coerced to Str
and used as a hash key, while the next element of the list is taken as the corresponding value.
my = 'a', 'b', c => 'd', 'e', 'f';
Same as
my = a => 'b', c => 'd', e => 'f';
or
my = <a b c d e f>;
or even
my = %( a => 'b', c => 'd', e => 'f' );
If you have an odd number of elements using most constructors you will see an error:
my = <a b c>; # OUTPUT: «hash initializer expected...»
There are two other valid ways of constructing a hash, but the user should be wary:
my = [ a => 'b', c => 'd', e => 'f' ]; # This format is NOT recommended.# It cannot be a constant and there# will be problems with nested hashes
or
my = ;
Please note that curly braces are used only in the case that we are not assigning it to a %
-sigiled variable; in case we use it for a %
-sigiled variable we will get an Potential difficulties: Useless use of hash composer on right side of hash assignment; did you mean := instead?
error. As this error indicates, however, we can use curly braces as long as we use also binding:
my := ;say ; # OUTPUT: «{a => b, c => d, e => f}»
Nested hashes can also be defined using the same syntax:
my = e => f => 'g';say <e><f>; # OUTPUT: «g»
However, what you are defining here is a key pointing to a Pair
, which is fine if that is what you want and your nested hash has a single key. But %h<e>
will point to a Pair
which will have these consequences:
my = e => f => 'g';<e><q> = 'k';# OUTPUT: «PairCannot modify an immutable Str (Nil) in block <unit>»
This, however, will effectively define a nested hash:
my = e => ;say <e>.^name; # OUTPUT: «Hash»say <e><f>; # OUTPUT: «g»
If a Pair
is encountered where a value is expected, it is used as a hash value:
my = 'a', 'b' => 'c';say <a>.^name; # OUTPUT: «Pair»say <a>.key; # OUTPUT: «b»
If the same key appears more than once, the value associated with its last occurrence is stored in the hash:
my = a => 1, a => 2;say <a>; # OUTPUT: «2»
To assign a hash to a variable which does not have the %
sigil, you may use the %()
hash constructor:
my = %( a => 1, b => 2 );say .^name; # OUTPUT: «Hash»say <a>; # OUTPUT: «1»
If one or more values reference the topic variable, $_
, the right-hand side of the assignment will be interpreted as a Block
, not a Hash:
my = [%( id => "1A", firstName => "Andy", lastName => "Adams" ),%( id => "2B", firstName => "Beth", lastName => "Burke" ),# ...];sub lookup-user (Hash )my = map, ;
This would have been avoided if you had used the %()
hash constructor. Only use curly braces for creating Blocks.
Hash slices§
You can assign to multiple keys at the same time with a slice.
my ; <a b c> = 2 xx *; .raku.say; # OUTPUT: «{:a(2), :b(2), :c(2)}»my ; <a b c> = ^3; .raku.say; # OUTPUT: «{:a(0), :b(1), :c(2)}»
Non-string keys (object hash)§
By default keys in { }
are forced to strings. To compose a hash with non-string keys, use a colon prefix:
my = :;
Note that with objects as keys, you often cannot use the <...>
construct for key lookup, as it creates only strings and allomorphs. Use the {...}
instead:
:<0>.say; # Int as key, IntStr in lookup; OUTPUT: «(Any)»:.say; # Int as key, Int in lookup; OUTPUT: «42»:<0>.say; # Str as key, IntStr in lookup; OUTPUT: «(Any)»:.say; # Str as key, Str in lookup; OUTPUT: «42»:<0>.say; # IntStr as key, IntStr in lookup; OUTPUT: «42»
Note: Rakudo implementation currently erroneously applies the same rules for :{ }
as it does for { }
and can construct a Block
in certain circumstances. To avoid that, you can instantiate a parameterized Hash directly. Parameterization of %
-sigiled variables is also supported:
my Num = "0" => 0e0; # Str keys and Num valuesmy = 0 => "x"; # Int keys and Any valuesmy Num = 0 => 0e0; # Int keys and Num valuesHash[Num,Int].new: 0, 0e0; # Int keys and Num values
Now if you want to define a hash to preserve the objects you are using as keys as the exact objects you are providing to the hash to use as keys, then object hashes are what you are looking for.
my ;my = now;= "Our first milestone.";sleep 1;my = now;= "Logging this Instant for spurious raisins.";for .sort -> (:, :)
This example uses an object hash that only accepts keys of type Instant
to implement a rudimentary, yet type-safe, logging mechanism. We utilize a named state variable for keeping track of the previous Instant
so that we can provide an interval.
The whole point of object hashes is to keep keys as objects-in-themselves. Currently object hashes utilize the WHICH method of an object, which returns a unique identifier for every mutable object. This is the keystone upon which the object identity operator (===) rests. Order and containers really matter here as the order of .keys
is undefined and one anonymous list is never === to another.
my ;my = now;= "Our first milestone.";sleep 1;my = now;= "Logging this Instant for spurious raisins.";say (, ) ~~ .keys; # OUTPUT: «False»say (, ) ~~ .keys.sort; # OUTPUT: «True»say (, ) === .keys.sort; # OUTPUT: «False»say === .keys.sort[0]; # OUTPUT: «True»
Since Instant
defines its own comparison methods, in our example a sort according to cmp will always provide the earliest instant object as the first element in the List
it returns.
If you would like to accept any object whatsoever in your hash, you can use Any
!
my ;= "This is an Instant";= "This is a DateTime, which is not an Instant";= "Monty Python references are neither DateTimes nor Instants";
There is a more concise syntax which uses binding.
my := :;
The binding is necessary because an object hash is about very solid, specific objects, which is something that binding is great at keeping track of but about which assignment doesn't concern itself much.
Since 6.d was released, Junction
s can also be used as hash keys. The result will also be a Junction
of the same type used as key.
my = %( a => 1, b => 2, c=> 3);say ; # OUTPUT: «any(1, 3)»say ; # OUTPUT: «one(2, 3)»say ; # OUTPUT: «all(1, 3)»
If a Junction of any kind is used to define a key, it will have the same effect of defining elements of the Junction
as separate keys:
my = %( "a"|"b" => 1, c => 2 );say ; # OUTPUT: «any(1, 2)»
Constraint value types§
Place a type object in-between the declarator and the name to constrain the type of all values of a Hash
.
my Int ;put <Goku> = 900;try# OUTPUT:# 9001# Type check failed in assignment to %h; expected Int but got Str ("string")
You can do the same by a more readable syntax.
my of Int; # the same as my Int %h
If you want to constraint the type of all keys of a Hash
, add {Type}
following the name of variable.
my ;
Even put these two constraints together.
my of Int;put = 42;trytrytry# OUTPUT:# 42# Type check failed in binding to parameter 'assignval'; expected Int but got Str ("String")# Type check failed in binding to parameter 'key'; expected Int but got Str ("string")# Type check failed in binding to parameter 'key'; expected Int but got Str ("string")
Looping over hash keys and values§
A common idiom for processing the elements in a hash is to loop over the keys and values, for instance,
my = 'a' => 1, 'e' => 2, 'i' => 3, 'o' => 4, 'u' => 5;for .kv -> ,
gives output similar to this:
a: 1 e: 2 o: 4 u: 5 i: 3
where we have used the kv
method to extract the keys and their respective values from the hash, so that we can pass these values into the loop.
Note that the order of the keys and values printed cannot be relied upon; the elements of a hash are not always stored the same way in memory for different runs of the same program. In fact, since version 2018.05, the order is guaranteed to be different in every invocation. Sometimes one wishes to process the elements sorted on, e.g., the keys of the hash. If one wishes to print the list of vowels in alphabetical order then one would write
my = 'a' => 1, 'e' => 2, 'i' => 3, 'o' => 4, 'u' => 5;for .sort(*.key)>>.kv -> (, )
which prints
a: 1 e: 2 i: 3 o: 4 u: 5
in alphabetical order as desired. To achieve this result, we sorted the hash of vowels by key (%vowels.sort(*.key)
) which we then ask for its keys and values by applying the .kv
method to each element via the unary >>
hyperoperator resulting in a List
of key/value lists. To extract the key/value the variables thus need to be wrapped in parentheses.
An alternative solution is to flatten the resulting list. Then the key/value pairs can be accessed in the same way as with plain .kv
:
my = 'a' => 1, 'e' => 2, 'i' => 3, 'o' => 4, 'u' => 5;for .sort(*.key)>>.kv.flat -> ,
You can also loop over a Hash
using destructuring.
In place editing of values§
There may be times when you would like to modify the values of a hash while iterating over them.
my = illuminatus => 23, hitchhikers => 42;# OUTPUT: «hitchhikers => 42, illuminatus => 23»for .values -> ; # FailsCATCH ;# OUTPUT: «X::AdHoc: Cannot assign to a readonly variable or a value»
This is traditionally accomplished by sending both the key and the value as follows.
my = illuminatus => 23, hitchhikers => 42;for .kv -> , ;
However, it is possible to leverage the signature of the block in order to specify that you would like read-write access to the values.
my = illuminatus => 23, hitchhikers => 42;for .values -> is rw ;
It is not possible directly to do in-place editing of hash keys, even in the case of object hashes; however, a key can be deleted and a new key/value pair added to achieve the same results. For example, given this hash:
my = a => 1, b => 2;for .keys.sort ->say ''; # OUTPUT: «a => 1; b => 2; »
replace key 'b' with 'bb' but retain 'b's value as the new key's value:
for .keys ->for .keys.sort ->say ''; # OUTPUT: «a => 1; bb => 2; »