Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom ternary operators #154

Open
LeaVerou opened this issue Jul 26, 2021 · 8 comments
Open

Custom ternary operators #154

LeaVerou opened this issue Jul 26, 2021 · 8 comments

Comments

@LeaVerou
Copy link
Collaborator

This has come up many times in my use cases: Jsep allows custom unary and binary operators, but the ternary operator is implemented ad hoc. It would be nice if there was an analogous addTernaryOp() function that allowed custom ternary operators. Possibly via the ternary operator plugin?

@6utt3rfly
Copy link
Collaborator

If we pull out the ternary plugin by default, then different variations could be implemented by others, maybe? Perhaps the Jsep ternary plugin could provide options? What kind of use cases have you come across?

@LeaVerou
Copy link
Collaborator Author

If we pull out the ternary plugin by default, then different variations could be implemented by others, maybe? Perhaps the Jsep ternary plugin could provide options?

Not sure what you mean here exactly, could you elaborate? What I was saying is, right now the code for the ternary explicitly checks for ? and :, I was saying it could use a data structure of operators that can be extended by jsep users. Obviously that would make the code a lot more complex however. But it is quite arbitrary that we allow this kind of extensibility for unary and binary operators but not ternary operators.

What kind of use cases have you come across?

Personally, I use jsep to parse Mavo's expression language, MavoScript. It has a fair bit of word-like operators, e.g. person where age > 2. There are cases where we wanted ternaries (e.g. person by (age - 18) as adultYears) and had to implement them by adding two binary operators, which is quite an ugly hack for a variety of reasons.

@6utt3rfly
Copy link
Collaborator

Not sure what you mean here exactly, could you elaborate?

The plugin file could export a function that returns the plugin:

export (options = {}) => {
const operators = new Set([options.operators | '?']);
const alternatives = new Set([options.alternatives | ':']);
return {
  name: 'jsepTernary',
  init(jsep) { ... };
});

So the user could then use the plugin something like:

import jsepTernary from 'jsep/plugins/jsepTernary.js';

jsep.register(jsepTernary({ operators: ['?', 'by'], alternatives: [':', 'as'] });

However, I can almost see 2 different plugins from your given examples, one that's like a ternary with no alternative (where age > 2), and one that's more like a cast (as adultYears), which I can see as both being straight-forward to add now with the various hooks

@LeaVerou
Copy link
Collaborator Author

LeaVerou commented Jul 27, 2021

(I'm going to use my use case as a meta-example here, but this applies to most ternary use cases, I think)

I was thinking of more along the lines of an addTernaryOp() function (and corresponding data structure) that the plugin exposes. Also, it looks like with your proposed code any operator can go with any alternative, which is not typically desirable. E.g. I may want to allow x by y as z and x join y on z but not x by y on z.
Wait, perhaps you mean that the plugin basically generates plugins for ternaries, so one could register many op/alt sets?

Related, should it be possible to add ternary operators with the same symbol as binary operators?
It seems not, because then one could add by and as as separate binary operators, and ['by', 'as'] as a ternary operator, in which case x by y as z becomes ambiguous.
Therefore, perhaps the second operator should be optional, to enable both x by y and x by y as z?
Or maybe the restriction should be that you can't register binary operators that are the same as the alternate operator in a ternary? That way as cannot be registered but by can.

@EricSmekens
Copy link
Owner

I think this can be closed by the introducement of:
https://www.npmjs.com/package/@jsep-plugin/ternary

@6utt3rfly
Copy link
Collaborator

I think @LeaVerou 's question was to support non-standard ternary separators (? :).
However, I think the x by y could just be a BinaryExpression, same as y as z, rather than looking at it as a ternary... But to detect x by y as z and only allow as in certain cases, then I think that would have to be done by a plugin, perhaps on an after-expression event to validate the expression.

person where x > 2 is a type of conditional expression which could be done through a plugin as well. I'm not sure if that's something jsep would want to support as an official plugin?

@LeaVerou
Copy link
Collaborator Author

LeaVerou commented Aug 25, 2021

I think @LeaVerou 's question was to support non-standard ternary separators (? :).

Correct.

person where x > 2 is a type of conditional expression which could be done through a plugin as well. I'm not sure if that's something jsep would want to support as an official plugin?

I think you may be getting confused with the specific operators, which are merely examples. person where x > 2 is not a ternary, that's a binary operation (person where (x > 2)). If you look at my comment that mentions it, it was not in the context of ternary operators.

However, I think the x by y could just be a BinaryExpression, same as y as z, rather than looking at it as a ternary... But to detect x by y as z and only allow as in certain cases, then I think that would have to be done by a plugin, perhaps on an after-expression event to validate the expression.

x by y as z is a ternary, because y as z by itself should not be valid. Currently I'm indeed implementing them as two binary operators, but it's a hack, for this very reason. Otherwise JS would just have a ? operator and a : operator and be done with it, no reason for a ternary.

IMO it seems far more reasonable to make ternary operators just as generalizable as binary and unary ones than these hacks.

@6utt3rfly
Copy link
Collaborator

I think something like this would do what you're asking? (it could be updated to be more efficient. But it deviates from esprima, too by adding the operator property to the node):

const CONDITIONAL_EXP = 'ConditionalExpression';

export default {
  name: 'ternary',

  ternaryOps: {
    '?': ':',
    'by': 'as',
  },

  addTernaryOp(conditional, alternate) {
    this.ternaryOps[conditional] = alternate;
  },

  removeTernaryOp(conditional) {
    delete this.ternaryOps[conditional];
  },

  init(jsep) {
    // Ternary expression: test ? consequent : alternate
    const self = this;
    jsep.hooks.add('after-expression', function gobbleTernary(env) {
      const operator = env.node && Object
        .keys(self.ternaryOps)
        .find(op => op === this.expr.substr(this.index, op.length));
      if (operator) {
        this.index++;
        const test = env.node;
        const consequent = this.gobbleExpression();

        if (!consequent) {
          this.throwError('Expected expression');
        }

        this.gobbleSpaces();

        if (self.ternaryOps[operator] === this.expr.substr(this.index, operator.length)) {
          this.index++;
          const alternate = this.gobbleExpression();

          if (!alternate) {
            this.throwError('Expected expression');
          }
          env.node = {
            type: CONDITIONAL_EXP,
            test,
            consequent,
            alternate,
            operator,
          };
        }
        // if binary operator is custom-added (i.e. object plugin), then correct it to a ternary node:
        else if (consequent.operator === self.ternaryOps[operator]) {
          env.node = {
            type: CONDITIONAL_EXP,
            test,
            consequent: consequent.left,
            alternate: consequent.right,
            operator,
          };
        }
        else {
          this.throwError('Expected :');
        }
      }
    });
  },
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants