Search code examples
typeshashmaprakutypechecking

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?


Solution

  • 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)})