مرحبا أقدم إليكم الجزء الثاني من ترجمي للدليل الخاص بتنفيذ لغة برمجة جافا سكريبت الخاصة بك - البرنامج التعليمي PL .
من المترجم
سنقوم بإنشاء لغة البرمجة الخاصة بنا - ((باللغة الأصلية - λanguage). في عملية الإنشاء ، سنستخدم الكثير من التقنيات المثيرة للاهتمام ، مثل النسب العودية ونمط نقل التحكم وتقنيات التحسين الأساسية. سيتم إنشاء نسختين من المترجم الشفهي - المترجمين العاديين والمترجمين الفوريين CPS ، المترجم الشفهي في JavaScript.
مؤلف النسخة الأصلية هو Mihai Bazon ، مؤلف مكتبة UglifyJS الشهيرة (أداة لتقليل وتنسيق رمز JS).
ملاحظة: يوجد خطأ في المترجم والمترجم: في التعبيرات مثل a() && b()
أو a() || b()
a() || b()
يتم تنفيذ كلا الجزأين دائمًا. هذا ، بالطبع ، خاطئ لأن a()
خطأ لـ &&
عامل التشغيل ، أو غير صحيح لـ ||
، ثم لا تلعب قيمة b()
أي دور. هذا ليس من الصعب إصلاحه.
مترجم بسيط
في الجزء السابق ، كتبنا 3 وظائف: TokenStream
و TokenStream
و TokenStream
. للحصول على AST من الكود ، نستخدم الكود التالي:
var ast = parse(TokenStream(InputStream(code)));
كتابة مترجم أسهل من المحلل اللغوي: نحن نعبر الشجرة بشكل متكرر وننفذ التعبيرات بترتيبها الطبيعي.
السياق ( Environment
)
لتنفيذ التعليمات البرمجية المناسبة ، نحتاج إلى سياق - كائن يحتوي على جميع المتغيرات في موقع معين. سيتم تمريره كوسيطة لوظيفة evaluate
.
في كل مرة ندخل فيها عقدة lambda
، يجب أن نضيف متغيرات جديدة إلى وسيطات السياق - دالة. إذا تداخلت الوسيطة مع المتغير من الكتلة الخارجية ، فيجب علينا استعادة القيمة القديمة للمتغير بعد الخروج من الوظيفة.
أسهل طريقة للقيام بذلك هي استخدام نموذج 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);
يحتوي كائن Environment
على حقل أصل يشير إلى السياق الخارجي. سيكون null
بالنسبة للسياق العالمي. يحتوي على حقل vars
حيث توجد جميع المتغيرات التي تنتمي إلى هذا السياق. بالنسبة للسياق العام ، فإنه يساوي على الفور كائن فارغ ( Object.create(null)
) ونسخة من متغيرات السياق الأصل ( Object.create(parent.vars)
) Object.create(parent.vars)
.
لديها عدة طرق:
extend()
- نسخ السياق الحالي.lookup(name)
- ابحث عن السياق الذي name
تعريف المتغير باسم name
.get(name)
- الحصول على قيمة اسم متغير اسمه. يلقي استثناء إذا لم يتم تعريف المتغير.set(name, value)
- اضبط قيمة المتغير. تبحث هذه الطريقة عن السياق الذي يتم فيه تعريف المتغير. إذا لم يتم تعريفه ، ونحن لسنا في سياق عالمي ، سيتم طرح استثناء.def(name, value)
- ينشئ (أو يتداخل أو يتجاهل) متغير في السياق الحالي.
evaluate
الوظيفة
الآن بعد أن أصبح لدينا كائن Environment
، يمكننا الانتقال إلى حل المشكلة الرئيسية. ستكون هذه الوظيفة عبارة عن كتلة switch
كبيرة ، والتي ستقوم ببعض الإجراءات اعتمادًا على نوع العقدة المرسلة:
function evaluate(exp, env) { switch (exp.type) {
بالنسبة للحرف ، نرجع ببساطة قيمته:
case "num": case "str": case "bool": return exp.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
وظيفة apply_op
لاحقًا. أيضًا ، ندعو evaluate
لكل من أجزاء التعبير:
case "binary": return apply_op(exp.operator, evaluate(exp.left, env), evaluate(exp.right, env));
ستُرجع العقدة من النوع lambda
إغلاق JavaScript عاديًا ، لذلك يمكن تسميتها كدالة عادية حتى من JavaScript. أضفت وظيفة make_lambda
، والتي make_lambda
لاحقًا:
case "lambda": return make_lambda(env, exp);
تنفيذ العقدة if
بسيط للغاية: أولاً نجد قيمة الشرط. إذا لم يكن خطأ ، فقم بإرجاع قيمة الفرع 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
:
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); } }
كان هذا هو الجزء الرئيسي من المترجم. أعلاه ، استخدمنا وظيفتين لم نطبقهما بعد ، لذلك دعونا نبدأ:
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
بسيطة وبديهية. على عكس جافا سكريبت ، التي يمكن أن تأخذ أي قيمة ، مثل المتغيرات ، حتى تلك التي لا معنى لها. نطلب أن تكون معاملات العوامل الحسابية أرقامًا ولا تسمح بالقسمة على الصفر. للسلاسل ، سنأتي بشيء لاحقًا.
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; }
كما ترى أعلاه ، فإنها تُرجع دالة JavaScript عادية تستخدم السياق الذي تم تمريره ووظائف AST. يتم تنفيذ كل العمل فقط عندما يتم استدعاء الوظيفة نفسها: يتم إنشاء سياق ، يتم تعيين الوسائط (إذا لم تكن كافية ، تصبح false
). ثم يتم تنفيذ الجسم وظيفة ببساطة في سياق جديد.
وظائف محلية
كما ترون ، لم تكن لدينا طريقة للتفاعل مع JavaScript من لغتنا. اعتدت على استخدام وظائف print
و println
، لكنني لم أعرفها في أي مكان. نحتاج إلى كتابتها في JavaScript وإضافتها فقط إلى السياق العام.
فيما يلي مثال على هذا الرمز:
كود كامل
يمكنك تنزيل جميع الكود الذي كتبناه طوال هذا الوقت. يمكن إطلاقه باستخدام 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. يحدث هذا لأن المترجم عودي ووصلت العودية إلى أقصى عمق.
هذه مشكلة خطيرة ، لكن هناك حل. أرغب في إضافة تصميمات جديدة للتكرار (من for
أو while
) ، ولكن دعنا نحاول الاستغناء عنها. العودية تبدو جميلة ، لذلك دعونا نتركها. سنرى لاحقا كيف نتغلب على هذا القيد.
هياكل البيانات (نقصها)
هناك ثلاثة أنواع من البيانات بلغتنا: الأرقام والسلاسل وأنواع المنطقية. قد يبدو أنه لا يمكنك إنشاء أنواع معقدة ، مثل المصفوفات أو الكائنات. لكن هذا ليس بالأمر ، لدينا نوع آخر: الوظيفة. اتضح أننا إذا اتبعنا حساب التفاضل والتكامل ، فيمكننا إنشاء أي هياكل بيانات ، بما في ذلك الكائنات ، حتى مع الميراث.
سأعرضه على مثال القوائم. دعونا نتخيل أن لدينا دالة cons
تقوم بإنشاء كائن يحتوي على قيمتين. دعنا نسمي هذا الكائن "خلية" أو "زوج". سنقوم بتسمية واحدة من القيم "سيارة" والآخر "cdr". فقط لأنهم يطلق عليهم في اللغة ليزب. الآن ، إذا كان لدينا كائن "خلية" ، فيمكننا الحصول على قيمه باستخدام وظائف car
و cdr
:
x = cons(10, 20); print(car(x));
الآن يمكننا بسهولة تحديد قائمة:
القائمة عبارة عن زوج يحتوي على العنصر الأول في `car` والعناصر المتبقية في` cdr`. لكن `cdr` يمكن أن يحتوي على قيمة واحدة فقط! هذه القيمة ستكون قائمة. القائمة عبارة عن زوج يحتوي على العنصر الأول في `car` والعناصر المتبقية في` cdr`. لكن `cdr` يمكن أن يحتوي على قيمة واحدة فقط! هذه القيمة ستكون قائمة. [...]
هذا هو نوع البيانات العودية. ولكن تبقى مشكلة واحدة: متى تحتاج إلى التوقف؟ منطقيا ، يجب أن نتوقف عندما cdr
هي قائمة فارغة. ولكن ما هي قائمة فارغة؟ للقيام بذلك ، دعونا نضيف كائنًا جديدًا يسمى "NIL". يمكن استخدامه كزوجين (يمكننا استخدام car
و cdr
عليها ، لكن النتيجة ستكون NIL
نفسها). لنقم الآن بإنشاء قائمة بالعناصر 1 و 2 و 3 و 4 و 5:
x = cons(1, cons(2, cons(3, cons(4, cons(5, NIL))))); print(car(x));
يبدو مروعا عندما لا يكون هناك بناء جملة خاص لهذا الغرض. لكنني أردت فقط أن أظهر أنه يمكن القيام بذلك باستخدام ميزات اللغة الحالية. هنا هو التنفيذ:
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
(ولكن هذا أمر غريب بالنظر إلى حقيقة أنها ليست في حساب التفاضل والتكامل الأصلي). بالطبع ، لا توجد لغة برمجة تفعل ذلك بهذه الطريقة ، لأنها غير فعالة للغاية ، لكنها لا تجعل حساب العمليات الحسابية أقل جمالًا. في لغة واضحة ، يقوم هذا الرمز بما يلي:
- تأخذ الدالة
cons
قيمتين ( a
و b
) وتقوم بإرجاع الوظيفة التي تحملها. هذه الوظيفة هي غاية الزوج. إنها تأخذ حجة وتدعوها لكلا قيم الزوج. - تستدعي وظيفة
car
الوسيطة التي تم تمريرها ، لتمرير دالة تُرجع الوسيطة الأولى. - تعمل وظيفة
cdr
مثل وظيفة car
، ولكن مع وجود الفرق الوحيد في أن الدالة التي تم تمريرها تُرجع الوسيطة الثانية. - تعمل دالة
NIL
بنفس الطريقة التي تعمل بها cons
، ولكنها تُرجع زوجًا له قيمتان تساوي قيمة 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));
هناك العديد من الخوارزميات في القوائم التي يمكن تنفيذها بشكل متكرر وتبدو منطقية. على سبيل المثال ، فيما يلي دالة تستدعي الوظيفة التي تم تمريرها لكل عنصر في القائمة:
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;
القوائم التي قمنا بتطبيقها أعلاه غير قابلة للتغيير (لا يمكننا تغيير car
أو cdr
بعد إنشاء القائمة). معظم Lisp لها وظائف لتغيير الزوج. في المخطط يطلق عليهم set-car!
/ set-cdr!
. في Common Lisp ، rplaca
/ rplacd
. هذه المرة نستخدم أسماء من المخطط:
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);
هذا يدل على أنه يمكننا تنفيذ هياكل البيانات القابلة للتغيير. لن أتعمق في كيفية عمله ، فمن الواضح من الكود.
يمكننا أن نذهب أبعد من ذلك وننفذ الكائنات ، لكن بدون تغييرات في بناء الجملة سيكون من الصعب القيام بها. هناك طريقة أخرى تتمثل في تطبيق بناء جملة جديد في الرمز / المحلل اللغوي وإضافة معالجته في المترجم. تقوم جميع لغات البرمجة الرئيسية بهذا ، ومن الضروري تحقيق الأداء العادي. سنضيف جملة جديدة في الجزء التالي من المقالة.
[من المترجم: إذا كنت مهتمًا بحساب التفاضل والتكامل lambda ، فهناك مقال رائع حول Habré مخصص لهذا الموضوع: حساب التفاضل والتكامل Lambda في حساب JavaScript .]
بناء جملة جديد
لغتنا has بها عدد قليل من الإنشاءات النحوية. على سبيل المثال ، لا توجد طريقة مباشرة لإضافة متغيرات جديدة. كما قلت من قبل ، يجب علينا استخدام IIFE ، لذلك يبدو مثل هذا:
(λ(x, y){ (λ(z){
سنقوم بإضافة الكلمة 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 التي كانت موجودة من قبل تسمى "خالية من السكر" (الأصلي: "desugaring").
ومع ذلك ، يجب علينا تغيير المحلل اللغوي على أي حال. دعنا نضيف عقدة "دع" جديدة لأنه يمكن تفسيرها بشكل أكثر كفاءة (لا حاجة لإنشاء عمليات إغلاق والاتصال بهم على الفور ، نحتاج فقط إلى نسخ السياق وتغييره).
أيضًا ، سنضيف دعمًا لـ "اسم العائلة" ، والذي كان في المخطط. يسهل إنشاء الحلقات:
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));
التعديلات التي يجب القيام بها لهذا:
- دعم لاسم اختياري بعد الكلمة
lambda
. إذا كان موجودًا ، فيجب علينا إضافة متغير إلى السياق الحالي يشير إلى الوظيفة نفسها. هذا هو بالضبط نفس الوظائف التي لها اسم في JavaScript. - دعم الكلمة الرئيسية الجديدة التالي يأتي اسمًا اختياريًا وقائمة (ربما تكون خالية) من تعريفات المتغيرات في هذا النموذج:
foo = EXPRESSION
، مفصولة بفواصل. نص تعبير let
هو تعبير واحد (والذي ، بالطبع ، يمكن أن يكون سلسلة من التعبيرات).
التغييرات محلل
أولاً ، تغيير بسيط في الرمز المميز ، أضف الكلمة الأساسية 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,
أضف الآن دالة تقوم بتوزيع تعبير 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(), }; }
هذا يعالج حالتين. إذا كان هناك بعد ذلك رمز مميز من النوع var
، فسيتم let
بذلك باسم. علاوة على ذلك ، نقرأ قائمة التعاريف باستخدام الوظيفة delimited
، حيث إنها بين قوسين ومفصولة بفواصل ، ونحن نستخدم دالة parse_vardef
، الموضحة أدناه. بعد ذلك ، نرجع عقدة call
النوع ، والتي تستدعي على الفور وظيفة تسمى (IIFE). الوسائط إلى الدالة هي المتغيرات المعرّفة من قِبل let
، وتمرير عقدة call
القيم كوسائط. وبالطبع ، يتم قراءة نص الدالة باستخدام parse_expression()
.
إذا كانت تركيبة بسيطة ، فسنقوم بإرجاع عقدة من النوع let
vars
وحقول body
. يحتوي حقل 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
:
التغييرات مترجم
نظرًا لأننا قررنا تغيير بنية AST بدلاً من "تكسيرها" في الأنواع القديمة من العقد ، يجب علينا إضافة معالجة المنطق الجديد إلى المترجم الشفهي.
لإضافة دعم لاسم الوظيفة الاختياري ، نقوم بتعديل الدالة make_lambda
:
function make_lambda(env, exp) { if (exp.name) {
إذا كانت الوظيفة لها اسم ، فعندما ننشئ الإغلاق ، نقوم بعمل نسخة من السياق ، ونضيف الوظيفة إلى السياق. الباقي لا يزال هو نفسه.
وأخيرًا ، لمعالجة عقدة النوع ، 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);
— .
. , , , . 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 مترجم