Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to enforce immutability with the Raku typesystem?

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 – Lists and Maps are definitely more fundamental data types than most of the other immutable types.]

What's different about Maps and Hashes 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?

like image 892
codesections Avatar asked Aug 07 '20 03:08

codesections


2 Answers

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)})
like image 145
Elizabeth Mattijsen Avatar answered Nov 08 '22 21:11

Elizabeth Mattijsen


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

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).

like image 3
jjmerelo Avatar answered Nov 08 '22 22:11

jjmerelo