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)})
Warning This is probably a nanswer for the original intent of the OP. Anyway, I'll try to answer all the questions that are posed, before arriving to the final nanswer. I guess this makes it a partial nanswer.
OK, let's try to answer all the questions in order.
first why does the inheritance work like that and
Well, you're putting a value in a container whose type is compatible with that value. No big deal here. This is the class hierarchy
A Hash
is-a
Map
, so there's no problem assigning it there, right? You could have declared it Cool
and it wouldn't complain anyway. It will not complain either if you simply use my $m
. But in any case, the container type will be what it's declared, while its content will still be a Hash
; if you use say $m.^name
it will still return Hash
.
Let's go for the second question:
what can I do about it if I really want the typechecker to guarantee that my immutable variables stay that way
Use binding, not assignment. In that case, you will only be able to bind if the type is exactly the same or there's an easy coercion. In this case, you need to bind to a map
my $m := Map.new('key', 'value');
By assigning a Hash to a Map, you are not really coercing it into a Map; you're simply using a compatible container for a variable that's still a Map. Even if you bind:
my Map $m := Hash.new('key', 'value');
still not coercing, still a Hash. You need to explicitly coerce (dare I say map
?)
my Map $m := Hash.new('key', 'value').Map;
and then, well, it will be immutable. Let's go to the next one:
Set.isa('SetHash'), Mix.isa('MixHash'), Bag.isa('BagHash'), Blob.isa('Buf'), and (if it counts) List.isa('Array') all return False
Well, Map.isa("Hash")
also returns False
. Same pattern here. Imagine Hash
is called HashMap
. Same thing. Inheritance goes only in one direction. You can still assign or bind a SetHash to a Set variable, you will still need to make it a SetHash to make it immutable.
is there anything I can do to get the typechecker to help me out more here?
Just assign to a Map
and convert whatever is assigned to a Map, or declare it from scratch. This will not error:
my Map $m where { .WHAT === Map } = Hash.new('key', 0).Map;
# Map.new((key => 0))
But you can simply say
my Map $m = Hash.new('key', 0).Map;
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,
OK, I see what you mean now. Scala does have a way of doing that, you can declare where values will go up and down in the hierarchy; I guess that includes being totally strict and only allowing the type itself. I don't know if this is a bug, but rather a feature. And anyway I can't think of a better solution that the one you mention, except you would probably need to do that for every single variable you want strictly type, checked, since it can't be parametrized (I think).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With