【发布时间】:2017-04-17 01:06:20
【问题描述】:
考虑一下 Haskell 中的这段代码:
let factorial n = if n < 2 then 1 else n * factorial (n-1) in factorial 3
我看到解释器以这样的顺序评估程序:
- 这是一个绑定。先评估定义,再评估“in”之后的部分。
- 这是一个定义。评估正文,然后将正文与名称相关联。
- 这是一个 lambda。捕捉环境,关闭并返回。
- 定义的主体已评估,现在将其写入名称。
- 计算定义,计算表达式的右侧部分。
- 计算表达式,返回结果。
我发现此模型存在以下问题:在第 3 步,当闭包捕获环境时,它对“阶乘”绑定一无所知。
我正在用 JavaScript 为类似 ML 的语言编写解释器,但我偶然发现了这个问题。例如,我的语言中的以下代码:
fac = \x -> if (== x, 0) { 1 } else { fac (- x, 1) } in fac 3
由于所描述的问题而无法工作。
其他语言的解释器如何解决这个问题?
这是解释器的代码供参考。
"use strict";
const grammar =
`
Expression "expression"
= Condition
/ Application
/ Lambda
/ Binding
/ Integer
/ String
/ Identifier
/ '(' expr: Expression ')' { return expr; }
_ "whitespace"
= [ \\t\\n\\r\\n]*
Integer "integer"
= [0-9]+ {
return { type: 'literal',
literalType: 'Integer',
value: parseInt(text(), 10)
};
}
String "string"
= '\"' value: ([^\"]* { return text(); } ) '\"' {
return { type: 'literal',
literalType: 'String',
value: value
};
}
Letter
= [a-zA-Z]
Identifier
= (Letter / '+' / '-' / '*' / '/' / '_' / '==' / '>' / '<')+ {
return {
type: 'identifier',
value: text()
}
}
Application
= id: Identifier _ args: ActualArguments {
return { type: 'application',
fun: id,
args: args
}
}
/ lambda: ('(' l: Lambda ')' { return l; }) _ args: ActualArguments {
return { type: 'application',
fun: lambda,
args: args
}
}
ActualArguments
= expr: Expression rest: (',' _ e: Expression { return e })* { return [expr].concat(rest); }
Lambda
= '\\\\' args: Arguments _ '->' _ body: Expression {
return { type: 'lambda',
args: args,
body: body
}
}
Arguments
= head: Identifier rest: (',' _ i: Identifier { return i; })* { return [head].concat(rest); }
Binding
= name: Identifier _ '=' _ def: Expression _ 'in' _ expr: Expression {
return {
type: 'binding',
name: name,
def: def,
expr: expr
}
}
Condition
= 'if' _ '(' _ cond: Expression _ ')' _ '{' _ expr1: Expression _ '}' expr2: ( _ 'else' _ '{' _ e: Expression _ '}' { return e; })? {
return {
type: 'condition',
condition: cond,
expr1,
expr2
}
}
`
const parser = peg.generate(grammar);
const std = {
'+': (arg1, arg2) => arg1 + arg2,
'-': (arg1, arg2) => arg1 - arg2,
'*': (arg1, arg2) => arg1 * arg2,
'/': (arg1, arg2) => arg1 / arg2,
'str': (arg1, arg2) => [arg1, arg2].join(""),
'>': (arg1, arg2) => arg1 > arg2,
'<': (arg1, arg2) => arg1 < arg2,
'==': (arg1, arg2) => arg1 === arg2,
'false': false,
'true': true,
'and': (arg1, arg2) => arg1 && arg2,
'or': (arg1, arg2) => arg1 || arg2
}
const makeLambda = (fun, parentEnv) => {
return (...args) => {
const env = Object.assign({}, parentEnv);
fun.args.forEach((el, i) => {
env[el.value] = args[i];
});
return _eval(fun.body, env);
}
}
const evalLiteral = (literal) => {
switch (literal.literalType) {
case 'Integer':
return parseInt(literal.value);
case 'String':
return String(literal.value);
default:
console.log('Literal type undefined');
return literal.value;
}
}
const _eval = (expr, parentEnv = std) => {
const env = Object.assign({}, parentEnv);
switch (expr.type) {
case 'application':
const fun = _eval(expr.fun, env);
const args = expr.args.map(arg => _eval(arg, env));
return fun.apply(null, args);
break;
case 'literal':
return evalLiteral(expr);
case 'identifier':
return env[expr.value];
case 'lambda':
return makeLambda(expr, env);
case 'binding':
env[expr.name.value] = _eval(expr.def, env);
return _eval(expr.expr, env);
case 'condition':
if (_eval(expr.condition, env)) {
return _eval(expr.expr1, env);
} else {
return _eval(expr.expr2, env);
}
}
}
const parseAndEval = (str) => {
try {
const parsed = parser.parse(str);
console.log(parsed);
return _eval(parsed);
} catch (e) {
if (e.name == "SyntaxError" ) {
return e.toString() +
" start: " + JSON.stringify(e.location.start) +
" end: " + JSON.stringify(e.location.end);
} else {
return e.toString();
}
}
}
【问题讨论】:
-
FWIW, in JavaScript 它为
factorial创建一个未初始化的绑定,然后计算右侧(实际上并没有使用绑定,因为函数是只是被定义,而不是运行),然后用结果值初始化绑定。因此,在评估初始化程序时绑定存在(未初始化)(因此可以处理早期引用错误,尽管 JavaScript 不这样做),并且在函数运行时具有值。 FWIW。 -
为了评估递归匿名函数(lambdas),你需要一个叫做 Y-Combinator 的东西。您可能会发现此链接很有帮助,kestas.kuliukas.com/YCombinatorExplained 和 stackoverflow.com/questions/93526/what-is-a-y-combinator
-
@zeronone 要使用 Y,您已经需要一个解释器。此外,一旦我们有了解释器,与仅使用递归绑定相比,Y 的效率非常低。
-
@AndrásKovács 我认为 zeronone 建议在预定义环境
const std = { ... , 'Y' : Yimpl }中添加 Y 作为预定义运算符,其中Yimpl可以在 JavaScript 中递归定义,我猜应该不会太低效. -
@chi 我不确定我是否可以在宿主语言中实现,因为宿主语言不知道我的语言环境。
标签: javascript haskell interpreter language-design