Bagaimana menerapkan bahasa pemrograman dalam JavaScript. Bagian 2: Penerjemah

Halo Saya hadir untuk Anda bagian kedua dari terjemahan saya panduan untuk menerapkan bahasa pemrograman JavaScript Anda - PL Tutorial .


Dari penerjemah


Kami akan membuat bahasa pemrograman kami sendiri - bahasa λ (dalam bahasa asli - λ bahasa ). Dalam proses pembuatan, kami akan menggunakan banyak teknik menarik, seperti keturunan rekursif, kontrol gaya transfer, dan teknik optimasi dasar. Dua versi penerjemah akan dibuat - penerjemah reguler dan CPS, trans-kompiler dalam JavaScript.


Penulis aslinya adalah Mihai Bazon , penulis perpustakaan UglifyJS yang terkenal (alat untuk meminimalkan dan memformat kode JS).



PS Ada bug di interpreter dan compiler: dalam ekspresi seperti a() && b() atau a() || b() a() || b() kedua bagian selalu dieksekusi. Ini, tentu saja, salah karena a() salah untuk operator && , atau tidak salah untuk || , maka nilai b() tidak memainkan peran apa pun. Ini tidak sulit untuk diperbaiki.


Penerjemah sederhana


Di bagian sebelumnya, kami menulis 3 fungsi: InputStream , TokenStream dan parse . Untuk mendapatkan AST dari kode, kami menggunakan kode berikut:


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

Menulis penerjemah lebih mudah daripada parser: kita hanya melintasi pohon secara rekursif dan mengeksekusi ekspresi dalam urutan normal mereka.


Konteks ( Environment )


Untuk eksekusi kode yang tepat, kita memerlukan konteks - objek yang berisi semua variabel di lokasi tertentu. Ini akan diteruskan sebagai argumen ke fungsi evaluate .


Setiap kali kita memasukkan node lambda , kita harus menambahkan variabel baru ke argumen konteks - fungsi. Jika argumen tumpang tindih variabel dari blok eksternal, kita harus mengembalikan nilai variabel lama setelah keluar dari fungsi.


Cara termudah untuk melakukan ini adalah dengan menggunakan warisan prototipe JavaScript. Ketika kita memasukkan fungsi baru, kita membuat konteks baru, mengatur konteks eksternal sebagai prototipe, dan memanggil fungsi dalam konteks baru. Berkat ini, kami tidak memiliki apa-apa - dalam konteks eksternal semua variabelnya akan tetap ada.


Berikut ini adalah implementasi objek 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; } }; 

Objek Environment memiliki bidang parent yang menunjuk ke konteks eksternal. Ini akan menjadi null untuk konteks global. Ini memiliki bidang vars di mana ada semua variabel yang termasuk dalam konteks ini. Untuk konteks global, itu langsung sama dengan objek kosong ( Object.create(null) ) dan salinan variabel dari konteks induk ( Object.create(parent.vars) ) untuk yang non-global.


Ini memiliki beberapa metode:


  • extend() - salin konteks saat ini.
  • lookup(name) - menemukan konteks di mana variabel bernama name didefinisikan.
  • get(name) - dapatkan nilai dari variabel bernama name . Melempar pengecualian jika variabel belum ditentukan.
  • set(name, value) - mengatur nilai suatu variabel. Metode ini mencari konteks di mana variabel didefinisikan. Jika tidak didefinisikan, dan kami tidak dalam konteks global, pengecualian akan dilemparkan.
  • def(name, value) - membuat (atau tumpang tindih atau menimpa) variabel dalam konteks saat ini.

evaluate fungsi


Sekarang kita memiliki objek Environment , kita dapat melanjutkan untuk menyelesaikan masalah utama. Fungsi ini akan menjadi blok switch besar, yang akan melakukan beberapa tindakan tergantung pada jenis node yang ditransmisikan:


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

Untuk literal, kami cukup mengembalikan nilainya:


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

Variabel diambil dari konteks (nama variabel terkandung dalam bidang value ):


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

Untuk menetapkan, kita harus memastikan bahwa di sisi kiri kita memiliki nama variabel (node var ). Jika tidak, maka kami hanya melempar pengecualian (kami tidak mendukung penugasan ke variabel selain variabel). Selanjutnya, kita mengatur nilai variabel menggunakan env.set . Perhatikan bahwa sisi kanan ekspresi harus dihitung menggunakan panggilan rekursif untuk 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)); 

Untuk simpul tipe binary kita harus menerapkan operator untuk kedua operan. Kami akan menulis fungsi apply_op nanti. Juga, kami memanggil evaluate untuk kedua bagian ekspresi:


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

Sebuah simpul tipe lambda akan mengembalikan penutupan JavaScript normal, sehingga dapat disebut seperti fungsi biasa bahkan dari JavaScript. Saya menambahkan fungsi make_lambda , yang akan saya tunjukkan nanti:


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

Eksekusi dari simpul if cukup sederhana: pertama kita temukan nilai kondisinya. Jika tidak salah, maka kembalikan nilai cabang then . Kalau tidak, jika ada cabang else , maka nilainya, atau 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; 

Node prog adalah urutan ekspresi. Kami cukup menjalankan semua ekspresi secara berurutan dan mengambil nilai yang terakhir (nilai urutan kosong false ):


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

Untuk simpul call tipe call kita perlu memanggil fungsi. Sebelum itu, kita akan menemukan nilai fungsi itu sendiri, menemukan nilai semua argumen dan memanggil fungsi menggunakan apply :


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

Kami tidak akan pernah sampai di sini, tetapi jika kami menambahkan tipe simpul baru ke parser dan lupa menambahkannya ke juru bahasa:


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

Ini adalah bagian utama dari penerjemah. Di atas, kami menggunakan dua fungsi yang belum kami implementasikan, jadi mari kita mulai:


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); } 

Ia menerima jenis dan argumen operator. switch sederhana dan intuitif. Tidak seperti JavaScript, yang dapat mengambil nilai apa pun, seperti variabel, bahkan yang tidak masuk akal. Kami mengharuskan operan operator aritmatika menjadi angka dan tidak mengizinkan pembagian dengan nol. Untuk string, kita akan menemukan sesuatu nanti.


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; } 

Seperti yang Anda lihat di atas, ia mengembalikan fungsi JavaScript biasa yang menggunakan konteks yang diteruskan dan fungsi AST. Semua pekerjaan dilakukan hanya ketika fungsi itu sendiri disebut: konteks dibuat, argumen ditetapkan (jika mereka tidak cukup, mereka menjadi false ). Selanjutnya, tubuh fungsi hanya dieksekusi dalam konteks baru.


Fungsi asli


Seperti yang Anda lihat, kami tidak punya cara untuk berinteraksi dengan JavaScript dari bahasa kami. Saya dulu menggunakan fungsi print dan println , tapi saya tidak mendefinisikannya di mana pun. Kita perlu menulisnya dalam JavaScript dan hanya menambahkannya ke konteks global.


Berikut ini contoh kode tersebut:


 //  -   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 

Seluruh kode


Anda dapat mengunduh semua kode yang telah kami tulis selama ini. Itu dapat diluncurkan menggunakan NodeJS. Cukup berikan kode ke aliran standar:


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

Contoh kode


Bahasa pemrograman kami, meskipun sederhana, dapat (secara teoritis) menyelesaikan masalah yang dapat diselesaikan oleh komputer sama sekali. Ini karena beberapa pria lebih pintar daripada saya - Alonzo Church dan Alan Turing - pernah membuktikan bahwa λ-calculus (lambda calculus) setara dengan mesin Turing, dan bahasa kami mengimplementasikan λ-calculus.


Ini berarti bahwa meskipun bahasa kita tidak memiliki peluang, kita semua dapat mewujudkannya menggunakan apa yang sudah kita miliki. Atau, jika ini sulit dilakukan, kita dapat menulis penerjemah untuk bahasa lain di sini.


Siklus


Loop tidak menjadi masalah jika kita mengalami rekursi. Saya sudah menunjukkan contoh loop diimplementasikan di atas rekursi. Ayo kita coba lagi.


 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); 

Tetapi di sini kita memiliki masalah: jika kita menambah jumlah iterasi, katakanlah menjadi 1000. Sebagai contoh, saya mendapatkan kesalahan “Ukuran stack panggilan maksimum terlampaui” setelah 600. Ini terjadi karena interpreter bersifat rekursif dan rekursi mencapai kedalaman maksimum.


Ini masalah serius, tetapi ada solusinya. Saya ingin menambahkan konstruksi baru untuk iterasi ( for atau while ), tapi mari kita coba lakukan tanpa itu. Rekursi itu terlihat indah, jadi mari kita tinggalkan. Kita akan lihat nanti bagaimana cara mengatasi batasan ini.


Struktur data (ketiadaan)


Ada tiga jenis data dalam bahasa λ kami: angka, string, dan tipe Boolean. Tampaknya Anda tidak dapat membuat tipe yang kompleks, seperti array, atau objek. Tapi ini bukan tat, kita punya satu lagi jenis: fungsi. Ternyata jika kita mengikuti kalkulus λ, maka kita dapat membuat struktur data apa pun, termasuk objek, bahkan dengan warisan.


Saya akan menunjukkannya pada contoh daftar. Mari kita bayangkan bahwa kita memiliki fungsi cons yang menciptakan objek yang berisi dua nilai. Sebut objek ini "sel" atau "pasangan." Kami akan memberi nama salah satu nilai "mobil" dan yang lainnya "cdr". Hanya karena mereka disebut dalam Lisp. Sekarang, jika kita memiliki objek "sel", maka kita bisa mendapatkan nilainya menggunakan fungsi car dan cdr :


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

Sekarang kita dapat dengan mudah mendefinisikan daftar:


Daftar adalah pasangan yang mengandung elemen pertama di `car` dan elemen lainnya di` cdr`. Tetapi `cdr` hanya dapat berisi satu nilai! Nilai ini akan menjadi daftar. Daftar adalah pasangan yang mengandung elemen pertama di `car` dan elemen lainnya di` cdr`. Tetapi `cdr` hanya dapat berisi satu nilai! Nilai ini akan menjadi daftar. [...]

Ini adalah tipe data rekursif. Tetapi satu masalah tetap ada: kapan Anda harus berhenti? Secara logis, kita harus berhenti ketika cdr adalah daftar kosong. Tetapi apa itu daftar kosong? Untuk melakukan ini, mari kita tambahkan objek baru yang disebut "NIL". Ini dapat digunakan sebagai pasangan (kita dapat menggunakan car dan cdr di atasnya, tetapi hasilnya adalah NIL itu sendiri). Sekarang mari kita buat daftar item 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     . 

Ini terlihat mengerikan ketika tidak ada sintaks khusus untuk ini. Tapi saya hanya ingin menunjukkan bahwa ini dapat dilakukan dengan menggunakan fitur bahasa λ yang ada. Berikut implementasinya:


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

Ketika saya pertama kali melihat cons / car / cdr dibuat dengan cara ini, saya terkejut bahwa mereka tidak memerlukan satu if (tapi ini aneh mengingat fakta bahwa itu tidak ada dalam λ-calculus asli). Tentu saja, tidak ada bahasa pemrograman yang melakukan hal ini, karena ini sangat tidak efisien, tetapi tidak membuat λ-calculi kurang indah. Dalam bahasa yang jelas, kode ini melakukan hal berikut:


  • fungsi cons mengambil dua nilai ( a dan b ) dan mengembalikan fungsi yang menampungnya. Fungsi itu adalah objek utama pasangan. Dia mengambil argumen dan menyebutnya untuk kedua nilai pasangan.
  • Fungsi car memanggil argumen yang diteruskan, melewati fungsi yang mengembalikan argumen pertama.
  • fungsi cdr melakukan hal yang sama dengan fungsi car , tetapi dengan satu-satunya perbedaan adalah bahwa fungsi yang dikirimkan mengembalikan argumen kedua.
  • fungsi NIL bekerja sama dengan cons , tetapi mengembalikan pasangan dengan dua nilai yang sama dengan NIL.

 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 

Ada banyak algoritma pada daftar yang dapat diimplementasikan secara rekursif dan terlihat logis. Misalnya, berikut adalah fungsi yang memanggil fungsi yang diteruskan untuk setiap item daftar:


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

Dan di sini ada satu lagi yang membuat daftar untuk sejumlah angka:


 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)); 

Daftar yang kami terapkan di atas tidak dapat diubah (kami tidak dapat mengubah car atau cdr setelah daftar dibuat). Sebagian besar Lisp memiliki fungsi untuk mengubah pasangan. Dalam Skema mereka disebut set-car! / set-cdr! . Dalam Common Lisp, rplaca / rplacd . Kali ini kami menggunakan nama-nama dari Skema:


 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 

Ini menunjukkan bahwa kita dapat menerapkan struktur data yang bisa berubah. Saya tidak akan masuk jauh ke cara kerjanya, jelas dari kode.


Kita dapat melangkah lebih jauh dan mengimplementasikan objek, tetapi tanpa perubahan dalam sintaks akan sulit untuk dilakukan. Cara lain adalah dengan mengimplementasikan sintaks baru dalam tokenizer / parser dan menambahkan pemrosesan mereka dalam interpreter. Semua bahasa pemrograman utama melakukan ini, dan perlu untuk mencapai kinerja normal. Kami akan menambahkan sintaks baru di bagian artikel selanjutnya.


[Dari penerjemah: jika Anda tertarik dengan lambda calculus, ada artikel keren tentang Habré yang didedikasikan untuk topik ini: Lambda calculus dalam JavaScript .]


Konstruksi sintaksis baru


Bahasa λ kami memiliki beberapa konstruksi sintaksis. Misalnya, tidak ada cara langsung untuk menambahkan variabel baru. Seperti yang saya katakan sebelumnya, kita harus menggunakan IIFE, sehingga terlihat seperti ini:


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

Kami akan menambahkan kata kunci let . Ini akan memungkinkan kami untuk menulis sesuatu seperti ini:


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

Untuk setiap variabel di blok let , variabel sebelumnya harus tersedia bahkan dari blok yang sama. Karenanya, kode di atas akan setara dengan yang berikut:


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

Perubahan-perubahan ini dapat dilakukan secara langsung di parser dan kemudian tidak akan membutuhkan perubahan dalam penerjemah. Alih-alih menambahkan node let baru, kita dapat mengubahnya menjadi node call dan lambda . Ini berarti bahwa kami tidak melakukan perubahan semantik dalam bahasa kami - ini disebut "gula sintaksis", dan operasi mengubah ini menjadi node AST yang ada sebelumnya disebut "tanpa gula" (asli: "desugaring").


Namun, kita harus mengganti pengurai. Mari kita tambahkan "let" node baru karena dapat ditafsirkan lebih efisien (tidak perlu membuat penutupan dan memanggil mereka segera, kita hanya perlu menyalin dan mengubah konteksnya).


Kami juga akan menambahkan dukungan untuk "beri nama", yang ada di Skema. Itu membuatnya lebih mudah untuk membuat loop:


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

Ini adalah loop "rekursif" yang menghitung jumlah 10 + 9 + ... + 0. Sebelumnya, kita harus melakukannya seperti ini:


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

Selain itu, untuk mempermudah ini, kami akan menambahkan sintaks "fungsi dengan nama". Ini akan terlihat seperti ini:


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

Modifikasi yang perlu dilakukan untuk ini:


  • Dukungan untuk nama opsional setelah kata kunci lambda . Jika ada, maka kita harus menambahkan variabel ke konteks saat ini yang akan menunjuk ke fungsi itu sendiri. Ini persis sama dengan fungsi dengan nama dalam JavaScript.
  • Dukungan untuk kata kunci let baru. Berikutnya adalah nama opsional dan daftar (mungkin kosong) definisi variabel dalam formulir ini: foo = EXPRESSION , dipisahkan oleh koma. Tubuh ekspresi let adalah ekspresi tunggal (yang, tentu saja, dapat menjadi urutan ekspresi).

Perubahan Parser


Pertama, perubahan kecil dalam tokenizer, tambahkan kata kunci let ke daftar kata kunci:


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

Ubah fungsi parse_lambda ia membaca nama opsional:


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

Sekarang tambahkan fungsi yang mem-parsing ekspresi 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(), }; } 

Ini menangani dua kasus. Jika setelah let ada token tipe var , maka ini let dengan nama. Selanjutnya, kami membaca daftar definisi menggunakan fungsi yang delimited , karena mereka berada dalam tanda kurung dan dipisahkan oleh koma, dan kami menggunakan fungsi parse_vardef , yang ditunjukkan di bawah ini. Selanjutnya, kami mengembalikan simpul tipe call , yang segera memanggil fungsi bernama (IIFE). Argumen untuk fungsi adalah variabel yang ditentukan oleh let , dan node call akan meneruskan nilai sebagai argumen. Dan, tentu saja, isi fungsi dibaca menggunakan parse_expression() .


Jika let sederhana, maka kita mengembalikan simpul tipe let dengan vars dan bidang isian. Bidang vars berisi larik variabel dalam format berikut: { name: VARIABLE, def: AST } , yang diuraikan oleh fungsi berikut:


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

Selain itu, Anda perlu menambahkan tanda centang untuk jenis ekspresi baru di fungsi parse_atom :


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

Perubahan Interpreter


Karena kami memutuskan untuk mengubah struktur AST alih-alih “memecahnya” menjadi tipe node yang lama, kami harus menambahkan pemrosesan logika baru ke interpreter.


Untuk menambahkan dukungan untuk nama fungsi opsional, kami memodifikasi fungsi 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; } 

Jika fungsi memiliki nama, maka saat kami membuat penutupan, kami membuat salinan konteks, dan menambahkan fungsi ke konteks. Sisanya tetap sama.


Dan akhirnya, untuk memproses sebuah simpul bertipe let , kita menambahkan case berikut ke interpreter:


 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); 

Perhatikan bahwa untuk setiap variabel konteks baru dibuat di mana variabel baru ditambahkan. Setelah itu, kita cukup mengeksekusi tubuh dalam konteks terakhir.


Contohnya


 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.


, , . , .


Kesimpulan


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


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


: JavaScript. Bagian 3: juru bahasa CPS

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


All Articles