Ладно, это что, монадическое?


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

Конкретные вопросы таковы: -- Документы определяются (по крайней мере, в процессе передачи) как объекты JSON, поэтому правила могут быть иерархическими, и поэтому механизм правил должен работать рекурсивно. Например, объект работник может иметь суб-объект, называемый компенсации и этот подобъект имеет поле PayPeriod, которое должно быть одним из "еженедельно", "раз в две недели" или "ежемесячно". -- он работает узлом.js и некоторые правила должны считываться из входных данных (например, для чтения дополнительных пользовательских данных из базы данных) , поэтому он должен выполняться в стиле продолжения.

Итак, я пришел к следующему выводу: каждое правило-это функция, которая принимает текущее значение, предлагаемое новое значение и обратный вызов, который вызывается вместе с используемым значением. Это значение может быть одним из двух входных данных или каким-то третьим значением рассчитано по правилу. Вот одно правило:

var nonEmpty = function(proposedValue, existingValue, callback) {
    callback( (proposedValue.length > 0) ? proposedValue : existingValue);
};

Это правило позволит вам только установить или заменить это поле значением ненулевой длины. Конечно, это имеет смысл только для строковых значений (игнорируйте списки на данный момент, поэтому нам нужно правило для принудительного выполнения string-ness):

var isString = function(proposedValue, existingValue, callback) {
    callback( ( typeof(proposedValue) === 'string') ? proposedValue : existingValue);
};

На самом деле, это похоже на обычную проблему, поэтому я написал генератор правил:

var ofType = function(typeName) {
    return function(proposedValue, existingValue, callback) {
    callback( ( typeof(proposedValue) === typeName) ? proposedValue : existingValue);
    };
};

var isString = ofType('string')

Но мне нужен способ связать правила вместе:

var and = function(f1, f2) {
    return function(proposedValue, existingValue, callback) {
    f1(proposedValue, existingValue, 
       function(newProposedValue) {
           f2(newProposedValue, existingValue, callback);
       });
    };
};

var nonEmptyString = and(isString, nonEmpty);

Таким образом, правило для администратора, чтобы обновить Запись сотрудника может быть:

limitedObject({
   lastName : nonEmptyString,
   firstName : nonEmptyString,
   compensation : limitedObject({
      payPeriod : oneOf('weekly', 'biweekly', 'monthly'),
      pay : numeric
  }
}) 

limitedObject (like ofType) является функцией, генерирующей правила, и она допускает только поля, указанные в ее аргументе, и применяет данное правило к значениям этих полей.

Итак, я написал все это, и это работает как заклинание. Все мои ошибки оказались ошибками в модульных тестах! Ну, почти все из них. В любом случае, если вы прочитали это далеко, вот мой вопрос:

Я лихорадочно изучал монады, и мое чтение вдохновило меня на решение этой проблемы. проблема вот в чем. Но действительно ли это монада?

(возможные ответы:" Да"," нет, но это нормально, потому что монады не совсем правильный подход к этой проблеме", и"нет, и вот что нужно изменить". Четвертые возможности также приветствуются.)

1 4

1 ответ:

Нет, это не кажется монадическим. То, что вы определили, похоже на мини-DSL комбинаторов правил , где у вас есть простые правила, такие как ofType(typeName), и способы объединения правил в более крупные правила, такие как and(rule1, rule2).

Для того чтобы иметь монаду, вам нужно некоторое понятие контекста, в который вы можете поместить любое значение. Вам также потребуются следующие операции:
  1. функция wrap(x) для помещениялюбого значения в некоторый контекст по умолчанию.
  2. функция map(f, m) для применение функции f для преобразования значения в пределах m без изменения контекста.
  3. функция flatten(mm) для сглаживания двух слоев контекста в один.
Эти операции должны удовлетворять определенным "очевидным" законам:
  1. Добавление слоя контекста снаружи и сворачивание дает вам то, с чего вы начали.
    flatten(wrap(m)) == m
    
  2. Добавление слоя контекста внутри и сворачивание возвращает вам то, что вы начали с.

    flatten(map(wrap, m)) == m
    
  3. Если у вас есть значение с тремя слоями контекста, не имеет значения, свернете ли вы сначала два внутренних или два внешних слоя.
    flatten(flatten(mmm)) == flatten(map(flatten, mmm))
    

Также можно определить монаду в терминах wrap, как указано выше, и другую операцию bind, однако это эквивалентно вышеописанному, поскольку вы можете определить bind в терминах map и flatten, и наоборот.

function bind(f, m) { return flatten(map(f, m)); }

# or

function map(f, m) { return bind(function(x) { return wrap(f(x)); }, m); }
function flatten(mm) { return bind(function(x) { return x; }, mm); }

Неясно, каким будет здесь понятие контекста, каким образом вы превратили бы любую ценность в правило. Таким образом, вопрос о том, как сгладить два слоя правил, имеет еще меньше смысла.

Я не думаю, что монада является подходящей абстракцией здесь. Однако там легко увидеть, что ваш and образует моноид С всегда следующим правилом (показано ниже) в качестве элемента идентичности.
function anything(proposedValue, existingValue, callback) {
    callback(proposedValue);
}