在javascript中以算法方式识别纯函数

时间:2015-04-24 22:40:43

标签: javascript algorithm function functional-programming static-analysis

是否可以使用javascript确定javascript函数是否为" pure"?

  

当一个函数以可预测的方式运行时,它被称为,在某种意义上,对于每个x,函数将始终返回相同的关联y值(即单值映射) )。

例如,一个纯函数:

function pure(x) {
  return x * x;
}

并且不纯洁:

var x = 0;
function impure(y) {
  x = x + y;
  return x++;
}

虽然在impure(0) !== impure(0)这里很容易说出来,但对于以下功能来说,它并不明显:

function weird(x) {
  if (x === "specificThing") {
    return false;
  } else {
    return true;
  }
}

var count = 0;
function surprise(x) {
  count++;
  if (count === 10e10 && x === 0) {
    return true;
  } else {
    return false;
  }
}

另一种问这个问题的方法是,是否可以使用javascript确定javascript函数是否为" impure"?

理论上可能有可能或不可能,但实际上可以采取哪些步骤来开始确定这一点,可能会给出一组约束或假设?

纯度的另一个定义包括可能不会改变非局部变量的警告,但我想考虑一个单独的问题。在这种情况下,我们正在考虑将一致输入映射到一致输出的函数。

3 个答案:

答案 0 :(得分:3)

JavaScript是Turing complete,因此它可以像解析任何其他编程语言一样分析和分析JavaScript。所以,问题实际上是:" JavaScript功能是否可以自动测试"纯度","

简短回答

有时候。

答案很长

对于某些函数,AST是直截了当且所有符号都包含在内。像function(X) { return X * X; }这样的东西可以证明是纯粹的(用于原始输入),因为函数体中使用的唯一变量是作为函数参数传入的变量。此函数不依赖于任何其他API调用,而是纯粹的算术。我们绝对可以证明它是纯粹的。

当我们允许任意内容时,事情会发生变化,因为当JavaScript需要执行可以&的操作时,JavaScript没有明确的类型,而是从复杂到原始数据类型(甚至从原始数据类型到原始数据类型)will happily type-coerce #39;是否应用了该操作。使用对象(而不是数字)调用上面的函数会在水下执行进一步的函数调用,并且这些函数根本不能保证纯粹(参见Andreas对此的回答)

绝大多数JS函数都不像我们的简单函数。对于大多数功能,我们不仅要证明它们是纯粹的,还要证明它们内部调用的所有功能都是纯粹的。现在我们正在进入halting problem。让我们来看一个荒谬的简单例子:

function(x) {
  if (typeof window !== "undefined") {
    return x * x;
  }
  return x * x * x;
}
这是纯粹的吗?好吧,如果我们在浏览器中运行它,那么在浏览器中它是纯粹的,因为始终定义window。但是在像Node.js这样的东西中,可能是纯粹的,但它可能不是:我们无法证明它是,也不能证明它不是,因为我们无法证明这一点函数运行时存在神秘的window变量。虽然Node.js没有全局window变量,但我们可以随时轻松地引入一个变量,并且函数的行为会发生变化。现在我们突然面临证明我们的整个代码是否会引入window变量(并且可能非常有创意地完成,例如global["win" + _abc] = true其中_abc"dow"字符串import Cocoa class CustomTextFieldCell: NSTextFieldCell { // When the background changes (as a result of selection/deselection) switch appropriate colours override var backgroundStyle: NSBackgroundStyle { didSet { if (backgroundStyle == NSBackgroundStyle.Dark) { if self.textColor == NSColor.redColor() { self.textColor = NSColor.yellowColor() } } else if (backgroundStyle == NSBackgroundStyle.Light) { if (self.textColor == NSColor.yellowColor()) { self.textColor = NSColor.redColor() } } } } // When the colour changes, switch to a better alternative for the cell's current background override var textColor: NSColor? { didSet { if let colour = self.textColor { if backgroundStyle == NSBackgroundStyle.Dark { if self.textColor == NSColor.redColor() { self.textColor = NSColor.yellowColor() } } else if backgroundStyle == NSBackgroundStyle.Light { if (self.textColor == NSColor.yellowColor()) { self.textColor = NSColor.redColor() } } } } } } )。这是一个失败的原因。

兔子洞深入,阅读停止问题将让你了解停止问题面临多少差异。

答案 1 :(得分:2)

即使你的第一个函数也不纯,因为在JavaScript pictureBox1.ImageLocation = reader["imagem"].ToString(); pictureBox1.Height = pictureBox1.Image.Height; pictureBox1.Width = pictureBox1.Image.Width; /*mw and mh are the main width and main heigth, i used this in case the user selects another image, then the window returns to it's original size before changing again.*/ this.Height = mh; this.Width = mw; this.Height += pictureBox1.Image.Height; this.Width = pictureBox1.Image.Width + 16; if (this.Width < mw) { this.Width = mw; } this.CenterToParent(); 中可以调用ToNumber转换,如果*恰好是具有用户定义的x,则可以调用任意用户代码。 {1}}或toString方法,或者有人碰巧使用valueOf进行修补。

可悲的事实是,在JS中几乎没有任何可以被证明是纯粹的。唯一不会产生副作用的操作是Object.prototype===!==!&&||。对于编译器中的优化来说,这是一个巨大的问题,顺便说一句。

答案 2 :(得分:1)

示例代码。限制:只能*猜测*如果某些代码是纯的=只能给出提示,但不能保证

/* programmatically guess if some javascript code is pure or impure
npm install acorn acorn-walk
license is CC0-1.0 */

const acorn_parse = require("acorn").parse;
const acorn_walk = require("acorn-walk");



// the code to analyze
const content = `
  ['k1'].map(key => {
    const val = data[key];
    let local1;
    var local2 = 2;
    var local3, local4;
    global1[key] = val; // find me
    global2.key = val; // find me
    global3.push(val); // find me
    global4.pop(); // find me
    global5.shift(); // find me
    global6 = 'impure'; // find me
    const local7 = global7[1234];
    var local8 = global8.map(x => 2*x);
    var local9 = global9.filter(Boolean);
    const local10 = global10.pop(); // find me
    local1 = 'ok';
    local2.push('ok');
    return [key, val];
  })
`;



// method names for our educated guess
const write_method_set = new Set(['push', 'pop', 'shift']);
const read_method_set = new Set(['map', 'filter', 'reduce', 'forEach']);

const is_write_method = method_name => write_method_set.has(method_name);
const is_read_method = method_name => read_method_set.has(method_name);
const is_local = name => (name != undefined && local_var_set.has(name));
const get_src = node => content.substring(node.start, node.end);

function test_assign(node, left_name) {
  if (left_name == undefined) {
    console.log(`TODO implement: detect write access in:`);
    console.dir(node);
    return;
  }
  if (!is_local(left_name)) console.log(`impure write access to global ${left_name}: ${get_src(node)}`);
  else console.log(`pure? write access to local ${left_name}: ${get_src(node)}`);
}

function test_call(node, left_name, method_name) {
  if (left_name == undefined) {
    console.log(`TODO implement: detect write access in:`)
    console.dir(node);
    return;
  }
  if (is_read_method(method_name)) return console.log(`pure? access to ${left_name}: ${get_src(node)}`);
  if (!is_local(left_name)) {
    if (is_write_method(method_name)) console.log(`impure write access to global ${left_name}: ${get_src(node)}`);
    else console.log(`pure? access to global ${left_name}: ${get_src(node)}`);
  }
  else console.log(`pure? write access to local ${left_name}: ${get_src(node)}`)
}



const local_var_set = new Set();

// throws on syntax error
let ast = acorn_parse(content, { ecmaVersion: 2020, sourceType: "module" });

acorn_walk.full(ast, (node, state, type) => {
  if (node.type == 'VariableDeclaration') {
    node.declarations.forEach(d => {
      local_var_set.add(d.id.name);
      console.log(`declare local: ${d.id.name}`);
    });
  }
  else if (node.type == 'AssignmentExpression') {
    const left_name =
      node.left.type == 'Identifier' ? node.left.name :
      node.left.type == 'MemberExpression' ? node.left.object.name :
      undefined
    ;
    test_assign(node, left_name);
  }
  else if (node.type == 'CallExpression') {
    if (node.callee.object.type == 'ArrayExpression') return; // simply ignore
    const left_name =
      node.callee.type == 'MemberExpression' ? node.callee.object.name :
      undefined
    ;
    const method_name =
      node.callee.type == 'MemberExpression' ? node.callee.property.name :
      undefined
    ;
    test_call(node, left_name, method_name);
  }
  //else console.dir(node);
});

样本输出

$ node test.js | grep impure
impure write access to global global1: global1[key] = val
impure write access to global global2: global2.key = val
impure write access to global global3: global3.push(val)
impure write access to global global4: global4.pop()
impure write access to global global5: global5.shift()
impure write access to global global6: global6 = 'impure'
impure write access to global global10: global10.pop()