Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Creating a hash that is read-only outside of a module, but read/write inside

I am creating a module that has some fairly heavily nested hashes. The hash needs to be semi-regularly modified by the module, which unfortunately rules out using Map.

Generally, a branch of the nested hash will be returned to users of the module [1], and the simplest thing to do is to just return that nested hash, e.g.:

return %data{$branch}{$subbranch} 
# ↪︎ %(subsubbranch1 => ... , subsubbranch2 => ... )

However, the nature of containers like arrays or hashes is that while you can make them read-only, the key/values can still be modified. The module users though should not actually modify those values for a number of reasons. Coercing to Map won't help, because if any of the values are also containers, they too will be modifiable.

My first thought was to subclass Hash (or otherwise make a custom Associative), but autovivification by default still goes to Hash. That, however, can be easily solved by overriding both AT-KEY and ASSIGN-KEY so that the AT-KEY returns an instance of the subclass if the key doesn't already exist:

class ProtectedHash is Hash {
    has %!hash = ();
    method EXISTS-KEY ($key)         { %!hash{$key}:exists   }
    method ASSIGN-KEY ($key, \value) { %!hash{$key} = value  }
    method AT-KEY     ($key) {
        %!hash{$key} := ProtectedHash.new unless %!hash{$key}:exists;
        %!hash{$key};
    }
}

What I'd like to do is to fail if the ASSIGN-KEY (or the autovivification part of AT-KEY) is called from outside my module. I thought about using something like $?MODULE but that would be set at compile time and always be true. It looks like I can shimmy off of Backtrace a bit and check for the name of the file that called, but how consistent can I assume the call trace to those two functions?

For example, for ASSIGN-KEY I've got:

method ASSIGN-KEY ($key, \value) { 
    my @trace = Backtrace.new.list[3..*];
        # The first three can be ignored:
        # 0: code          at ...Backtrace.pm6 
        # 1: method new    at ...Backtrace.pm6
        # 2: method AT-KEY at ...ThisFile.pm6
    if/unless ??? {
        %!hash{$key} = value
    }
}

AT-KEY is normally called by the sub postcircumfix<{ }> (in which case @trace[0] can be ignored, and trace[1] would be the one of interest) but could also be, albeit rarely, called directly, in which case trace[0] is where I'd want to verify the file name.

Are there any other common ways in which AT-KEY or ASSIGN-KEY might be called? Or should check those two steps account for 99.9% of calls to those methods? [2]


[1] There are only a few subx4 branches that a user might want to manipulate, and so I figure it's best to provide them with the necessarily-slower .Hash method for when they really need it than to assume they always need a manipulable container. At times these may be called enough (particularly via a get-branch($foo){$subbranch}{$subsubbranch} pattern), that the addition overhead in creating a deepclone of the Hash becomes decently consequential.
[2] I'm not too concerned about preventing ANY access (although I'm certainly curious if that's possible purely via subclassing), because I'm sure that a fairly industrious coder could always figure something out, but I'd like to catch the most common ones as a way of saying "Can't touch this!" (cue the 90's music…) and provide an Awesome error message.

like image 802
user0721090601 Avatar asked Apr 25 '19 02:04

user0721090601


2 Answers

It's probably easier to achieve this by returning something wrapping the original Array or Hash, or alternatively using but to do a shallow copy and mix in to it (which means you retain the original type).

We can declare a role like this:

role Can'tTouchThis {
    method AT-KEY(|) {
        untouchable callsame
    }

    method ASSIGN-KEY(|) {
        die "Cannot assign to this";
    }

    method AT-POS(|) {
        untouchable callsame
    }

    method ASSIGN-POS(|) {
        die "Cannot assign to this";
    }
}

Where the sub untouchable is defined as:

multi untouchable(Positional \p) {
    p but Can'tTouchThis
}
multi untouchable(Associative \a) {
    a but Can'tTouchThis
}
multi untouchable(\o) {
    o
}

Thus handling nested data structures by - on access - creating a read-only facade to those too.

Here's an example and some test cases to illustrate the effect:

class Example {
    has %!foo = a => [ 1, 2, [ 3, 4] ], b => { c => { d => 42, e => 19 }, f => 100 };

    method get($sym) {
        untouchable %!foo{$sym}
    }
}

given Example.new {
    use Test;

    # Positional cases
    is .get('a')[0], 1;
    is .get('a')[2][1], 4;
    dies-ok { .get('a')[1] = 42 };
    is .get('a')[1], 2;

    # Associative cases
    is .get('b')<c><d>, 42;
    dies-ok { .get('b')<f> = 99 };
    dies-ok { .get('b')<c><d> = 99 };
    is .get('b')<f>, 100;
    is .get('b')<c><d>, 42;

    # Auto-viv also doesn't work
    dies-ok { .get('a')[4]<a> = 99 };
    dies-ok { .get('a')[4][0] = 99 };
}

Remove the untouchable call in the get method to see the majority of the tests here fail due to lack of protection.

like image 156
Jonathan Worthington Avatar answered Oct 16 '22 02:10

Jonathan Worthington


The solution I ultimately employed served my needs, and I'm posting it here for those who may encounter similar situations. (The answer with role mixing unfortunately doesn't survive binding)

My ultimate approach was to worry the most about unintended editing. To protect against this, I created an Associative-type class called DB-Item that internally has a hash. The AT-KEY method returns the item from the hash if it exists, but ASSIGN-KEY and BIND-KEY simply immediately fail with an appropriate error message. The only other method is ADD-TO-DATABASE. That method handles adds leafs/branches depending on what it's passed (and in general end users should be wary of using all caps methods directly). Since branches can be of different lengths, this also greatly simplifies the initial DB creation:

class DB-Item does Associative {

  has %!hash   = ();
  my  $epitaph = "Modification of the database is not a good idea:\n" ~
                 "  - Use .clone if you want to get a editable branch.\n" ~ 
                 "  - If you really know what you're doing, use .ADD-TO-DATABASE";

  method ADD-TO-DATABASE (*@branch) {
    if @branch == 2 {
      %!hash{@branch.head} = @branch.tail
    }else{
      %!hash{@branch.head} = DB-Item.new;
      %!hash{@branch.head}.ADD-TO-DATABASE(@branch[1..*]);
    }
  }

  method ASSIGN-KEY(|) is hidden-from-backtrace { die $epitaph }
  method BIND-KEY(|)   is hidden-from-backtrace { die $epitaph }

  method EXISTS-KEY($key) { %!hash{$key}:exists                        }
  method AT-KEY($key)     { %!hash{$key}:exists ?? %!hash{$key} !! Nil }

  method clone { ... }
}
like image 4
user0721090601 Avatar answered Oct 16 '22 03:10

user0721090601