I appreciate the value of immutable data structures, and really like that Raku has many built in. I particularly like that the compiler/typechecker will enforce immutability for me – I might have an off day or get careless about something, but the compiler never will.
Or at least, that's what I thought.
I was very surprised to see, however, that the following code runs without a peep from the typechecker:
my Map $m = Hash.new('key', 'value');
say $m.WHAT # OUTPUT: «(Hash)»
After consulting the docs, I see that Map
is a parent class for Hash
(and thus Hash.isa('Map')
returns True
. So I understand how (on a mechanical level) that typechecks successfully. But I'm left with two questions: first why does the inheritance work like that and, second, what can I do about it if I really want the typechecker to guarantee that my immutable variables stay that way.
On the "why" question — what's different about Maps that they're built like this? None of Raku's other immutable types are: Set.isa('SetHash')
, Mix.isa('MixHash')
, Bag.isa('BagHash')
, Blob.isa('Buf')
, and (if it counts) List.isa('Array')
all return False
. [Edit: as jjmerelo points out below, I reversed all of these. I should have said SetHash.isa('Set')
, MixHash.isa('Mix')
, BagHash.isa('Bag')
and Buf.isa('Blob')
all return False
. Interestingly, Array.isa('List')
returns True
, which lends some support to Elizabeth Mattijsen's statement that this is a historical oversight – List
s and Map
s are definitely more fundamental data types than most of the other immutable types.]
What's different about Map
s and Hash
es that they have this behavior?
On the more practical question, is there anything I can do to get the typechecker to help me out more here? I know that, in this specific case, I can write something like
my Map $m where { .WHAT === Map } = Hash.new('key', 0); # Throws the error I wanted
Or even
subset MapForRealThisTime of Map where { .WHAT === Map }
Are those really the best alternatives? They both feel a bit clunky (and the where
block could potentially have a runtime cost?) but maybe that's the best approach?
More generally, what I'd really like is a way to typecheck in strict mode, so to speak. If I explicitly declare the type of a variable, I'd really like the compiler to guarantee that the variable has that exact type – not some other type that happens to have that type as a parent. Is there any more general approach I can take, or am I just asking for a level of strictness that Raku isn't going to provide?
What's different about Maps and Hashes that they have this behavior?
Personally, I think this is a historical oversight that needs fixing at some point in the future.
Are those really the best alternatives?
I think you're looking for the is
trait in this context:
my %m is Map = a => 42, b => 666;
dd %m; # Map.new((:a(42),:b(666)))
%m<a> = 666; # Cannot change key 'a' in an immutable Map
%m<c> = 666; # Cannot add key 'c' to an immutable Map
am I just asking for a level of strictness that Raku isn't going to provide
I'm afraid you are. You can use the =:=
operator in a where
clause:
subset RealMap of Map where .WHAT =:= Map;
my RealMap $m = Map.new((a => 42)); # works
my RealMap $h = Hash.new((a => 42));
# Type check failed in assignment to $m; expected RealMap but got Hash ({:a(42)})