Создание хеша, который доступен только для чтения вне модуля, но для чтения / записи внутри - PullRequest
8 голосов
/ 25 апреля 2019

Я создаю модуль, который имеет довольно сильно вложенные хэши.Хеш должен быть полурегулярно модифицирован модулем, который, к сожалению, исключает использование Map.

Как правило, ветвь вложенного хэша будет возвращена пользователям модуля [1], исамое простое, что нужно сделать, это просто вернуть этот вложенный хэш, например:

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

Однако природа контейнеров, таких как массивы или хэши, заключается в том, что, хотя вы можете сделать их доступными только для чтения, ключ / значения все еще могутбыть изменены.Тем не менее, пользователи модуля не должны изменять эти значения по ряду причин.Принуждение к Map не поможет, потому что, если любое из значений также является контейнерами, они тоже будут модифицируемыми.

Моей первой мыслью было создание подкласса Hash (или иным образом создание пользовательского Associative), но автовификация по умолчанию все еще идет в Hash.Это, однако, может быть легко решено путем переопределения как AT-KEY, так и ASSIGN-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-KEY или ASSIGN-KEY?Или следует проверить, что эти два шага составляют 99,9% обращений к этим методам?[2]


[1] Есть только несколько веток subx4, которыми пользователь может манипулировать, и поэтому я считаю, что лучше предоставить им обязательно более медленный метод .Hash, когдаим это действительно нужно, чем предполагать, что им всегда нужен манипулируемый контейнер.Иногда их можно вызвать достаточно (в частности, с помощью шаблона get-branch($foo){$subbranch}{$subsubbranch}), что дополнительные издержки при создании глубокого клона хэша становятся достаточно последовательными.
[2] Я не слишком обеспокоен предотвращением ЛЮБОГО доступа (хотяМне, конечно, любопытно, возможно ли это исключительно через подклассы), потому что я уверен, что довольно трудолюбивый кодер всегда может что-то выяснить, но я бы хотел воспользоваться наиболее распространенными из них, чтобы сказать: «Не могу коснутьсяэтот!"(прослушайте музыку 90-х годов ...) и предоставьте сообщение об ошибке Awesome .

Ответы [ 2 ]

8 голосов
/ 25 апреля 2019

Вероятно, легче достичь этого, возвращая что-то, оборачивающее оригинал Array или Hash, или, альтернативно, используя 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, чтобы увидеть, что большинство тестов здесь проваливаются из-за отсутствия защиты.

4 голосов
/ 28 апреля 2019

Решение, которое я в конечном итоге использовал, служило моим потребностям, и я публикую его здесь для тех, кто может столкнуться с подобными ситуациями. (К сожалению, ответ со смешением ролей не переживает связывания)

Мой основной подход - больше всего беспокоиться о непреднамеренном редактировании. Чтобы защититься от этого, я создал класс Associative типа DB-Item, который внутренне имеет хэш. Метод AT-KEY возвращает элемент из хэша, если он существует, но ASSIGN-KEY и BIND-KEY просто сразу завершаются ошибкой с соответствующим сообщением об ошибке. Единственный другой метод - ADD-TO-DATABASE. Этот метод обрабатывает добавление листьев / веток в зависимости от того, что он был передан (и, как правило, конечные пользователи должны опасаться прямого использования всех методов caps). Поскольку ветви могут быть разной длины, это также значительно упрощает первоначальное создание БД:

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 { ... }
}
...