javascript - 如何解析纯函数

标签 javascript functional-programming code-analysis

假设您有以下功能

var action = (function () {

  var a = 42;
  var b = 2;

  function action(c) {
    return a + 4 * b + c;
  }

  return action;
}());

// how would you parse action into it's serialized LISP / AST format?
var parsed = parse(action);

是否可以有一个函数引用函数 action 并输出 LISP 格式 (lambda (c) (plus (plus 42 (multiply 4 2)) c))

我们可以对action 的内容进行一些限制。

  • 正文只能是一个表达式
  • 它应该是一个纯函数
  • 任何自由变量都是常量

主要问题是给定一个函数,您可以使用一系列输入调用它的源代码,您能否找到正确的值来替换自由变量?

对于上面的示例,您知道 a 和 b 是常量,您可以智能地绘制一些值的输出并查看模式,然后只知道常量是什么。

问题:

您将如何编写一个函数,该函数采用函数引用及其源代码并为该函数生成某种形式的 AST,并用任何自由变量替换其运行时值。

AST 格式的一个示例是代码的 LISP 等价物。

我基本上想序列化和反序列化该函数并使其表现相同

应该注意的是,如果将 { a: a, b: b } 传递给分析函数,问题就会变得微不足道。那将是作弊。

用例:

我想生成一种与语言无关的纯 JavaScript 函数形式,这样我就可以有效地将它传递给 C++,而无需我的库的用户使用 DSL 来创建此函数

假设您有一个数据库驱动程序

var cursor = db.table("my-table").map(function (row) {
  return ["foo", row.foo]
})

您想在运行时确定函数是什么并将其转换为 AST 格式,以便您可以使用高效的查询构建器将其转换为 SQL 或您的数据库具有的任何查询引擎。

这意味着你不必写:

var cursor = db.table("my-table").map(function (rowQueryObject) {
    return db.createArray(db.StringConstant("foo"), rowQueryObject.getProperty("foo"))
})

这是一个 DB 库可以使用查询对象执行的函数,让您无需冗长的方法即可构建查询对象转换。

最佳答案

这是一个完整的解决方案(使用解析函数可访问的变量目录):

var CONSTANTS = { 
   a: 42,
   b: 2,
   c: 4
};

function test() {
    return a + 4 * b + c;
}

function getReturnStatement(func) {
    var funcStr = func.toString();
    return (/return\s+(.*?);/g).exec(funcStr)[1];
}

function replaceVariables(expr) {
    var current = '';
    for (var i = 0; i < expr.length; i += 1) {
        while (/[a-zA-Z_$]/.test(expr[i]) && i < expr.length) {
            current += expr[i];
            i += 1;
        }
        if (isNumber(CONSTANTS[current])) {
            expr = expr.replace(current, CONSTANTS[current]);
        }
        current = '';
    }
    return expr;
}

function isNumber(arg) {
    return !isNaN(parseInt(arg, 10));
}      

function tokenize(expr) {
    var tokens = [];
    for (var i = 0; i < expr.length; i += 1) {
        if (isWhitespace(expr[i])) {
            continue;
        } else if (isOperator(expr[i])) {
            tokens.push({
                type: 'operator',
                value: expr[i]
            });
        } else if (isParentheses(expr[i])) {
            tokens.push({
                type: 'parant',
                value: expr[i]
            });
        } else {
            var num = '';
            while (isNumber(expr[i]) && i < expr.length) {
                num += expr[i];
                i += 1;
            }
            i -= 1;
            tokens.push({
                type: 'number',
                value: parseInt(num, 10)
            });
        }
    }
    return tokens;
}

function toPrefix(tokens) {
    var operandStack = [],
        operatorStack = [],
        current,
        top = function (stack) {
            if (stack) {
                return stack[stack.length - 1];
            }
            return undefined;
        };

    while (tokens.length) {
        current = tokens.pop();
        if (current.type === 'number') {
            operandStack.push(current);
        } else if (current.value === '(' || 
                !operatorStack.length || 
                (getPrecendence(current.value) >
                 getPrecendence(top(operatorStack).value))) {

            operatorStack.push(current);
        } else if (current.value === ')') {
            while (top(operatorStack).value !== '(') {
                var tempOperator = operatorStack.pop(),
                    right = operandStack.pop(),
                    left = operandStack.pop();
                operandStack.push(tempOperator, left, right);
            }
            operatorStack.pop();
        } else if (getPrecendence(current.value) <= 
                getPrecendence(top(operatorStack).value)) {
            while (operatorStack.length &&
                    getPrecendence(current.value) <=
                    getPrecendence(top(operatorStack).value)) {

                tempOperator = operatorStack.pop();
                right = operandStack.pop();
                left = operandStack.pop();
                operandStack.push(tempOperator, left, right);
            }
        }

    }

    while (operatorStack.length) {
        tempOperator = operatorStack.pop();
        right = operandStack.pop();
        left = operandStack.pop();
        operandStack.push(tempOperator, left, right);
    }

    return operandStack;
}

function isWhitespace(arg) {
    return (/^\s$/).test(arg);
}

function isOperator(arg) {
    return (/^[*+\/-]$/).test(arg);
}

function isParentheses(arg) {
    return (/^[)(]$/).test(arg);
}

function getPrecendence(operator) {
    console.log(operator);
    switch (operator) {
        case '*':
            return 4;
        case '/':
            return 4;
        case '+':
            return 2;
        case '-':
            return 2;
        default:
            return undefined;
    }
}

function getLispString(tokens) {
    var result = '';
    tokens.forEach(function (e) {
        if (e)
            switch (e.type) {
            case 'number':
            result += e.value;
            break;
            case 'parant':
            result += e.value;
            break;
            case 'operator':
            result += getOperator(e.value);
            break;
            default:
            break;
        }
        result += ' ';
    });
    return result;
}

function getOperator(operator) {
    switch (operator) {
        case '+':
            return 'plus';
        case '*':
            return 'multiplicate';
        case '-':
            return 'minus';
        case '\\':
            return 'divide';
        default:
            return undefined;
    }
}

var res = getReturnStatement(test);
console.log(res);
res = replaceVariables(res);
console.log(res);
var tokens = tokenize(res);
console.log(tokens);
var prefix = toPrefix(tokens);
console.log(prefix);
console.log(getLispString(prefix));

我只是写的,所以风格可能有一些问题,但我认为思路很清楚。

您可以使用.toString 方法获取函数体。之后就可以使用正则表达式来匹配返回语句了

(/return\s+(.*?);/g).exec(funcStr)[1];

注意这里必须使用分号才能匹配成功!在下一步中,使用 CONSTANTS 对象将所有变量转换为数字值(我看到您还剩下一些参数,因此您可能需要在此处进行少量修改)。之后,字符串被标记化,以便于解析。在下一步中,中缀表达式被转换为前缀表达式。在最后一步,我构建了一个字符串,使输出看起来像您需要的那样(+ - plus, - - minus 等等)。

关于javascript - 如何解析纯函数,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/14353978/

相关文章:

c# - 将无主类型标记为弃用或过时

javascript - Angularjs $scope.$watchCollection 仅第一级属性

javascript - 通过 javascript 触发数据工厂管道

javascript - ui-grid表格PDF导出

scala - 使用严格的函数式编程从偏序集生成 DAG

functional-programming - 为什么状态单子(monad)中需要平面图?

python - 为部分参数应用 map

c# - VS2010 代码分析期间的 IDisposable 和 CA2000 警告

javascript - 使用 AngularJs ng-repeat 对项目详细信息进行分组

c# - 求一个ASP.Net的代码分析工具