JavaScriptでプログラミング蚀語を実装する方法。 パヌト2通蚳

こんにちは JavaScriptプログラミング蚀語を実装するためのガむド、 PLチュヌトリアルの翻蚳の第2郚を玹介したす。


翻蚳者から


独自のプログラミング蚀語-λ蚀語 元の蚀語 -λanguageを䜜成したす。 䜜成のプロセスでは、再垰降䞋、コントロヌル転送スタむル、基本的な最適化手法など、倚くの興味深い手法を䜿甚したす。 むンタプリタの2぀のバヌゞョンが䜜成されたす-通垞のむンタプリタずCPSむンタプリタ、JavaScriptのトランスコンパむラ。


オリゞナルの䜜者は、有名なUglifyJSラむブラリJSコヌドを最小化およびフォヌマットするためのツヌルの䜜者であるMihai Bazonです。



PSむンタプリタずコンパむラにバグがありたす a() && b()たたはa() || b()ような匏で a() || b()䞡方の郚分が垞に実行されたす。 もちろん、これは間違っおa() &&挔算子に察しおa()停であるか、 ||に察しお停ではないためです。 、 b()倀は䜕の圹割も果たしたせん。 これを修正するのは難しくありたせん。


シンプルな通蚳


前のパヌトでは、 InputStream 、 TokenStreamおよびparse 3぀の関数をTokenStreamしたした。 コヌドからASTを取埗するには、次のコヌドを䜿甚したす。


 var ast = parse(TokenStream(InputStream(code))); 

むンタプリタの蚘述は、パヌサヌよりも簡単です。ツリヌを再垰的に走査し、通垞の順序で匏を実行するだけです。


コンテキスト Environment 


適切なコヌドを実行するには、コンテキストが必芁です。これは、特定の堎所にあるすべおの倉数を含むオブゞェクトです。 evaluate関数の匕数ずしお枡されたす。


ラムダノヌドに入るたびに、コンテキストに新しい倉数を远加する必芁がありたす-関数の匕数。 匕数が倖郚ブロックからの倉数ず重耇する堎合、関数を終了した埌に倉数の叀い倀を埩元する必芁がありたす。


これを行う最も簡単な方法は、JavaScriptプロトタむプ継承を䜿甚するこずです。 新しい関数を入力するず、新しいコンテキストを䜜成し、倖郚コンテキストをそのプロトタむプずしお蚭定し、新しいコンテキストで関数を呌び出したす。 このおかげで、私たちには䜕もありたせん-倖郚のコンテキストでは、その倉数はすべお残りたす。


Environmentオブゞェクトの実装は次のずおりです。


 function Environment(parent) { this.vars = Object.create(parent ? parent.vars : null); this.parent = parent; } Environment.prototype = { extend: function() { return new Environment(this); }, lookup: function(name) { var scope = this; while (scope) { if (Object.prototype.hasOwnProperty.call(scope.vars, name)) return scope; scope = scope.parent; } }, get: function(name) { if (name in this.vars) return this.vars[name]; throw new Error("Undefined variable " + name); }, set: function(name, value) { var scope = this.lookup(name); //          if (!scope && this.parent) throw new Error("Undefined variable " + name); return (scope || this).vars[name] = value; }, def: function(name, value) { return this.vars[name] = value; } }; 

Environmentオブゞェクトには、倖郚コンテキストを指すparentフィヌルドがありたす。 グロヌバルコンテキストの堎合はnullになりたす。 varsフィヌルドがあり、このコンテキストに属するすべおの倉数がありたす。 グロヌバルコンテキストの堎合、空のオブゞェクト Object.create(null) および非グロヌバルコンテキストの芪コンテキストの倉数のコピヌ Object.create(parent.vars) にすぐに等しくなりたす。


いく぀かの方法がありたす



evaluate関数


Environmentオブゞェクトができたので、次に䞻な問題の解決に進みたす。 この関数は、送信ノヌドのタむプに応じおいく぀かのアクションを実行する倧きなswitchブロックになりたす。


 function evaluate(exp, env) { switch (exp.type) { 

リテラルの堎合、単にその倀を返したす。


  case "num": case "str": case "bool": return exp.value; 

倉数はコンテキストから取埗されたす倉数の名前はvalueフィヌルドに含たれおいvalue 。


  case "var": return env.get(exp.value); 

割り圓おるには、巊偎に倉数の名前ノヌドvar があるこずを確認する必芁がありたす。 そうでない堎合は、単に䟋倖をスロヌしたす倉数以倖ぞの代入はサポヌトしおいたせん。 次に、 env.setを䜿甚しお倉数の倀を蚭定したす。 匏の右蟺は、 evaluateする再垰呌び出しを䜿甚しお蚈算する必芁があるこずに泚意しおください。


  case "assign": if (exp.left.type != "var") throw new Error("Cannot assign to " + JSON.stringify(exp.left)); return env.set(exp.left.value, evaluate(exp.right, env)); 

タむプがbinaryノヌドの堎合、䞡方のオペランドに挔算子を適甚する必芁がありたす。 apply_op関数は埌で蚘述したす。 たた、匏の䞡方の郚分に察しおevaluateを呌び出したす。


  case "binary": return apply_op(exp.operator, evaluate(exp.left, env), evaluate(exp.right, env)); 

lambda型のノヌドは通垞のJavaScriptクロヌゞャヌを返すため、JavaScriptからでも通垞の関数のように呌び出すこずができたす。 make_lambda関数を远加したした。これに぀いおは埌で説明したす。


  case "lambda": return make_lambda(env, exp); 

ifノヌドの実行if非垞に簡単です。最初に条件の倀を芋぀けたす。 falseでない堎合は、 thenブランチの倀を返したす。 それ以倖の堎合、 elseブランチがある堎合、その倀、たたはfalse 


  case "if": var cond = evaluate(exp.cond, env); if (cond !== false) return evaluate(exp.then, env); return exp.else ? evaluate(exp.else, env) : false; 

progノヌドは䞀連の匏です。 すべおの匏を順番に実行し、埌者の倀を取埗したす空のシヌケンスの倀はfalse 。


  case "prog": var val = false; exp.prog.forEach(function(exp){ val = evaluate(exp, env) }); return val; 

callタむプのノヌドの堎合、関数を呌び出す必芁がありたす。 その前に、関数自䜓の倀を芋぀け、すべおの匕数の倀を芋぀け、 applyを䜿甚しお関数を呌び出しapply 。


  case "call": var func = evaluate(exp.func, env); return func.apply(null, exp.args.map(function(arg){ return evaluate(arg, env); })); 

ここには到達したせんが、パヌサヌに新しいノヌドタむプを远加し、むンタヌプリタヌに远加するのを忘れた堎合は、次のようにしたす。


  default: throw new Error("I don't know how to evaluate " + exp.type); } } 

これが通蚳の䞻芁郚分でした。 䞊蚘では、ただ実装しおいない2぀の関数を䜿甚したので、始めたしょう。


apply_op 


 function apply_op(op, a, b) { function num(x) { if (typeof x != "number") throw new Error("Expected number but got " + x); return x; } function div(x) { if (num(x) == 0) throw new Error("Divide by zero"); return x; } switch (op) { case "+" : return num(a) + num(b); case "-" : return num(a) - num(b); case "*" : return num(a) * num(b); case "/" : return num(a) / div(b); case "%" : return num(a) % div(b); case "&&" : return a !== false && b; case "||" : return a !== false ? a : b; case "<" : return num(a) < num(b); case ">" : return num(a) > num(b); case "<=" : return num(a) <= num(b); case ">=" : return num(a) >= num(b); case "==" : return a === b; case "!=" : return a !== b; } throw new Error("Can't apply operator " + op); } 

挔算子のタむプず匕数を受け取りたす。 シンプルで盎感的なswitch 。 倉数のような任意の倀をずるこずができるJavaScriptずは異なり、意味をなさないものもです。 算術挔算子のオペランドは数倀であり、れロによる陀算を蚱可しないこずが必芁です。 文字列に぀いおは、埌で䜕かを考えたす。


make_lambda 


 function make_lambda(env, exp) { function lambda() { var names = exp.vars; var scope = env.extend(); for (var i = 0; i < names.length; ++i) scope.def(names[i], i < arguments.length ? arguments[i] : false); return evaluate(exp.body, scope); } return lambda; } 

䞊蚘のように、枡されたコンテキストずAST関数を䜿甚する通垞のJavaScript関数を返したす。 すべおの䜜業は、関数自䜓が呌び出されたずきにのみ実行されたす。コンテキストが䜜成され、匕数が蚭定されたす十分でない堎合はfalseになりfalse 。 次に、関数の本䜓が新しいコンテキストで実行されたす。


ネむティブ関数


ご芧のずおり、私たちの蚀語のJavaScriptずやり取りする方法はありたせんでした。 以前はprintおよびprintln関数を䜿甚しおいたしたが、どこにも定矩しおいたせんでした。 JavaScriptで蚘述し、グロヌバルコンテキストに远加するだけです。


そのようなコヌドの䟋を次に瀺したす。


 //  -   var code = "sum = lambda(x, y) x + y; print(sum(2, 3));"; // ,  parse  TokenStream,      InputStream var ast = parse(TokenStream(InputStream(code))); //    var globalEnv = new Environment(); //  ""  "print" globalEnv.def("print", function(txt){ console.log(txt); }); //  evaluate(ast, globalEnv); //  5 

党コヌド


これたでに䜜成したすべおのコヌドをダりンロヌドできたす 。 NodeJSを䜿甚しお起動できたす。 コヌドを暙準ストリヌムに枡すだけです


 echo 'sum = lambda(x, y) x + y; println(sum(2, 3));' | node lambda-eval1.js 

コヌド䟋


私たちのプログラミング蚀語は、シンプルですが、䞀般的にコンピュヌタヌで解決できる問題を理論的には解決できたす。 これは、私よりも賢い男-アロンゟチャヌチずアランチュヌリング-が䞀床、λ蚈算ラムダ蚈算がチュヌリングマシンに盞圓し、λ蚀語がλ蚈算を実装するこずを蚌明したためです。


これは、たずえ私たちの蚀語に機䌚がなくおも、すでに持っおいるものを䜿っお同じこずを実珟できるこずを意味したす。 たたは、これが困難な堎合は、別の蚀語のむンタヌプリタヌを䜜成できたす。


サむクル


再垰があればルヌプは問題になりたせん。 再垰の䞊に実装されたルヌプの䟋をすでに瀺したした。 もう䞀床詊しおみたしょう。


 print_range = λ(a, b) if a <= b { print(a); if a + 1 <= b { print(", "); print_range(a + 1, b); } else println(""); }; print_range(1, 10); 

しかし、ここで問題がありたす。たずえば、反埩回数を1000に増やすず、600の埌に「Maximum call stack size exceeded」ずいう゚ラヌが衚瀺されたす。これは、むンタヌプリタヌが再垰的で再垰が最倧深床に達するために発生したす。


これは深刻な問題ですが、解決策がありたす。 繰り返しのために forたたはwhile 新しいコンストラクトを远加したいのですが、それらを䜿わずにやっおみたしょう。 再垰はきれいに芋えるので、それを残したしょう。 この制限を回避する方法に぀いおは埌で説明したす。


デヌタ構造その欠劂


λ蚀語には、数倀、文字列、ブヌル型の3皮類のデヌタがありたす。 配列やオブゞェクトなどの耇雑なタむプを䜜成するこずは䞍可胜に思えるかもしれたせん。 しかし、これはtatではありたせん。もう1぀のタむプがありたすfunction。 λ蚈算に埓うず、継承を含めお、オブゞェクトを含む任意のデヌタ構造を䜜成できるこずがわかりたす。


リストの䟋で瀺したす。 2぀の倀を含むオブゞェクトを䜜成するcons関数があるずしたす。 このオブゞェクトを「セル」たたは「ペア」ず呌びたしょう。 倀の1぀に「car」、もう1぀に「cdr」ずいう名前を付けたす。 Lispではそう呌ばれおいるからです。 オブゞェクト「セル」がある堎合、関数carおよびcdrを䜿甚しおその倀を取埗できたす。


 x = cons(10, 20); print(car(x)); #  10 print(cdr(x)); #  20 

これで、リストを簡単に定矩できたす。


リストは、「car」の最初の芁玠ず「cdr」の残りの芁玠を含むペアです。 しかし、 `cdr`には1぀の倀しか含めるこずができたせん この倀はリストになりたす。 リストは、「car」の最初の芁玠ず「cdr」の残りの芁玠を含むペアです。 しかし、 `cdr`には1぀の倀しか含めるこずができたせん この倀はリストになりたす。 [...]

これは再垰的なデヌタ型です。 しかし、1぀の問題が残っおいたす。い぀停止する必芁があるのでしょうか。 論理的には、 cdrが空のリストになったら停止する必芁がありたす。 しかし、空のリストずは䜕ですか これを行うには、「NIL」ずいう新しいオブゞェクトを远加したしょう。 ペアずしお䜿甚できたす cdrずcdrを䜿甚できたすが、結果はNILそのものになりたす。 それでは、アむテム1、2、3、4、5のリストを䜜成したしょう。


 x = cons(1, cons(2, cons(3, cons(4, cons(5, NIL))))); print(car(x)); # 1 print(car(cdr(x))); # 2  Lisp  . cadr print(car(cdr(cdr(x)))); # 3 caddr print(car(cdr(cdr(cdr(x))))); # 4 cadddr print(car(cdr(cdr(cdr(cdr(x)))))); # 5     . 

このための特別な構文がない堎合、ひどく芋えたす。 しかし、これは既存のλ蚀語機胜を䜿甚しお実行できるこずを瀺したかっただけです。 実装は次のずおりです。


 cons = λ(a, b) λ(f) f(a, b); car = λ(cell) cell(λ(a, b) a); cdr = λ(cell) cell(λ(a, b) b); NIL = λ(f) f(NIL, NIL); 

この方法で䜜成されたcons / car / cdrを最初に芋たずき、 ifが1぀も必芁ないこずに驚きたしたただし、元のλ蚈算にないずいう事実を考えるず、これは奇劙なこずではありたせん。 もちろん、この方法でこれを行うプログラミング蚀語はありたせん。なぜなら、それは非垞に非効率的だからです。しかし、λ蚈算をそれほど矎しくしたせん。 明確な蚀語では、このコヌドは次のこずを行いたす。



 cons = λ(a, b) λ(f) f(a, b); car = λ(cell) cell(λ(a, b) a); cdr = λ(cell) cell(λ(a, b) b); NIL = λ(f) f(NIL, NIL); x = cons(1, cons(2, cons(3, cons(4, cons(5, NIL))))); println(car(x)); # 1 println(car(cdr(x))); # 2 println(car(cdr(cdr(x)))); # 3 println(car(cdr(cdr(cdr(x))))); # 4 println(car(cdr(cdr(cdr(cdr(x)))))); # 5 

リストには、再垰的に実装でき、論理的に芋える倚くのアルゎリズムがありたす。 たずえば、次の関数は、リストアむテムごずに枡された関数を呌び出したす。


 foreach = λ(list, f) if list != NIL { f(car(list)); foreach(cdr(list), f); }; foreach(x, println); 

そしお、ここに番号の範囲のリストを䜜成する別のものがありたす


 range = λ(a, b) if a <= b then cons(a, range(a + 1, b)) else NIL; #     1  8 foreach(range(1, 8), λ(x) println(x * x)); 

䞊蚘で実装したリストは䞍倉ですリストの䜜成埌にcdrたたはcdr倉曎するこずはできたせん。 ほずんどのLispには、ペアを倉曎する機胜がありたす。 Schemeでは、これらはset-car!ず呌ばset-car! / set-cdr! 。 Common Lispでは、 rplaca / rplacd 。 今回は、Schemeの名前を䜿甚したす。


 cons = λ(x, y) λ(a, i, v) if a == "get" then if i == 0 then x else y else if i == 0 then x = v else y = v; car = λ(cell) cell("get", 0); cdr = λ(cell) cell("get", 1); set-car! = λ(cell, val) cell("set", 0, val); set-cdr! = λ(cell, val) cell("set", 1, val); #  NIL     NIL = cons(0, 0); set-car!(NIL, NIL); set-cdr!(NIL, NIL); ## : x = cons(1, 2); println(car(x)); # 1 println(cdr(x)); # 2 set-car!(x, 10); set-cdr!(x, 20); println(car(x)); # 10 println(cdr(x)); # 20 

これは、可倉デヌタ構造を実装できるこずを瀺しおいたす。 私はそれがどのように機胜するかに぀いお深くは述べたせんが、それはコヌドから明らかです。


さらに進んでオブゞェクトを実装するこずもできたすが、構文を倉曎しないず実行が困難になりたす。 別の方法は、トヌクナむザヌ/パヌサヌに新しい構文を実装し、むンタヌプリタヌに凊理を远加するこずです。 すべおの䞻芁なプログラミング蚀語がこれを行い、通垞のパフォヌマンスを達成する必芁がありたす。 蚘事の次の郚分で新しい構文を远加したす。


[翻蚳者からラムダ蚈算に興味があるなら、このトピックに特化したHabréのクヌルな蚘事がありたす JavaScriptのラムダ蚈算 ]。


新しい構文構成


λ蚀語には、かなりの構文構造がありたす。 たずえば、新しい倉数を盎接远加する方法はありたせん。 前にも蚀ったように、IIFEを䜿甚する必芁があるため、次のようになりたす。


 (λ(x, y){ (λ(z){ ## it gets worse when one of the vars depends on another print(x + y + z); })(x + y); })(2, 3); 

letキヌワヌドを远加したす。 これにより、次のような蚘述が可胜になりたす。


 let (x = 2, y = 3, z = x + y) print(x + y + z); 

letブロック内の各倉数に぀いお、同じブロックからでも以前の倉数が利甚可胜でなければなりたせん。 したがっお、䞊蚘のコヌドは次のコヌドず同等になりたす。


 (λ(x){ (λ(y){ (λ(z){ print(x + y + z); })(x + y); })(3); })(2); 

これらの倉曎は、パヌサヌで盎接行うこずができ、むンタヌプリタヌでの倉曎は䞍芁です。 新しいletノヌドを远加する代わりに、それをcallノヌドずlambda倉えるこずができたす。 これは、蚀語に意味的な倉曎を加えなかったこずを意味したす-これは「構文糖」ず呌ばれ、これを以前に存圚したASTノヌドに倉換する操䜜は「シュガヌレス」元「脱糖」ず呌ばれたす。


ただし、ずにかくパヌサヌを倉曎する必芁がありたす。 より効率的に解釈できるため、新しい「let」ノヌドを远加したしょうクロヌゞャヌを䜜成しおすぐに呌び出す必芁はなく、コンテキストをコピヌしお倉曎するだけです。


たた、Schemeにあった「let named」のサポヌトを远加したす。 ルヌプの䜜成が簡単になりたす。


 print(let loop (n = 10) if n > 0 then n + loop(n - 1) else 0); 

これは、10 + 9 + ... + 0の合蚈をカりントする「再垰的な」ルヌプです。以前は、次のようにする必芁がありたした。


 print((λ(loop){ loop = λ(n) if n > 0 then n + loop(n - 1) else 0; loop(10); })()); 

たた、これを簡単にするために、「名前付き関数」の構文を远加したす。 次のようになりたす。


 print((λ loop (n) if n > 0 then n + loop(n - 1) else 0) (10)); 

このために行う必芁がある倉曎



パヌサヌの倉曎


最初に、トヌクナむザヌの小さな倉曎、 letキヌワヌドをキヌワヌドのリストに远加したす。


 var keywords = " let if then else lambda λ true false "; 

オプションの名前を読み取るparse_lambda関数を倉曎したす。


 function parse_lambda() { return { type: "lambda", name: input.peek().type == "var" ? input.next().value : null, //   vars: delimited("(", ")", ",", parse_varname), body: parse_expression() }; } 

次に、 let匏を解析する関数を远加したす。


 function parse_let() { skip_kw("let"); if (input.peek().type == "var") { var name = input.next().value; var defs = delimited("(", ")", ",", parse_vardef); return { type: "call", func: { type: "lambda", name: name, vars: defs.map(function(def){ return def.name }), body: parse_expression(), }, args: defs.map(function(def){ return def.def || FALSE }) }; } return { type: "let", vars: delimited("(", ")", ",", parse_vardef), body: parse_expression(), }; } 

これは2぀のケヌスを凊理したす。 let埌にvarトヌクンがある堎合、これは名前付きのletです。 さらに、括匧で囲たれ、カンマで区切られおいるため、 delimited関数を䜿甚しお定矩のリストを読み取り、 parse_vardef瀺すparse_vardef関数を䜿甚したす。 次に、タむプcallノヌドを返しcall 。これは、IIFEずいう名前の関数をすぐに呌び出したす。 関数の匕数はletで定矩された倉数であり、 callノヌドは倀を匕数ずしお枡したす。 そしお、もちろん、関数の本䜓はparse_expression()を䜿甚しおparse_expression()たす。


これが単玔なletである堎合、 varsフィヌルドずbodyフィヌルドを持぀let型のノヌドを返したす。 varsフィヌルドには、次の圢匏の倉数の配列が含たれたす { name: VARIABLE, def: AST } 、これらは次の関数によっお解析されたす


 function parse_vardef() { var name = parse_varname(), def; if (is_op("=")) { input.next(); def = parse_expression(); } return { name: name, def: def }; } 

たた、新しいタむプの匏のチェックをparse_atom関数にparse_atomたす。


 //      parse_if if (is_kw("let")) return parse_let(); 

通蚳の倉曎


ASTの構造を叀いタむプのノヌドに「クラッキング」する代わりに倉曎するこずにしたため、むンタヌプリタヌに新しいロゞックの凊理を远加する必芁がありたす。


オプションの関数名のサポヌトを远加するには、 make_lambda関数を倉曎したす。


 function make_lambda(env, exp) { if (exp.name) { //  env = env.extend(); //  env.def(exp.name, lambda); //  } //  function lambda() { var names = exp.vars; var scope = env.extend(); for (var i = 0; i < names.length; ++i) scope.def(names[i], i < arguments.length ? arguments[i] : false); return evaluate(exp.body, scope); } return lambda; } 

関数に名前がある堎合、クロヌゞャヌを䜜成するずきに、コンテキストのコピヌを䜜成し、関数をコンテキストに远加したす。 残りは同じたたです。


最埌に、 let型のノヌドを凊理するために、むンタヌプリタヌに次のケヌスを远加したす。


 case "let": exp.vars.forEach(function(v){ var scope = env.extend(); scope.def(v.name, v.def ? evaluate(v.def, env) : false); env = scope; }); return evaluate(exp.body, env); 

各倉数に察しお、新しい倉数が远加される新しいコンテキストが䜜成されるこずに泚意しおください。 その埌、最埌のコンテキストで本䜓を実行するだけです。


䟋


 println(let loop (n = 100) if n > 0 then n + loop(n - 1) else 0); let (x = 2, y = x + 1, z = x + y) println(x + y + z); #   ..     let # print(x + y + z); let (x = 10) { let (x = x * 2, y = x * x) { println(x); ## 20 println(y); ## 400 }; println(x); ## 10 }; 


— .


. , , , . JavaScript λ.


:


 globalEnv.def("fibJS", function fibJS(n){ if (n < 2) return n; return fibJS(n - 1) + fibJS(n - 2); }); globalEnv.def("time", function(fn){ var t1 = Date.now(); var ret = fn(); var t2 = Date.now(); println("Time: " + (t2 - t1) + "ms"); return ret; }); 

time , , , .


 fib = λ(n) if n < 2 then n else fib(n - 1) + fib(n - 2); print("fib(10): "); time( λ() println(fib(10)) ); print("fibJS(10): "); time( λ() println(fibJS(10)) ); println("---"); 

, Google Chrome, n (27), λ , , JS 4 . , , .


λ JavaScript. , for / while ; JS. ? JS , .


, , JavaScript, , JavaScript.


, , . , .


おわりに


, . , - ; , , ? — JavaScript. , JavaScript — ? , , JavaScript, , , . JavaScript ( , ).


, , Lisp — : //. , , .. Lisp . Lisp let , , Lisp.


: JavaScript. 3: CPS-



Source: https://habr.com/ru/post/J443812/


All Articles