javascript - 是否可以直接评估Mapbox表达式?

标签 javascript expression interpreter mapbox-gl-js

我正在寻找一种JavaScript表达式语法来指定JSON中的 Action 。 Mapbox's Expressions正是我要寻找的东西,但是我找不到关于是否可以在Mapbox之外使用的任何文档。那可能吗?如果是这样,您将如何做?

最佳答案

它们只是抽象语法树的JSON形式,因此您可以编写自己的执行程序。特别是,根据他们自己的文档,他们似乎遵循以下约定:

  • 数组是表达式,而所有其他JSON类型都是文字(奇怪的是,这直接意味着没有数组文字!稍后我将详细说明修复问题)
  • 数组的第一项是要执行的函数,而其余各项是该函数的参数。
  • 根对象不一定与表达式语法相关,只是它们发生的地方才使用它。
  • 唯一的“有状态”是let / var函数,该函数允许您创建范围为封闭的let表达式的变量,这表明它们有某种方式可以将上下文传递给函数。

  • 所以,让我们建立一个!我将尝试在下面逐行浏览代码,但是如果您喜欢那里的格式,也可以只看问题末尾的代码片段源。

    在这里,我们稍后将定义可用于表达式语言的所有功能
    const OPERATIONS = {};
    
    现在,让我们设置评估器功能。显然,它必须接收将要计算的表达式,而且必须接收可以由操作修改的上下文。
    const evaluate = (expression, context = {}) => {
    
    首先,我们通过评估文字本身来处理文字
      if (!(expression instanceof Array)) {
        return expression;
      }
    
    对,现在到真正的交易:
    让我们找出要运行的操作及其参数。
      const [operationKey, ...rawParameters] = expression;
      const operation = OPERATIONS[operationKey];
    
    我们通过恐慌来处理未知的操作! AAAH!
      if (operation == null) {
        throw new Error(`Unknown operation ${operationKey}!`);
      }
    
    哦,很好,我们知道此操作!现在,我们应该怎么称呼它?
    它显然需要接收其参数以及上下文,
    如果它是那些讨厌的有状态操作之一。另外,因为我们
    通过Mapbox的let可以看到,操作可以创建新的上下文!
    我建议以下签名,不过您可以为自己更改
    特定的偏好和用例:
    第一个参数:
    当前背景
    第二个参数:
    所有操作参数的数组。这使得
    如果操作是可变参数,则易于迭代,并且操作更简单
    仍然可以使用解构来获得“固定”签名。
    我们将传递未评估的参数“raw”,以便
    行动可以对它做任何邪恶的事情。
    返回值:
    无论操作要评估什么!
      return operation(context, rawParameters);
    };
    
    对对对,我们已经建立了评估器,但是我们如何实际使用它呢?
    我们需要一些操作,让我们从简单的操作开始吧:
    还记得我上面说的参数数组是原始的吗?我们需要在我们的操作函数中手动评估它们。
    OPERATIONS["-"] = (context, [a, b]) => evaluate(a, context) - evaluate(b, context);
    OPERATIONS["+"] = (context, [a, b]) => evaluate(a, context) + evaluate(b, context);
    
    好的,那很容易,但是如果我们想接受任意数量的参数呢?
    OPERATIONS["*"] = (context, parameters) => parameters
      .map(p => evaluate(p, context))
      .reduce((accumulator, x) => accumulator * x);
    
    对,现在让我们实现我们所说的那些数组。解决方案很简单,有一个操作可以根据其参数创建数组!
    OPERATIONS["array"] = (context, parameters) => parameters
      .map(p => evaluate(p, context));
    
    太酷了,但是撒旦本人的邪恶之子呢? letvar吗?
    让我们从它们中较小的一个开始:容易,我们只需读取该变量名称在上下文中存储的任何内容!
    OPERATIONS["var"] = (context, [variable]) => context[variable];
    
    现在,“tricky”代码let既可变又变化上下文!
    我将在这里拉出括号,因为它会比以前漂亮的单行操作大一些!
    OPERATIONS["let"] = (context, [...definitions]) => {
    
    是的,我们有一个上下文,但是我们不想在let块之外污染它!因此,让我们将其复制到一个新的临时目录中:
      const innerContext = { ...context };
    
    现在我们需要循环定义,记住,它们每个都是2个元素:
    变量名及其值表达式!但是首先,我们需要选择最后一个参数,该参数是要在结果上下文中执行的表达式:
      const body = definitions.pop()
    
    让我们排除一些明显的问题,如果定义中的事物奇数,则用户是错误的!让我们把它们扔在他们丑陋的脸上!让我们使用一个神秘的错误消息只是为了邪恶...
      if (definitions.length % 2 === 1) {
        throw new Error("Unmatched definitions!");
      }
    
    太酷了,现在我们要做的很酷的事情就是创建这些变量:
      for (let i = 0; i < definitions.length - 1; i += 2) {
        const name = definitions[i];
        const value = definitions[i + 1];
    
    在这里,我选择了同一块中的变量可以依赖
    如果您不喜欢以前的变量,请使用父级
    上下文,而不是我们当前正在修改的上下文。
        innerContext[name] = evaluate(value, innerContext);
      }
    
    变量已完成,现在让我们评估一下 body !
      return evaluate(body, innerContext);
    };
    
    至此完成!这是评估语法树的基础!
    您可能现在想继续并添加自己的特定于域的操作。
    我编写了此代码片段,以演示它如何最终工作,并且如果您的风格如此,则使用代码注释而不是识字编码。 HTML和CSS无关紧要,只是一些口红使其显得更像样。

    // Here we will later define all functions available for the expression language
    const OPERATIONS = {};
    
    // Now, let's set up the evaluator function.
    // It obviously must receive the expression it will evaluate,
    // but also a context that can be modified by operations.
    const evaluate = (expression, context = {}) => {
      // First, we deal with literals by evaluating them as themselves
      if (!(expression instanceof Array)) {
        return expression;
      }
    
      // Right, now to the real deal:
      // let's find out what operation to run and its parameters.
      const [operationKey, ...rawParameters] = expression;
      const operation = OPERATIONS[operationKey];
    
      // We handle unknown operations by panicking! AAAH!
      if (operation == null) {
        throw new Error(`Unknown operation ${operationKey}!`);
      }
    
      // Oh nice, we know this operation! Now, how should we call it?
      // It obviously needs to receive its parameters, as well as the context,
      // in case it is one of those pesky stateful operations. Plus, as we
      // have seen with Mapbox's `let`, operations can create new contexts!
      //
      // I propose the following signature, though you can change it for your
      // particular preference and use-cases:
      //
      // First parameter:
      //      Current context
      // Second parameter:
      //      Array of all of the operation's parameters. This makes for
      //      easy iteration if the operation is variadic, and simpler stuff
      //      can still just use deconstruction to have a "fixed" signature.
      //      We will pass the parameters "raw", not evaluated, so that the
      //      operation can do whatever evil things it wants to do to them.
      // Return value:
      //      Whatever the operation wants to evaluate to!
      return operation(context, rawParameters);
    };
    
    // Right, right, we have set up the evaluator, but how do we actually use it?
    // We need some operations, let's start with the easy ones to wet our feet:
    // Remember how I said above that the parameters array comes in raw?
    // We'll need to evaluate them manually inside our operation functions.
    OPERATIONS["-"] = (context, [a, b]) => evaluate(a, context) - evaluate(b, context);
    OPERATIONS["+"] = (context, [a, b]) => evaluate(a, context) + evaluate(b, context);
    
    // Okay, that was easy, but what if we want
    // to accept an arbitrary amount of arguments?
    OPERATIONS["*"] = (context, parameters) => parameters
      .map(p => evaluate(p, context))
      .reduce((accumulator, x) => accumulator * x);
      
    // Right, now let's implement those arrays we spoke of.
    // The solution is simple, have an operation that
    // creates the array from its parameters!
    OPERATIONS["array"] = (context, parameters) => parameters
      .map(p => evaluate(p, context));
    
    // Cool, cool, but what about the evil spawns of Satan himself? Let and Var?
    
    // Let's start with the lesser of them:
    // Easy, we just read whatever was stored in the context for that variable name!
    OPERATIONS["var"] = (context, [variable]) => context[variable];
    
    // Now, the "tricky" one, Let, which is both variadic AND changes the context!
    // I'll pull out my braces here  because it's gonna be a bit bigger than the
    // previous beautiful one-line operations!
    OPERATIONS["let"] = (context, [...definitions]) => {
      // Right, we have A context, but we don't want to pollute it outside
      // the Let block! So let's copy it to a new temporary one:
      const innerContext = { ...context
      };
    
      // Now we need to loop the definitions, remember, they are 2 elements each:
      // A variable name, and its value expression! But first, we need to pick
      // out the last argument which is the expression to be executed in the
      // resulting context:
      const body = definitions.pop()
    
      // Let's get the obvious stuff out of the way, if we have an odd number of
      // things in our definitions, the user is wrong! Let's throw it on their
      // ugly face! Let's use a cryptic error message just to be evil...
      if (definitions.length % 2 === 1) {
        throw new Error("Unmatched definitions!");
      }
    
      // Cool, now we get to do the cool stuff which is create those variables:
      for (let i = 0; i < definitions.length - 1; i += 2) {
        const name = definitions[i];
        const value = definitions[i + 1];
    
        // Here I made the choice that variables in the same block can depend
        // on previous variables, if that's not to your liking, use the parent
        // context instead of the one we're modifying at the moment.
        innerContext[name] = evaluate(value, innerContext);
      }
    
      // Variables are DONE, now let's evaluate the body!
      return evaluate(body, innerContext);
    };
    
    // Bonus points for reading the snippet code:
    // Remember that we are not limited to numeric values,
    // anything that JSON accepts we accept too!
    // So here's some simple string manipulation.
    OPERATIONS["join"] = (context, [separator, things]) => evaluate(things, context)
      .flat()
      .join(separator);
    
    // And we're done! That is the basic of evaluating a syntax tree!
    
    
    
    
    
    
    
    // Not really relevant to the question itself, just a quick and dirty REPL
    
    (() => {
      const input = document.getElementById("input");
      const output = document.getElementById("output");
    
      const runSnippet = () => {
        let expression;
    
        try {
          expression = JSON.parse(input.value);
        } catch (e) {
          // Let the user type at peace by not spamming errors on partial JSON
          return;
        }
    
        const result = evaluate(expression);
        output.innerText = JSON.stringify(result, null, 2);
      }
    
      input.addEventListener("input", runSnippet);
    
      runSnippet();
    })();
    html {
      display: flex;
      align-items: stretch;
      justify-content: stretch;
      height: 100vh;
      background: beige;
    }
    
    body {
      flex: 1;
      display: grid;
      grid-template-rows: 1fr auto;
      grid-gap: 1em;
    }
    
    textarea {
      padding: 0.5em;
      border: none;
      background: rgba(0, 0, 0, 0.8);
      color: white;
      resize: none;
    }
    <textarea id="input">
    [
      "let",
      "pi", 3.14159,
      "radius", 5,
      [
        "join",
        " ",
        [
          "array",
          "a circle with radius",
          ["var", "radius"],
          "has a perimeter of",
          [
            "*",
            2,
            ["var", "pi"],
            ["var", "radius"]
          ]
        ]
      ]
    ]
    
    </textarea>
    <pre id="output">
    </pre>

    关于javascript - 是否可以直接评估Mapbox表达式?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/65011797/

    相关文章:

    javascript - 检查 beforeunload 是否是由关闭选项卡/窗口或刷新引起的

    javascript - 在 Google 表格中使用计数器时遇到的问题

    R 组合表达式列表中的表达式

    regex - Powershell + 正则表达式 - 如何获得多个匹配项?

    python - 如何更改我的构建配置,以便 cmd 指向 python 解释器的实际位置?

    javascript - 如何创建可变填充

    javascript - XHR的asnyc参数设置为TRUE时不返回任何内容

    linq - 使用 LINQ to NHibernate 将 LINQ IQueryable 转换为分页 IQueryable

    MATLAB 饼图标签关闭解释器

    node.js - 指定 NodeJS 解释器