Okay, is this * this * monadic?

I put a security layer in front of the documented document, and I needed an abstract way for the application to determine which new documents and which updates to existing documents are legal for a particular user.

Special Issues - Documents are defined (at least in transit) as JSON objects, so rules can be hierarchical and therefore the rules engine must work recursively. For example, an Employee object may have a sub-object called Compensation, and this sub-object has a PayPeriod field, which should be one of the “weekly”, “two-week”, or “monthly” ones. - it launches Node.js, and some rules should read from input (for example, to read more user data from the database), so it should work in a continuation style.

So, I came up with the following: each rule is a function that takes a current value, a proposed new value, and a callback that is called with the value to be used. This value can be one of two inputs or some third value calculated according to the rule. Here is one rule:

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

This rule will allow you to set or replace this field with a value of nonzero length. Of course, this only makes sense for string values ​​(ignore lists for now, so we need a rule to force string-ness to be used):

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

Actually this seems like a normal problem, so I wrote a rule generator:

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

but I need a way for string rules:

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

Thus, an administrator rule for updating an Employee record can be:

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

limitedObject (for example, ofType ) is a function that generates a rule, and it allows only the fields specified in its argument, and applies this rule to the values ​​of these fields.

So, I wrote all this and it works like a charm. All my errors turned out to be errors in unit tests! Well, almost all of them. Anyway, if you read this far, here is my question:

I studied the monads febrilely, and my reading inspired me to solve this problem. But is it really monadic?

(Possible answers: “Yes,” “No, but that’s good, because monads aren’t very suitable for this problem,” and “No, and what needs to be changed here.” Fourth possibilities are also welcome.)

+4
source share
1 answer

No, this does not seem monadic. What you defined seems to be mini-DSL rule combinators, where you have simple rules like ofType(typeName) and ways to combine rules into larger rules like and(rule1, rule2) .

To have a monad, you need some kind of context concept in which you can put any value. You will also need the following operations:

  • The wrap(x) function to put any value in some default context.
  • The map(f, m) function to use the f function to convert a value inside m without changing the context.
  • The flatten(mm) function to align two layers of context into one.

These operations must satisfy certain “obvious” laws:

  • Adding a context layer outside and collapsing brings you back to where you started.

     flatten(wrap(m)) == m 
  • Adding a context layer inward and collapsing brings you back to where you started.

     flatten(map(wrap, m)) == m 
  • If you have a value with three levels of context, it doesn't matter, you will first collapse the two inner or two outer layers.

     flatten(flatten(mmm)) == flatten(map(flatten, mmm)) 

You can also define a monad in terms of wrap , as described above, and another bind operation, however this is equivalent to the above, since you can define bind in terms of map and flatten , and vice versa.

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

It is not clear what context will be here, how you would turn any value into a rule. Thus, the question of how to smooth out two levels of rules makes even less sense.

I don't think monad is a suitable abstraction here.

However, it is easy to see that your and creates a monoid with the same rule (shown below) as a single element.

 function anything(proposedValue, existingValue, callback) { callback(proposedValue); } 
+4
source

Source: https://habr.com/ru/post/1369251/


All Articles