创建一个散列,该散列在模块外部是只读的,但在模块内部是可读写的

时间:2019-04-25 02:56:52

标签: hash immutability perl6

我正在创建一个模块,该模块具有大量嵌套的哈希值。哈希值需要由模块进行半规则修改,不幸的是,该哈希规则无法使用Map

通常,嵌套哈希的分支将返回给模块[1]的用户,最简单的方法是仅返回该嵌套哈希,例如:

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

但是,数组或哈希之类的容器的性质是,尽管您可以将它们设置为只读,但仍可以修改键/值。但是,出于多种原因,模块用户实际上不应修改这些值。强制转换为Map无济于事,因为如果任何值也是容器,它们也将是可修改的。

我的第一个想法是继承Hash的子类(或以其他方式创建自定义Associative),但是默认情况下,自动生存仍然归哈希所有。但是,可以通过同时覆盖AT-KEYASSIGN-KEY来解决此问题,以便如果密钥不存在,AT-KEY返回子类的实例:

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};
    }
}

如果从模块的外部调用ASSIGN-KEY(或AT-KEY的自动生存部分),我想做的就是失败。我曾考虑过使用$?MODULE之类的东西,但这将在编译时设置,并且始终是真实的。看来我可以稍微回溯一下Backtrace并检查被调用文件的名称,但是我可以假设调用跟踪到这两个函数的一致性如何?

例如,对于ASSIGN-KEY,我得到了:

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通常由子postcircumfix<{ }>调用(在这种情况下,@trace[0]可以忽略,trace[1]将是感兴趣的对象),但也可以是,尽管很少,直接调用,在这种情况下,trace[0]是我要验证文件名的位置。

还有其他可以调用AT-KEYASSIGN-KEY的方式吗?还是应该检查这两个步骤占这些方法调用的99.9%? [2]


[1]用户可能只想操纵几个subx4分支,所以我认为最好是在他们真正需要它时为他们提供必要的.Hash方法,而不是假设他们总是需要一个可操作的容器。有时这些调用可能足够多(尤其是通过get-branch($foo){$subbranch}{$subsubbranch}模式调用),以至于在创建Hash的深克隆时所产生的额外开销变得相当重要。
[2]我不太担心阻止任何访问(尽管我很好奇这是否纯粹可以通过子类实现),因为我敢肯定一个相当勤奋的编码器总能找出一些问题,但是我想抓住最常见的说法,说“不能碰这个!” (提示90年代的音乐…),并提供一条Awesome错误消息。

2 个答案:

答案 0 :(得分:8)

通过返回包裹原始ArrayHash的内容,或者使用but进行浅表复制并混入其中,这可能更容易实现(这意味着您保留了原始类型)。

我们可以声明这样的角色:

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";
    }
}

其中sub untouchable的定义为:

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

因此,在访问时,也通过创建嵌套的数据结构来处理嵌套的数据结构。

这是一个示例和一些测试用例来说明效果:

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 };
}

删除untouchable方法中的get调用以查看此处的大多数测试由于缺乏保护而失败。

答案 1 :(得分:4)

我最终采用的解决方案满足了我的需求,我将其发布在这里供那些可能遇到类似情况的人使用。 (不幸的是,角色混合的答案无法在绑定中幸存下来)

我的最终方法是最担心意外编辑。为了防止这种情况,我创建了一个名为{-{1}}-类型的类,称为DB-Item,该类在内部具有哈希。 Associative方法从哈希中返回该项目(如果存在),但是AT-KEYASSIGN-KEY只是立即失败并显示一条适当的错误消息。唯一的其他方法是BIND-KEY。该方法句柄根据传递的内容添加叶子/分支(通常,最终用户应警惕直接使用所有caps方法)。由于分支的长度可以不同,所以这也大大简化了初始数据库的创建:

ADD-TO-DATABASE