معمارية تطبيقات المصادر المفتوحة › Processing.js 44 / 51 السابق التالي

Processing.js

بنية تطبيقات البرمجيات مفتوحة المصدر (المجلد الثاني)Processing.js

بنية تطبيقات البرمجيات مفتوحة المصدر (المجلد الثاني) Processing.js

Mike Kamermans

إن كنت تستمتع بهذه الكتب، فقد يعجبك أيضًا Software Design by Example in Python وSoftware Design by Example in JavaScript.

طوّر Ben Fry و Casey Reas في الأصل لغة البرمجة Processing، التي انطلقت بوصفها لغة برمجة مفتوحة المصدر (قائمة على Java) لمساعدة مجتمعات الفنون الإلكترونية والتصميم البصري على تعلّم أساسيات البرمجة الحاسوبية ضمن سياق بصري. ولأنها تقدّم نموذجًا مبسَّطًا إلى حدٍّ بعيد للرسوميات ثنائية الأبعاد وثلاثية الأبعاد مقارنةً بأغلب لغات البرمجة، فقد سرعان ما صارت ملائمة لمجموعة واسعة من الأنشطة، من تدريس البرمجة عبر كتابة تصويرات بيانية (visualisations) صغيرة إلى إنشاء منشآت فنية متعددة الجدران، وصارت قادرة على تنفيذ تنوّع كبير من المهام، من مجرد قراءة سلسلة من النصوص إلى الدور بوصفه محرّر التطوير المتكامل (IDE) الفعلي لبرمجة وتشغيل لوحات "Arduino" مفتوحة المصدر الشائعة التي تُستخدم في النمذجة الأولية. ومع استمراريتها في نيل الشعبية، فقد رسّخت Processing موقعها كلغة برمجة سهلة التعلّم وشائعة الاستخدام في كل ما يتّصل بالأمور البصرية، وغيرها الكثير.

يتكوّن برنامج Processing الأساسي، الذي يُسمّى «sketch»، من دالتين: setup و draw. الأولى هي نقطة دخول البرنامج الرئيسة، ويمكن أن تحتوي على أي عدد من تعليمات التهيئة. وبعد انتهاء setup، تستطيع برامج Processing أن تفعل أحد أمرين: 1) أن تستدعي draw وتجدول استدعاءً آخرًا لـ draw عند فترة زمنية ثابتة بمجرد اكتماله؛ أو 2) أن تستدعي draw ثم تنتظر أحداث إدخال من المستخدم. وافتراضيًا تفعل Processing الأمر الأول؛ أما استدعاء noLoop فيفضي إلى الأمر الثاني. ويتيح هذا وجود نمطين لعرض الـ sketches، وهما: بيئة رسومية بمعدل إطارات ثابت، وبيئة رسومية تفاعلية محدَّثة بالأحداث. وفي الحالتين تُراقَب أحداث المستخدم، ويمكن معالجتها إمّا في معالجات الأحداث الخاصة بها، أو — بالنسبة إلى أحداث معيّنة تضبط قيمًا عالمية دائمة — مباشرةً داخل الدالة draw.

Processing.js مشروع شقيق لـ Processing، صُمِّم لإيصاله إلى الويب دون الحاجة إلى Java أو إلى أي إضافات (plugins). وقد بدأ كمحاولة من John Resig لاختبار ما إذا كانت لغة Processing قابلة للنقل إلى الويب، باستخدام عنصر HTML5 — الذي كان وقتها جديدًا تمامًا — كسياق رسومي، مع إصدار مكتبة إثبات المفهوم (proof of concept) للعامة عام 2008. وقد كُتب Processing.js بعقلية مفادها أن «شيفرتك يجب أن تعمل ببساطة»، ثم جرى تحسينه على مدى السنين حتى تجعل تصويرات البيانات (data visualisations) والفن الرقمي والحركات التفاعلية والرسوم التعليمية وألعاب الفيديو وغيرها تعمل باستخدام معايير الويب ودون أي إضافات. فأنت تكتب شيفرتك بلغة Processing، إما في Processing IDE أو في المحرِّر الذي تفضّله، ثم تضعها في صفحة ويب باستخدام عنصر، فيتولّى Processing.js الباقي، بعرض كل شيء داخل عنصر `` والسماح للمستخدم بالتفاعل مع الرسوميات بالطريقة نفسها التي يتفاعل بها مع برنامج Processing مستقلّ عادي.

17.1. كيف يعمل؟

يتميّز Processing.js بأنه مشروع مفتوح المصدر غير اعتيادي قليلًا، إذ إنّ قاعدة الشيفرة (code base) ملف واحد اسمه processing.js يحتوي على شيفرة Processing، وهو الكائن الواحد الذي تتكوّن منه المكتبة بأكملها. ومن حيث كيفية تنظيم الشيفرة، فإننا نُعيد خلط الأشياء باستمرار داخل هذا الكائن في محاولة لتنظيفه قليلًا مع كل إصدار. أمّا تصميمه فبسيط نسبيًا، ويمكن وصف وظيفته بجملة واحدة: فهو يعيد كتابة شيفرة Processing إلى شيفرة JavaScript خالصة، وتُرسَم كل نداءات دوال واجهة Processing API إلى دالة مقابلة في كائن Processing في JavaScript، بحيث يُحدث ذلك على عنصر `` الأثر نفسه الذي يُحدثه نداء Processing على لوحة الرسم (canvas) في Java Applet.

ولأداءٍ أعلى، لدينا مساران منفصلان للشيفرة الخاصّان بدوال الأبعادين ودوال الأبعاد الثلاثة، وعند تحميل أي sketch يُستخدَم أحدهما أو الآخر لحلّ أغلفة الدوال (function wrappers)، حتى لا نضيف حجمًا زائدًا (bloat) إلى النسخ قيد التشغيل. لكن من حيث بنى البيانات ومسار التنفيذ (code flow)، فإن معرفتك بـ JavaScript تكفي لتقرأ processing.js، باستثناء مُحلِّل الصياغة (syntax parser) على الأرجح.

توحيد Java و JavaScript

تعني إعادة كتابة شيفرة Processing إلى شيفرة JavaScript أنك تستطيع ببساطة أن تطلب من المتصفح تنفيذ الشيفرة المُعاد كتابتها، فإن كانت إعادة الكتابة صحيحة فإن الأمور تعمل ببساطة. لكن التأكد من صحّة إعادة الكتابة قد استهلك، ولا يزال يستهلك أحيانًا، جهدًا كبيرًا. فصياغة Processing قائمة على Java، ما يعني أن Processing.js عليه في جوهره أن يحوّل شيفرة Java إلى شيفرة JavaScript. وقد تحقّق ذلك في البدء عبر التعامل مع شيفرة Java بوصفها سلسلة نصية (string)، ثم استبدال مقاطع منها بما يقابلها في JavaScript بشكل تكراري. (ولمن يهتمّ بمولفة مبكّرة للمحلِّل، فيمكن العثور عليها هنا، من السطر 37 حتى السطر 266.) ويمرّ ذلك بسلاسة مع مجموعة صياغة صغيرة، لكن مع مرور الوقت وتراكم التعقيد فوق التعقيد بدأ هذا النهج في الانهيار. وعليه أُعيد كتابة المحلِّل كليًا ليُنشئ شجرة صياغة مجرّدة (Abstract Syntax Tree، AST) بدلاً من ذلك، بأن يفكّك شيفرة Java أوّلًا إلى كتل وظيفية، ثم يربط كل واحدة منها بصياغتها المقابلة في JavaScript. والنتيجة أن Processing.js صار يحتوي فعليًا، على حساب سهولة القراءة، مترجمًا فوريًا (transcompiler) من Java إلى JavaScript. (والقراء مدعوّون إلى تصفّح هذه الشيفرة حتى السطر 19217.)

إليك شيفرة sketch بـ Processing:

    void setup() {
      size(200,200);
      noCursor();
      noStroke();
      smooth(); }

    void draw() {
      fill(255,10);
      rect(-1,-1,width+1,height+1);
      float f = frameCount*PI/frameRate;
      float d = 10+abs(60*sin(f));
      fill(0,100,0,50);
      ellipse(mouseX, mouseY, d,d); }

وهذه هي صيغتها بعد التحويل إلى Processing.js:

    function($p) {
        function setup() {
            $p.size(200, 200);
            $p.noCursor();
            $p.noStroke();
            $p.smooth(); }
        $p.setup = setup;

        function draw() {
            $p.fill(255, 10);
            $p.rect(-1, -1, $p.width + 1, $p.height + 1);
            var f = $p.frameCount * $p.PI / $p.__frameRate;
            var d = 10 + $p.abs(60 * $p.sin(f));
            $p.fill(0, 100, 0, 50);
            $p.ellipse($p.mouseX, $p.mouseY, d, d); }
        $p.draw = draw; }

يبدو هذا شيئًا رائعًا، لكن هناك بعض المشكلات عند تحويل صياغة Java إلى صياغة JavaScript:

  1. برامج Java كيانات معزولة. أما برامج JavaScript فتشارك العالم مع صفحة ويب.
  2. Java مُصنَّفة الأنواع بشدّة. أما JavaScript فلا.
  3. Java لغة موجّهة نحو الكائنات قائمة على الأصناف/الكائنات. أما JavaScript فلا.
  4. لدى Java متغيّرات ودوال متباينة. أما JavaScript فلا.
  5. Java تسمح بالحمل الزائد (overloading) للدوال. أما JavaScript فلا.
  6. Java تسمح باستيراد شيفرة مُصرَّفة. أما JavaScript فلا فكرة لديه أصلًا عمّا يعنيه ذلك.

كان التعامل مع هذه المشكلات مسألة موازنة (trade-off) بين ما يحتاجه المستخدمون وما نستطيع فعله في ضوء تقنيات الويب. وتناقش الأقسام التالية كلًّا من هذه القضايا بمزيد من التفصيل.

17.2. فروق جوهرية

برامج Java لها خيوطها الخاصة؛ أما JavaScript فيمكنه أن يُجمِّد متصفحك.

برامج Java كيانات معزولة، تعمل في خيطها الخاص ضمن مجموعة التطبيقات الأكبر على نظامك. أما برامج JavaScript، من جهة أخرى، تعيش داخل متصفح، وتتنافس فيما بينها بطريقة لا تفعلها تطبيقات سطح المكتب. فحين يحمّل برنامج Java ملفًا، ينتظر البرنامج حتى يكتمل تحميل المورد، ثم يستأنف عمله كما هو مقصود. وفي سياقٍ يكون فيه البرنامج كيانًا معزولًا وحده، يكون هذا الأمر سليمًا. يبقى نظام التشغيل مستجيبًا لأنه المسؤول عن جدولة الخيوط (thread scheduling)، وحتى لو استغرق البرنامج ساعة في تحميل كل بياناته، فما زال بإمكانك استخدام حاسوبك. أما على صفحة ويب، فالأمور لا تسير على هذا النحو. فلو كان لديك «برنامج» JavaScript ينتظر اكتمال تحميل مورد، فإنه سيقفل عمليته (process) حتى يصبح ذلك المورد متاحًا. فإن كنت تستخدم متصفحًا يخصّص عملية لكل تبويب، فسيُقفل التبويب ويبقى بقية المتصفح قابلًا للاستخدام. أما إن كنت تستخدم متصفحًا لا يفعل ذلك، فسيبدو متصفحك بأكمله متجمّدًا. لذلك، مهما كان ما تمثّله العملية، فلن تكون الصفحة التي يعمل عليها السكربت قابلة للاستخدام حتى يكتمل تحميل المورد، ومن الممكن تمامًا أن يُجمِّد JavaScript المتصفح بأكمله.

وهذا غير مقبول على الويب الحديث، حيث تُنقل الموارد بشكل غير متزامن (asynchronously)، ويُتوقَّع من الصفحة أن تعمل بصفة طبيعية بينما تُحمَّل الموارد في الخلفية. ورغم أن هذا رائع لصفحات الويب التقليدية، إلا أنه يمثّل عقدة ذهنية حقيقية لتطبيقات الويب: فكيف تجعل JavaScript خاملةً في انتظار تحميل مورد، مع عدم وجود آلية صريحة لجعل JavaScript خاملة؟ وعلى الرغم من غياب الخيوط الصريحة في JavaScript، فإن هناك نموذج أحداث (event model)، وهناك كائن XMLHTTPRequest لطلب بيانات اعتباطية (لا XML أو HTML فحسب) من عناوين URL اعتباطية. ويأتي هذا الكائن مع عدة أحداث حالة مختلفة، ويمكننا استخدامه لجلب البيانات بشكل غير متزامن مع بقاء المتصفح مستجيبًا. وهذا رائع في البرامج التي تتحكم في شيفرتها: فأنت تجعلها تتوقف ببساطة بعد جدولة طلب البيانات، ثم تستأنف التنفيذ حين تتاح البيانات. لكن هذا يكاد يكون مستحيلًا بالنسبة إلى شيفرة كُتبت على أساس فكرة التحميل المتزامن للموارد. وحقن «الخمول» في برامج يُفترض أن تعمل بمعدل إطارات ثابت ليس خيارًا مطروحًا، لذا اضطررنا إلى ابتكار مقارب بديلة.

وبالنسبة إلى بعض الأمور، قررنا فرض الانتظار المتزامن على أي حال. فمثلًا، تحميل ملف يحوي نصوصًا (strings) يستخدم XMLHTTPRequest متزامنًا، ويوقف تنفيذ الصفحة حتى تتاح البيانات. أما الأمور الأخرى فقد اضطررنا فيها إلى الإبداع. فمثلًا، تحميل الصور يستخدم آلية المتصفح المدمجة في تحميل الصور؛ فنحن نبني كائن Image جديدًا في JavaScript، ونضبط السمة src الخاصة به على عنوان URL للصورة، ويتولّى المتصفح الباقي، فيُخبرنا بأن الصورة جاهزة عبر الحدث onload. ولا يستند هذا حتى إلى XMLHTTPRequest، وإنما يستغلّ ببساطة قدرات المتصفح.

ولتبسيط الأمور عندما تكون مسبقًا تعرف الصور التي تحمّلها، أضفنا تعليمات التحميل المسبق (preload) بحيث لا يبدأ الـ sketch في التنفيذ حتى تكتمل عملية التحميل المسبق. ويمكن للمستخدم أن يذكر أي عدد من الصور المطلوب تحميلها مسبقًا عبر كتلة تعليقات في بداية الـ sketch؛ ثم يتتبّع Processing.js عمليات تحميل الصور المعلّقة. ويخبرنا حدث onload الخاص بالصورة بأن نقلها قد اكتمل وأنها تُعدّ جاهزة للعرض (بدلًا من أنها نُزّلت فحسب ولم يُفكّ ترميزها بعد إلى مصفوفة بكسلات في الذاكرة)، وعندها يمكننا ملء كائن Processing ‏PImage المقابل بالقيم الصحيحة (width و height وبيانات البكسلات وغيرها) وإزالة الصورة من القائمة. ومتى أصبحت القائمة فارغة، يُنفَّذ الـ sketch، ولن تتطلّب الصور المستخدَمة أثناء عمره انتظارًا.

إليك مثالًا على تعليمات التحميل المسبق:

    /* @pjs preload="./worldmap.jpg"; */

    PImage img;

    void setup() {
      size(640,480);
      noLoop();
      img = loadImage("worldmap.jpg"); }

    void draw() {
      image(img,0,0); }

وبالنسبة إلى أمور أخرى، اضطررنا إلى بناء أنظمة «انتظرني» أكثر تعقيدًا. فالخطوط (fonts)، بخلاف الصور، لا تملك آلية تحميل مدمجة في المتصفح (أو على الأقل لا تملك نظامًا بوظائف تحميل الصور). فمع أنه من الممكن تحميل خط باستخدام قاعدة CSS من نوع @font-face والاعتماد على المتصفح لإنجاز الأمر كله، إلا أنه لا توجد أحداث JavaScript يمكن استخدامها لتحديد اكتمال تحميل خط. وقد نرى ببطء تضاف أحداث إلى المتصفحات لتوليد أحداث JavaScript عند اكتمال تنزيل الخط، لكن هذه الأحداث تأتي «في وقت أبكر من اللازم»، إذ قد يحتاج المتصفح إلى ما بين بضعة أجزاء من الثانية وبضع مئات منها إضافية لتحليل الخط فعلًا لاستخدامه في الصفحة بعد التنزيل. وبذلك فإن التصرّف بناءً على هذه الأحداث سيؤدي إمّا إلى عدم تطبيق أي خط، وإمّا إلى تطبيق الخط الخطأ إذا وُجد خط احتياطي معروف. وبدلًا من الاعتماد على هذه الأحداث، نضمّن خط TrueType مجهريًا لا يحتوي إلّا على الحرف "A" بمقاييس صغيرة إلى حدّ المستحيل، ونطلب من المتصفح تحميل هذا الخط عبر قاعدة @font-face بمعرّف URI للبيانات (data URI) يحتوي على بايت-كود الخط بوصفه سلسلة BASE64. وهذا الخط صغير إلى حدّ يسمح لنا بالاعتماد على توفّره فورًا. وأي تعليمات تحميل خطوط أخرى فنقارن فيها مقاييس النص بين الخط المطلوب وهذا الخط المجهري. ويُنشأ عنصر مخفيّ بتنسيق نصّي بالخط المطلوب، مع جعل خطنا المجهري خطًا احتياطيًا (fallback). ما دام النص داخل ذلك صغيرًا إلى حدّ مستحيل، نعرف أن الخط المطلوب غير متاح بعد، فنكتفي بالاستطلاع (polling) على فترات منتظمة حتى تكتسب مقاييس النص معقولية.

Java مُصنَّفة الأنواع بشدّة؛ أما JavaScript فلا.

في Java، العدد 2 والعدد 2.0 قيمتان مختلفتان، وستُنتجان سلوكًا مختلفًا أثناء العمليات الحسابية. فمثلًا، الشيفرة i = 1/2 تجعل i تساوي 0، لأن الأعداد تُعامَل بوصفها أعدادًا صحيحة، في حين أن i = 1/2.0 و i = 1.0/2 وحتى i = 1./2. تجعل i تساوي 0.5 كلها، لأن الأعداد تُعدّ كسورًا عشرية ذات جزء صحيح غير صفري وجزء كسري صفري. وحتى لو كان نوع البيانات المقصود عددًا عشريًا متحركًا (floating point)، فإن كانت العملية الحسابية تستخدم أعدادًا صحيحة فقط فستكون النتيجة عددًا صحيحًا. ويتيح لك هذا كتابة عبارات حسابية إبداعية نوعًا ما في Java، وبالتالي في Processing، غير أن هذه العبارات قد تُنتج نتائج مختلفة إلى حدّ مذهل عند نقلها إلى Processing.js، لأن JavaScript لا يعرف سوى «الأعداد». ومن وجهة نظر JavaScript، فإن 2 و 2.0 العدد نفسه، وقد ينشئ ذلك أخطاءً شديدة الغرابة عند تشغيل sketch باستخدام Processing.js.

قد يبدو هذا مسألة بالغة الأهمية، وكنا في البداية مقتنعين بأنها ستكون كذلك، لكن لا يمكن مجادلة ما يقوله العالم الحقيقي: تبيّن أن هذه المسألة تكاد لا تكون مشكلة أبدًا لمن ينشرون الـ sketches الخاصة بهم على الويب باستخدام Processing.js. وبدلًا من حلّ هذه المسألة بطريقة باردة وإبداعية، كان حلّها مذهلًا في بساطته؛ فأدركنا أننا لا نحلّها، وكخيار في التصميم لا نوي قطّ العودة إلى هذا القرار. فبعيدًا عن إضافة جدول رموز (symbol table) بأنواع صارمة، نتيح به محاكاة الأنواع (types) في JavaScript والتبديل بين الوظائف بحسب النوع، لا يمكن حلّ هذا التعارض حلًّا سليمًا دون ترك أخطاء في الحالات الحدّية (edge cases) يصعب العثور عليها أكثر بكثير؛ ولهذا، بدلًا من إضافة حجم إلى الشيفرة وإبطاء في التنفيذ، تركنا هذه الخاصية الفريدة (quirk) كما هي. وهي خاصية موثّقة جيدًا، ولن يحاول «الشيفرة الجيدة» استغلال صبّ نوع الأعداد الضمني في Java. ومع ذلك، ستنسى أحيانًا، وقد تكون النتيجة مقتعة إلى حدّ بعيد.

Java لغة موجّهة نحو الكائنات قائمة على الأصناف/الكائنات، بفضاءات منفصلة للمتغيّرات والدوال؛ أما JavaScript فلا.

تستخدم JavaScript كائنات النماذج الأولية (prototype)، ومعها نموذج الوراثة المترتب عليها. وهذا يعني أن كل الكائنات هي في جوهرها أزواج مفتاح/قيمة، يكون فيها كل مفتاح سلسلة نصية، بينما تكون القيم إمّا قيمًا أوّلية (primitives) أو مصفوفات أو كائنات أو دوال. ومن جهة الوراثة، تستطيع النماذج الأولية أن تمتدّ بنماذج أولية أخرى، لكن لا يوجد مفهوم حقيقي لـ«الصنف الأعلى» (superclass) و«الصنف الأدنى» (subclass). ولكي تعمل شيفرة كائنية موجّهة نحو الكائنات «مكتوبة الأسلوب الصحيح» كما في Java، اضطررنا إلى تنفيذ الوراثة الكلاسيكية (classical inheritance) لـ JavaScript داخل Processing.js، من دون أن نجعلها بطيئة بصورة بالغة (ونظن أننا نجحنا في هذه الناحية). واضطررنا كذلك إلى ابتكار طريقة تمنع أسماء المتغيّرات من الاصطدام بأسماء الدوال. وبطبيعة كائنات JavaScript المعتمدة على المفتاح/القيمة، فإن تعريف متغيّر اسمه line ثم تعريف دالة مثل line(x1,y1,x2,y2) يترك لك كائنًا يستخدم آخر ما جرى التصريح به للمفتاح نفسه. فـ JavaScript تضبط object.line = "some value" نيابةً عنك أوّلًا، ثم تضبط object.line = function(x1,y1,x2,y2){…}، متجاوزةً ما ظننت أن متغيّرك line عليه.

كان إنشاء إدارة منفصلة للمتغيّرات من جهة وللدوال من جهة أخرى سيُبطئ المكتبة كثيرًا، لذا تشرح الوثائق مرة أخرى أن استخدام متغيّر ودالة بالاسم نفسه فكرة سيّئة. ولو كان الجميع يكتب شيفرة «صحيحة النمط» لما كانت هذه مشكلة تُذكر، فنحن نسمّي المتغيّرات والدوال بحسب وظيفتها أو ما تفعله، لكن العالم الحقيقي يسير على نحو مختلف. أحيانًا لن تعمل شيفرتك، وذلك لأننا قررنا أن يكون انهيار شيفرتك بسبب تعارض في التسمية أفضل من أن تعمل شيفرتك دائمًا لكن تكون بطيئة دائمًا. وكان السبب الثاني لعدم تنفيذ فصل بين المتغيّرات والدوال أن ذلك قد يكسر شيفرة JavaScript المستخدَمة داخل الـ sketches الخاصة بـ Processing. فالإغلاقيات (closures) وسلسلة النطاق (scope chain) في JavaScript تعتمد على طبيعة المفتاح/القيمة في الكائنات، لذا فإن إحداث خلل فيها بكتابة إدارة خاصة بنا كان سيؤثّر بدوره بشدّة على الأداء من حيث التصريف الفوري (Just-In-Time) والضغط القائم على الإغلاقيات الوظيفية.

Java تسمح بالحمل الزائد (overloading) للدوال؛ أما JavaScript فلا.

من أقوى مزايا Java أنك تستطيع تعريف دالة، ولنفترض add(int,int)، ثم تعريف دالة أخرى بالاسم نفسه لكن بعدد وسائط مختلف، مثل add(int,int,int)، أو بأنواع وسائط مختلفة، مثل add(ComplexNumber,ComplexNumber). فاستدعاء add بوسيطَين أو بثلاثة وسائط صحيحة يستدعي تلقائيًا الدالة المناسبة، واستدعاء add بأعداد عشرية أو بكائنات Car يولّد خطأ. أما JavaScript، من جهة أخرى، فلا تدعم ذلك. ففي JavaScript الدالة خاصية (property)، ويمكنك أن تستخرج قيمتها بالتحويل غير المباشر (dereference) — وفي هذه الحالة تعطيك JavaScript قيمة استنادًا إلى الإكراه التلقائي للأنواع (type coercion)، وهو true هنا عندما تشير الخاصية إلى تعريف دالة، و false عندما لا تشير إليه — أو يمكنك استدعاؤها كدالة باستخدام معاملات التنفيذ (وهي ما تعرفه باسم أقواس تحوي صفرًا أوزيد من الوسائط بينها). فإذا عرّفت دالة باسم add(x,y) ثم استدعيتها باسم add(1,2,3,4,5,6)، فإن JavaScript لا تمانع ذلك. ستضبط x على 1 و y على 2 وتتجاهل بقية الوسائط ببساطة. ولجعل الحمل الزائد يعمل، فإننا نُعيد كتابة الدوال التي لها الاسم نفسه لكن عدد وسائط مختلف إلى دالة مرقّمة، بحيث يتحوّل function(a,b,c) في المصدر إلى function$3(a,b,c) في الشيفرة المُعاد كتابتها، ويتحوّل function(a,b,c,d) إلى function$4(a,b,c,d)، مما يضمن اختيار مسارات الشيفرة الصحيحة.

ولنا أيضًا أن حللنا، في معظمها، مشكلة الحمل الزائد للدوال التي لها العدد نفسه من الوسائط لكن بأنواع وسائط مختلفة، شرط أن تستطيع JavaScript تمييز تلك الأنواع على أنها مختلفة. فـ JavaScript تستطيع تحديد نوع خاصية وظيفيًا باستخدام المعامل typeof، الذي يُعيد إمّا number و string أو object أو function بحسب ما تمثّله الخاصية. فإعلان var x = 3 ثم تتبعه x = '6' سيجعل typeof x يُبلّغ عن number بعد الإعلان الأوّلي وعن string بعد إعادة الإسناد. ومتى اختلفت الدوال التي لها العدد نفسه من الوسائط في نوع الوسائط، فإننا نُعيد تسميتها ونبدّل بينها استنادًا إلى نتيجة عملية typeof. ولا يصلح هذا عندما تأخذ الدوال وسائط من نوع object، ولذلك نضيف لهذه الدوال فحصًا إضافيًا يستند إلى المعامل instanceof (الذي يُعيد اسم الدالة التي استُخدمت لإنشاء الكائن) لجعل الحمل الزائد للدوال يعمل. وفي الحقيقة، الموضع الوحيد الذي لا نستطيع فيه تصريف (transcompile) دوال محمَّلة زائدًا بنجاح هو موضع تساوي عدد الوسائط بين الدوال مع اختلاف أنواع الوسائط حين تكون هذه الأنواع عددية مختلفة. ولأن JavaScript لا تملك إلّا نوعًا عدديًا واحدًا، فإن تصريح دوال مثل add(int x, int y) و add(float x, float y) و add(double x, double y) سيتعارض. أما كل ما عداه فسيعمل على الوجه الصحيح.

Java تسمح باستيراد شيفرة مُصرَّفة.

أحيانًا لا تكفي Processing وحدها، فتُقدَّم وظائف إضافية في صورة مكتبة Processing. وتأتي هذه على هيئة ملف .jarchive يحوي شيفرة Java مُصرَّفة، ويقدّم أمورًا مثل الشبكات والصوت والفيديو والتخاطب مع العتاد وغيرها من الدوال غير المعتادة التي لا تغطيها Processing نفسها.

وهذه مشكلة، لأن شيفرة Java المُصرَّفة هي شيفرة بايت (byte code) لـ Java. وقد سبب لنا ذلك آلامًا كثيرة: فكيف ندعم استيراد المكتبات دون كتابة مفكّك (decompiler) لشيفرة بايت Java؟ وبعد نحو عام من النقاشات، استقرّنا على الحلّ الذي قد يبدو أبسط الحلول. وبدلًا من محاولة تغطية مكتبات Processing أيضًا، قررنا دعم الكلمة المفتاحية import داخل الـ sketches وإنشاء Processing.js Library API، بحيث يستطيع مطوّرو المكتبات كتابة نسخة JavaScript من مكتبتهم (حيثما ذلك ممكن، نظرًا لطبيعة الويب)، فإذا كتبوا حزمة تُستخدَم عبر import processing.video فإن Processing الأصلية ستلتقط ملف .jarchive، و Processing.js سيالتقط بدلًا منه processing.video.js، وبذلك تضمن أن الأمور «تعمل ببساطة». ومن المقرر أن تتوفّر هذه الوظيفة في Processing.js 1.4، واستيراد المكتبات هو آخر ميزة كبرى ما زالت ناقصة في Processing.js (فنحن حاليًا ندعم الكلمة المفتاحية import من الناحية المفاهيمية فقط، أي بحذفها من الشيفرة المصدرية قبل التحويل)، وستكون هذه آخر خطوة كبرى نحو التكافؤ.

لماذا نختار JavaScript مع أنه لا يستطيع تنفيذ ما تنفّذه Java؟

هذا ليس سؤالًا غير منطقي، وله إجابات متعددة. وأوضحها أن JavaScript تأتي مع المتصفح. فأنت لا «ثبِّت» JavaScript بنفسك، ولا توجد إضافة (plugin) تُنزَّل أولًا؛ إنها موجودة فحسب. فإذا أردت نقل شيء ما إلى الويب، فأنت محصور مع JavaScript. غير أن مرونة JavaScript تجعل عبارة «محصورًا معها» لا توفّر حقّ ما للغة من قوة. من ثمّ فإن أحد أسباب اختيار JavaScript هو «لأنها موجودة بالفعل». فكل جهاز قد يهمّك يأتي اليوم بمتصفح يدعم JavaScript. ولا يمكن قول الشيء ذاته عن Java، التي تُقدَّم باطراد أقلّ بوصفها تقنية مُثبَّتة مسبقًا، إن كانت متاحة أصلًا.

لكن الإجابة الأدقّ هي أنه ليس صحيحًا تمامًا القول بأن JavaScript «لا تستطيع» أن تفعل ما تفعله Java؛ فهي تستطيع، لكن ستكون أبطأ فحسب. فرغم أن JavaScript خارج الصندوق لا تستطيع تنفيذ بعض ما تنفّذه Java، فإنها تبقى لغة برمجة توريّة-كاملة (Turing-complete)، ويمكن جعلها تحاكي أي لغة برمجة أخرى، على حساب السرعة. ولنا تقنيًا أن نكتب مفسّرًا (interpreter) كاملًا لـ Java، بذاكرة كومة من نوع String، ونماذج منفصلة للمتغيّرات والدوال، وتوجيه نحو الكائنات قائم على الأصناف/الكائنات مع تسلّطات هرمية صارمة للصنف، وكل ما عداه في عهدة Sun (أو Oracle في هذه الأيام)، لكن هذا ليس ما نحن هنا من أجله: فغرض Processing.js هو تقديم تحويل من Processing إلى الويب، بأقل قدر من الشيفرة يلزم لذلك. وهذا يعني أنه رغم قرارنا بعدم جعله ينفّذ أمورًا بعينها خاصة بـ Java، فإن مكتبتنا تتمتع بفائدة واحدة هائلة: فهي تتعامل مع JavaScript المضمّنة على نحو جيّد إلى حدّ بعيد.

وفي الواقع، وخلال اجتماع بين أفراد Processing.js وأفراد Processing في Bocoup في بوسطن عام 2010، سأل Ben Fry من John Resig لماذا استخدم استبدال التعابير النمطية (regular expression replacement) وتحويلًا جزئيًا فحسب بدلًا من كتابة محلِّل ومُصرِّف ملائمين. وكانت إجابة John أنه من المهم لديه أن يتمكّن الناس من مزج صياغة Processing (أي Java) مع JavaScript دون أن يضطروا إلى الاختيار بينهما. وكان ذلك الاختيار الأوّلي حاسمًا في تشكيل فلسفة Processing.js منذ ذلك الحين. وقد اجتهدنا في الحفاظ على ذلك في شيفرتنا، ونرى عائدًا واضحًا حين نطالع جميع مستخدمي Processing.js «المحضين للويب» الذين لم يستخدموا Processing قطّ، والذين يسرّهم مزج صياغة Processing مع JavaScript دون أي مشكلة.

يبيّن المثال التالي كيف تعمل JavaScript و Processing معًا.

    // JavaScript (would throw an error in native Processing)
    var cs = { x: 50,
               y: 0,
               label: "my label",
               rotate: function(theta) {
                         var nx = this.x*cos(theta) - this.y*sin(theta);
                         var ny = this.x*sin(theta) + this.y*cos(theta);
                         this.x = nx; this.y = ny; }};

    // Processing
    float angle = 0;

    void setup() {
      size(200,200);
      strokeWeight(15); }

    void draw() {
      translate(width/2,height/2);
      angle += PI/frameRate;
      while(angle>2*PI) { angle-=2*PI; }
      jQuery('#log').text(angle); // JavaScript (error in native Processing)
      cs.rotate(angle);           // legal JavaScript as well as Processing
      stroke(random(255));
      point(cs.x, cs.y); }

الكثير من الأمور في Java هي وعود (promises): فالتنميط الصارم للأنواع وعدٌ للمُصرِّف بأن المحتوى مطابق لما هو معلَن، والرؤية (visibility) وعدٌ بشأن من سيستدعي الدوال ويشير إلى المتغيّرات، والواجهات (interfaces) وعدٌ بأن النسخ (instances) تحتوي على الدوال التي تصفها الواجهة، وغيرها. ومن يخلف تلك الوعود ينبري المُصرِّف بالشكوى. لكن إن لم تخلفها — وهذه إحدى أكثر الأفكار أهميةً بالنسبة إلى Processing.js — فإنك لا تحتاج إلى الشيفرة الإضافية التي تُجسّد تلك الوعود كي يعمل البرنامج. فإذا وضعت عددًا في متغيّر، وتعاملت شيفرتك مع ذلك المتغيّر وكأنه يحمل عددًا، فإن var varname في النهاية لا يقلّ عن int varname. أتحتاج إلى التنميط؟ في Java تحتاج إليه، وفي JavaScript لا تحتاج إليه، فلماذا تفرضه؟ وينطبق الأمر نفسه على وعود الشيفرة الأخرى. فإذا لم يشتكِ مُصرِّف Processing من شيفرتك، يمكننا تجريد كل الصياغة الصريحة التي تُجسّد تلك الوعود، وستظل تعمل على نحو سليم.

وقد جعل هذا Processing.js مكتبة مفيدة إلى حدّ المبالغة في تصوير البيانات (data visualisation) وعرض الوسائط وحتى الترفيه. فالـ sketches في Processing الأصلية تعمل، لكن الـ sketches التي تمزج Java و JavaScript تعمل أيضًا على نحو سليم، وكذلك الـ sketches التي تستخدم JavaScript خالصة بمعاملة Processing.js بوصفه إطار رسم على canvas مُنمّقًا. وفي مسعى إلى بلوغ التكافؤ مع Processing الأصلية، دون فرض صياغة خاصة بـ Java وحدها، لقي المشروع جمهورًا بعرض الويب نفسه. وقد رأينا نشاطًا في أنحاء الويب كلها باستخدام Processing.js. فكل جهة، من IBM إلى Google، بنت تصويرات بيانية وعروضًا تقديمية وحتى ألعابًا باستخدام Processing.js—Processing.js يصنع فرقًا حقيقيًا.

ومن المزايا الرائعة الأخرى لتحويل صياغة Java إلى JavaScript مع إبقاء JavaScript كما هو، أننا أتاحنا شيئًا لم نفكّر فيه أصلًا: فـ Processing.js سيعمل مع أي شيء يعمل مع JavaScript. ومن أمتع ما نرااه الآن، مثلًا، أن الناس يستخدمون CoffeeScript (لغة برمجة بسيطة ورائعة شبيهة بـ Ruby تُصرَّف إلى JavaScript) بالاقتران مع Processing.js، بنتائج بارعة حقًّا. فرغم أننا انطلقنا لبناء «Processing للويب» بالاعتماد على تحليل صياغة Processing، إلا أن الناس أخذوا ما صنعناه واستخدموه مع صياغات جديدة تمامًا. ولم يكونوا ليعجزوا عن ذلك لو جعلنا Processing.js مجرّد مفسّر لـ Java. فبالتمسك بتحويل الشيفرة بدلًا من كتابة مفسّر شيفرة، منح Processing.js لغة Processing انتشارًا على الويب يفوق كثيرًا ما كانت لتناله لو ظلت خاصة بـ Java وحدها، أو حتى لو أبقينا لها صياغة Java وحدها مع تولّي JavaScript للتنفيذ على الويب. وقد كان انتشار شيفرتنا لا لدى المستخدمين النهائيين فحسب، بل لدى من يحاولون دمجها مع تقنياتهم الخاصة، مذهلًا وملهمًا في آن. ومن الواضح أننا نفعل شيئًا صحيحًا، ويبدو أن الويب راضٍ عما نفعله.

النتيجة

مع اقترابنا من Processing.js 1.4.0، فقد أنتج عملنا مكتبة ستشغّل أي sketch تعطيها إياه، شرط ألّا يعتمد على استيراد مكتبات Java المُصرَّفة. فإن كنت تكتبه بـ Processing ويعمل، تستطيع وضعه على صفحة ويب وسيعمل ببساطة. وبسبب الفروق في الوصول إلى العتاد وفي التطبيقات منخفضة المستوى لأجزاء مختلفة من خط أنابيب العرض (rendering pipeline)، ستكون هناك فروق في التوقيت، لكن بشكل عام فإن الـ sketch الذي يعمل بمعدل 60 إطارًا في الثانية في Processing IDE سيعمل بمعدل 60 إطارًا في الثانية على حاسوب حديث مع متصفح حديث. وقد بلغنا مرحلة بدأت فيها تقارير الأخطاء تخفّ، ولم يعد معظم العمل يتعلق بإضافة دعم للميزات، بل أكثر بإصلاح الأخطاء وتحسين الشيفرة.

وبفضل جهود المطوّرين الكثيرين في معالجة أكثر من 1800 تقرير خطأ، فإن sketches الخاصة بـ Processing تعمل باستخدام Processing.js «تعمل ببساطة». وحتى الـ sketches التي تعتمد على استيراد المكتبات يمكن جعلها تعمل، شريطة توفّر شيفرة المكتبة. وفي الظروف المواتية تكون المكتبة مكتوبة على نحو يتيح لك إعادة كتابتها إلى شيفرة Processing خالصة عبر بضع عمليات بحث واستبدال (search-replace). وفي هذه الحالة يمكن جعل الشيفرة تعمل على الويب خلال لحظات معدودة. وحينما تقوم المكتبة بأمر ما لا يمكن تنفيذه في Processing خالص، لكنه يمكن تنفيذه باستخدام JavaScript المجرّدة، يلزم جهد أكبر لمحاكاة المكتبة على نحو فعّال بشيفرة JavaScript، ومع ذلك فإن النقل يبقى ممكنًا. والحالات الوحيدة من شيفرة Processing التي يتعذّر نقلها هي تلك التي تعتمد على وظائف لا تتاح للمتصفحات بطبيعتها، مثل التفاعل المباشر مع أجهزة العتاد (ككاميرات الويب أو لوحات Arduino) أو إجراء كتابات غير مراقَبة على القرص، وإن كان هذا أيضًا آخذًا في التغيّر. فالمتصفحات تضيف باستمرار وظائف تتيح تطبيقات أعقد، وقد تختفي العوامل المقيِّدة اليوم بعد عام، ومن المرجّح على الأرجح أن تصبح، في المستقبل غير البعيد، حتى الـ sketches التي يتعذّر اليوم تشغيلها على الويب قابلة للنقل.

17.3. مكوّنات الشيفرة

يُقدَّم Processing.js ويُطوَّر بوصفه ملفًا واحدًا كبيرًا، لكنه من الناحية المعمارية يمثّل ثلاثة مكوّنات مختلفة: 1) المُشغِّل (launcher)، المسؤول عن تحويل شيفرة Processing إلى JavaScript بنكهة Processing.js وتنفيذها، و2) وظائف ساكنة (static) يمكن أن تستخدمها جميع الـ sketches، و3) وظائف خاصة بـ sketch يجب ربطها بنسخ (instances) فردية.

المُشغِّل (launcher)

يتولّى مكوّن المُشغِّل ثلاثة أمور: المعالجة الأولية للشيفرة، وتحويل الشيفرة، وتنفيذ الـ sketch.

المعالجة الأولية (preprocessing)

في خطوة المعالجة الأولية، تُفصل تعليمات (directives) Processing.js عن الشيفرة وتُنفَّذ. ويأتي هذه التعليمات في صنفين: إعدادات وتعليمات تحميل. وعدد التعليمات صغير، تمسًّا بفلسفة «ينبغي أن تعمل ببساطة»، والإعدادات الوحيدة التي يستطيع مؤلفو الـ sketches تغييرها متعلقة بتفاعل الصفحة. وافتراضيًا سيستمر الـ sketch في العمل إذا لم تكن الصفحة في بؤرة التركيز (focus)، لكن التعليم pauseOnBlur = true يُعدّ الـ sketch بحيث يوقف التنفيذ حين لا تكون الصفحة التي يعمل عليها في بؤرة التركيز، ويعيد التنفيذ حين تعود الصفحة إلى بؤرة التركيز. وكذلك افتراضيًا لا يُوجَّه إدخال لوحة المفاتيح إلى sketch إلا حين يكون هو في بؤرة التركيز. وهذا مهم بصفة خاصة حين يشغّل الناس أكثر من sketch على الصفحة نفسها، إذ لا ينبغي أن يعالج إدخال لوحة المفاتيح الموجَّه إلى sketch آخرَه. غير أنه يمكن تعطيل هذه الوظيفة، فتُوجَّه أحداث لوحة المفاتيح إلى كل sketch يعمل في الصفحة، بالاستخدام التعليم globalKeyEvents = true.

وتأخذ تعليمات التحميل شكل التحميل المسبق للصور والخطوط الذي سبق ذكره. ولأن الصور والخطوط يمكن أن تستخدمها عدة sketches، فهي تُحمَّل وتُتابَع عالميًا، حتى لا تحاول sketches مختلفة تنفيذ عمليات تحميل متعددة للمورد نفسه.

تحويل الشيفرة

يفكّك مكوّن تحويل الشيفرة الشيفرة المصدرية إلى عُقد شجرة الصياغة المجرّدة (AST)، مثل الجمل (statements) والتعابير (expressions) والدوال والمتغيّرات والأصناف وغيرها. ثم تُوسَّع شجرة الصياغة هذه إلى شيفرة JavaScript تبني برنامجًا مكافئًا للـ sketch عند تنفيذه. وتشدّد هذه الشيفرة المحوَّلة على إطار النسخ (instance framework) في Processing.js لإعداد علاقات الأصناف، حيث تتحوّل الأصناف في شيفرة Processing المصدرية إلى نماذج أولية (prototypes) في JavaScript مع دوال خاصة لتحديد الأصناف الأعلى ولربط دوال ومتغيّرات الصنف الأعلى.

تنفيذ الـ sketch

الخطوة الأخيرة في عملية الإقلاع هي تنفيذ الـ sketch، وتتألف من تحديد ما إذا كان التحميل المسبق قد انتهى كلّه، وإن كان كذلك، فعندها نضيف الـ sketch إلى قائمة النسخ قيد التشغيل ونُطلق حدث JavaScript ‏onLoad الخاص به، ليتخذ مستمعو الـ sketch أي إجراء مناسب. وبعد ذلك تُنفَّذ سلسلة Processing: ‏setup ثم draw، وإذا كان الـ sketch من النوع الذي يعمل في حلقة (looping sketch)، نضبط استدعاءً دوريًا لـ draw بطول فترة يقارب أقرب ما يمكن لمعدل الإطارات (framerate) المطلوب للـ sketch.

المكتبة الساكنة (static library)

يقع معظم Processing.js تحت عنوان «المكتبة الساكنة»، ممثّلًا الثوابت والدوال العامة وأنواع البيانات العامة. وكثير من هذه يؤدي في الواقع مهمّة مزدوجة: إذ يُعرَّف كخصائص عامة (global properties)، ثم يُسنَد إلى النسخ (instances) بوصفه اسمًا مستعارًا (alias) للحصول على مسارات شيفرة أسرع. أما الثوابت العامة مثل رموز المفاتيح (key codes) وخريطة الألوان (color mappings) فتُحفظ داخل كائن Processing نفسه، وتُهيَّأ مرة واحدة، ثم يُشار إليها عند بناء النسخ عبر مُنشئ (constructor) Processing. وينطبق الأمر نفسه على دوال المساعدة المستقلّة، مما يتيح لنا إبقاء الشيفرة أقرب ما يمكن إلى «اكتبها مرة واحدة، وشغّلها في أي مكان» من دون التضحية بالأداء.

وعلى Processing.js أن يدعم عددًا كبيرًا من أنواع البيانات المعقّدة، ليس فقط دعمًا لأنواع البيانات المستخدَمة في Processing، بل أيضًا لأسباب تتعلق بآلياته الداخلية. وهذه الأنواع معرَّفة أيضًا في مُنشئ Processing:

  • ‏Char، وهو كائن داخلي يُستخدم للتغلب على بعض غرائب سلوك نوع البيانات char في Java.
  • ‏PShape، ويمثّل كائنات الأشكال.
  • ‏PShapeSVG، وهو امتداد لكائنات PShape، يُبنى من XML بصيغة SVG ويمثّله. أما PShapeSVG فقد نفّذنا له شيفرة خاصة تحوّل تعليمات SVG إلى تعليمات ``. ولأن Processing لا يوفّر دعماً كاملًا لـ SVG، فإن الشيفرة التي وفّرناها بعدم الاعتماد على مكتبة SVG خارجية تعني أننا نستطيع أن نستوعب كل سطر شيفرة يتعلق باستيرادات SVG. فهو لا يحلّل إلّا ما يجب أن يحلّله، ولا يهدر مساحةً بشيفرة تتبع المواصفة (spec) لكنها غير مستخدَمة لأن Processing الأصلية لا تدعمها.
  • ‏XMLElement، وهو كائن مستند XML. وقد نفّذنا لـ XMLElement أيضًا شيفرة خاصة، معتمدين على المتصفح أوّلًا لتحميل عنصر XML في بنية قائمة على Node، ثم اجتياز بنية العُقد لبناء كائن أكثر رشاقة. وهذا أيضًا يعني أننا لا نحتفظ داخل Processing.js بأي شيفرة ميتة تشغل مساحة وتسبّب أخطاءً محتملة لأن ترقيعًا ما (patch) استعمل بالخطأ دالةً لا وجود لها.
  • ‏PMatrix2D و PMatrix3D، تؤدّيان عمليات المصفوفات في وضعي الأبعادين والأبعاد الثلاثة.
  • ‏PImage، ويمثّل مورد صورة. وهو في الحقيقة غلاف (wrapper) لكائن Image، مع بعض الدوال والخصائص الإضافية كي تطابق واجهته البرمجية واجهة Processing API.
  • ‏PFont، ويمثّل مورد خط. ولا يوجد كائن Font معرَّف لـ JavaScript (على الأقل في الوقت الحالي)، لذا فإن تنفيذنا لـ PFont لا يخزّن الخط بوصفه كائنًا، بل يحمّله عبر المتصفح، ويحسب مقاييسه بناءً على كيفية عرض المتصفح للنص به، ثم يخزّن (caches) كائن PFont الناتج. ولأداءٍ أعلى، تحمل كائنات PFont إشارةً إلى اللوحة (canvas) التي استُخدمت لتحديد خصائص الخط، تحسّبًا للحاجة إلى حساب textWidth، لكن لأننا نتتبّع كائنات PFont على أساس زوج الاسم/الحجم، فإن كان الـ sketch يستخدم أحجام نصوص متباينة كثيرة، أو خطوطًا بوجه عام، فسيستهلك ذلك ذاكرةً أكثر من اللازم. ولهذا، تمسح كائنات PFont لوحتها المخزَّنة مؤقتًا وتستدعي بدلًا منها دالة عامة لحساب textWidth عندما ينمو المخزن المؤقت أكثر من اللازم. وكاستراتيجية ثانوية لتوفير الذاكرة، إذا ظل مخزن الخطوط يكبر بعد مسح اللوحة المخزّنة لكل PFont، فإن التخزين المؤقت للخطوط يُعطَّل كليًا، وتغييرات الخط في الـ sketch فتكتفي بأن تبسّط إنشاء كائنات PFont مؤقّتة المهملة مع كل تغيير في اسم الخط أو حجم النص أو تباعد الأسطر (leading).
  • ‏DrawingShared و Drawing2D و Drawing3D، وتجمع جميع دوال الرسوميات. وكائن DrawingShared هو في الواقع أكبر فخّ للأداء في Processing.js. فهو يحدّد ما إذا كان الـ sketch يُقلَع في وضع الأبعادين أم الأبعاد الثلاثة، ثم يعيد ربط جميع دوال الرسوميات إمّا بكائن Drawing2D أو بكائن Drawing3D. ويؤكّد هذا مسارًا قصيرًا لتعليمات الرسوميات، لأن sketches الخاصة بـ Processing ثنائية الأبعاد لا يمكنها استخدام دوال ثلاثية الأبعاد، والعكس. وبربط إحدى مجموعتَي دوال الرسوميات فقط، نكسب سرعةً من عدم الحاجة إلى التبديل على وضع الرسوميات في كل دالة لتحديد مسار التنفيذ، ونوفّر مساحةً عدم ربط دوال الرسوميات التي لا يمكن أن تُستخدم قط.
  • ‏ArrayList، حاوية تحاكي ArrayList في Java.
  • ‏HashMap، حاوية تحاكي HashMap في Java. وArrayList و HashMap تحديدًا هما بنى بيانات خاصة بسبب طريقة تنفيذ Java لهما. فهاتان الحاويتان تعتمدان على مفهومَي التساوي (equality) والتجزئة (hashing) في Java، ولكل كائنات Java على حدّ سواء دالة equals ودالة hashCode تتيحان تخزينها في القوائم والخرائط. أما الحاويات غير القائمة على التجزئة فتُحلّ الكائنات فيها على أساس التساوي لا على أساس الهوية (identity). وعليه، يمرّ list.remove(myobject) على القائمة بحثًا عن عنصر تتحقق فيه element.equals(myobject) — لا element == myobject —. ولأن كل كائن يجب أن يملك دالة equals، نفّذنا دالة «equals افتراضية» (virtual equals) على جانب JavaScript من الأمور. تأخذ هذه الدالة كائنين كوسومين، وتتحقق مما إذا كان أيٌّ منهما ينفّذ دالة equals خاصة به، فإن كان الأمر كذلك فإن التنفيذ ينفذ إلى تلك الدالة. وإن لم يكنا كذلك، وكان الكائنان الممرَّران قيمًا أوّلية (primitives)، فيُتحقَّق من تساوي القيم الأولية. وإن لم يكونا كذلك، فلا يوجد تساوٍ. أما حاويات التجزئة فالأمر فيها أكثر إثارة، إذ تعمل حاويات التجزئة كأشجار اختصارات. فالحاوية تغلّف فعليًا عددًا متغيّرًا من القوائم، كل واحدة مربوطة برمز تجزئة محدّد. وتُعثر على الكائنات أوّلًا بإيجاد الحاوية التي تطابق رمز تجزئتها، ثم يُبحث عن الكائن داخلها على أساس تقييم التساوي. ولأن كل كائنات Java لها دالة hashCode، فقد كتبنا أيضًا دالة «hashcode افتراضية» (virtual hashcode) تأخذ كائنًا واحدًا كوسيط. وتتحقق الدالة مما إذا كان الكائن ينفّذ دالة hashCode خاصة به، فإن كان الأمر كذلك فإن التنفيذ ينفذ إلى تلك الدالة. وإلا فحُسب رمز التجزئة على أساس خوارزمية التجزئة ذاتها المستخدمة في Java.

الإدارة (administration)

آخر قطعة من الوظائف في مكتبة الشيفرة الساكنة هي قائمة النسخ الخاصة بكل الـ sketches التي تعمل حاليًا في الصفحة. وتخزّن قائمة النسخ هذه الـ sketches بحسب اللوحة (canvas) التي حُمِّلت فيها، حتى يستطيع المستخدمون استدعاء Processing.getInstanceById('canvasid') والحصول على مرجع إلى الـ sketch الخاص بهم لأغراض التفاعل مع الصفحة.

شيفرة النسخ (instance code)

تأخذ شيفرة النسخ شكل تعريفات p.functor = function(arg, …) الخاصة بواجهة Processing API، و p.constant = … الخاصة بمتغيّرات حالة الـ sketch (حيث p هو مرجعنا إلى الـ sketch الجاري تهيئته). وليست أيٌّ من هاتين موجودًا في كتل شيفرة مخصّصة. إنما تُنظَّم الشيفرة على أساس الوظيفة، بحيث تُعرَّف شيفرة النسخ المتعلقة بعمليات PShape قرب كائن PShape، وتُعرَّف شيفرة النسخ الخاصة بدوال الرسوميات قرب كائنَي Drawing2D و Drawing3D أو بداخلهما.

وللحفاظ على سرعة التنفيذ، فإن كثيرًا من الشيفرة التي يمكن كتابتها كشيفرة ساكنة بغلاف نسخة (instance wrapper) منفَّذة فعليًا كشيفرة نسخ خالصة. فمثلًا، دالة lerpColor(c1, c2, ratio) التي تحدّد اللون المقابل للاستيفاء الخطي (linear interpolation) بين لونين، معرَّفة كدالة نسخة. فبدلًا من جعل p.lerpColor(c1, c2, ratio) غلافًا لدالة ساكنة ما بوصفها Processing.lerpColor(c1, c2, ratio)، فإن عدم اعتماد أي شيء آخر في Processing.js على lerpColor يعني أن تنفيذ الشيفرة أسرع إن كتبناها كدالة نسخة خالصة. ورغم أن هذا «يُضخّم» كائن النسخة، فإن معظم الدوال التي نُلحّ على جعلها دوال نسخ بدلًا من أغلفة تستدعي المكتبة الساكنة هي دوال صغيرة. وبذلك فإننا، مقابل تضحية في الذاكرة، نشئ مسارات شيفرة سريعة إلى حدّ بعيد. فرغم أن كائن Processing الكامل سيحتلّ شريحة ذاكرة لمرة واحدة تعادل نحو 5 ميغابايت عند تهيئته الأوّلي، فإن الشيفرة المتطلّبة لـ sketches الفردية لا تشغل سوى نحو 500 كيلوبايت.

17.4. تطوير Processing.js

يُعمل على Processing.js بكثافة، وهو ما نستطيع فعله فقط لأن منهجنا في التطوير يتمسّك بقواعد أساسية قليلة. ولأن هذه القواعد تؤثّر في بنية Processing.js المعمارية، فمن المفيد أن نلقي نظرة سريعة عليها قبل إنهاء هذا الفصل.

اجعله يعمل

كتابة شيفرة تعمل تبدو مقدّمة بديهية (tautological)؛ فأنت تكتب الشيفرة، فعند انتهاء عملك تكون إما قد عملت — لأن هذا ما قصدتَه — أو لم تعمل، ولم تنتهِ بعد. غير أن عبارة «اجعله يعمل» ترافقها نتيجة تلتزم بها: اجعله يعمل، وحين تنتهي، أثبت ذلك.

إن كان هناك أمر واحد فوق كل ما عداه سمح لـ Processing.js بالنمو بالمعدل الذي نما به، فهو وجود الاختبارات (tests). فأي تذكرة (ticket) تتطلب المساس بالشيفرة، سواء بكتابة شيفرة جديدة أو بإعادة كتابة شيفرة قديمة، لا يمكن وسمها بأنها محسومة قبل أن يتوفّر اختبار وحدة (unit test) أو اختبار مرجعي (reference test) يتيح للآخرين التحقق ليس فقط من أن الشيفرة تعمل كما ينبغي، بل وأنها تنكسر متى ينبغي. وبالنسبة إلى معظم الشيفرة، يتضمّن ذلك عادةً اختبار وحدة—أي قطعة شيفرة قصيرة تستدعي دالة وتختبر ببساطة ما إذا كانت الدالة تُعيد القيم الصحيحة، في حالة الاستدعاءات القانونية وغير القانونية على حدّ سواء. وهذا لا يتيح لنا اختبار مساهمات الشيفرة فحسب، بل يتيح لنا أيضًا إجراء اختبارات الانحدار (regression tests).

وقبل قبول أي شيفرة ودمجها في فرع التطوير المستقر لدينا، تخضع مكتبة Processing.js المعدَّلة للتحقق في مواجهة مجموعة اختبارات وحدات تنمو باستمرار. والإصلاحات الكبيرة واختبارات الأداء بوجه خاص معرَّضة لأن تنجح اختبارات وحدات الخاصة بها، لكنها تكسر أجزاء كانت تعمل جيدًا قبل إعادة الكتابة. ووجود اختبارات لكل دالة في الواجهة البرمجية، وكذلك للدوال الداخلية، يعني أنه مع نمو Processing.js لا نكسر عن غير قصد التوافق مع الإصدارات السابقة. فباستثناء تغييرات الواجهة المدمِّرة، إذا لم يفشل أي اختبار قبل مساهمة أو تعديل شيفرة، فلا يُسمح لأي اختبار أن يفشل مع الشيفرة الجديدة.

إليك مثالًا على اختبار وحدة يتحقّق من إنشاء كائنات مضمّنة (inline).

    interface I {
      int getX();
      void test(); }

    I i = new I() {
      int x = 5;
      public int getX() {
        return x; }
      public void test() {
        x++; }};

    i.test();

    _checkEqual(i.getX(), 6);
    _checkEqual(i instanceof I, true);
    _checkEqual(i instanceof Object, true);

إلى جانب اختبارات وحدات الشيفرة المعتادة، لدينا أيضًا اختبارات مرجعية بصرية (أو «ref»). ولأن Processing.js منقول عن لغة برمجة بصرية، فإن بعض الاختبارات لا يمكن إجراؤها بوصفها اختبارات وحدات وحدها. فاختبار ما إذا كان بيضاوي (ellipse) قد رُسم على البكسلات الصحيحة، أو ما إذا كان خط رأسي عرضه بكسل واحد قد رُسم حادًّا أم مموَّهًا لا يمكن تحديده دون مرجع بصري. ولأن كل المتصفحات الرئيسية تنفّذ عنصر `` و Canvas2D API بفروق دقيقة، فلا يمكن اختبار هذه الأمور إلا بتشغيل الشيفرة في متصفح والتحقّق من أن الـ sketch الناتج يبدو مطابقًا لما ينتجه Processing الأصلية. ولتسهيل الأمر على المطوّرين، نستخدم مجموعة اختبارات آلية لهذا الغرض، حيث تُشغَّل حالات الاختبار الجديدة عبر Processing، فتولّد بيانات «كيف ينبغي أن يبدو» تُستخدم في مقارنة البكسلات. ثم تُخزَّن هذه البيانات كتعليق داخل الـ sketch الذي ولّدها، لتشكّل اختبارًا، وتُشغَّل هذه الاختبارات بعد ذلك بواسطة Processing.js في صفحة اختبار مرجعي بصري تنفّذ كل اختبار وتُجري مقارنات بكسلية بين «كيف ينبغي أن يبدو» و«كيف يبدو فعلًا». وإذا اختلفت البكسلات، يفشل الاختبار، ويُعرض للمطوّر ثلاث صور: كيف ينبغي أن يبدو، وكيف عرضه Processing.js، والفرق بينهما، مع تحديد المناطق المشكِلة بالبكسلات الحمراء، والصحيحة بالبكسلات البيضاء. وكما في اختبارات الوحدات، يجب أن تنجح هذه الاختبارات قبل قبول أي مساهمة شيفرة.

اجعله سريعًا

في مشروع مفتوح المصدر، جعل الأمور تعمل ليس سوى الخطوة الأولى في حياة الدالة. فمتى عملت الأمور، تريد أن تتأكد من أنها تعمل بسرعة. واستنادًا إلى مبدأ «إن لم تستطع قياسه فلا يمكنك تحسينه»، فإن معظم دوال Processing.js لا تأتي مع اختبارات الوحدات أو الاختبارات المرجعية فحسب، بل ومع اختبارات الأداء (أو «perf»). فقطع الشيفرة الصغيرة التي تستدعي دالة فحسب، دون اختبار صحّتها، تُشغَّل بضع مئات من المرات متتالية، ويُسجَّل زمن تنفيذها في صفحة ويب خاصة باختبارات الأداء. وهذا يتيح لنا تحديد مدى جودة أداء Processing.js (أو سوءه!) في المتصفحات التي تدعم عنصر `` الخاص بـ HTML5. وكلما اجتاز ترقيع تحسين (optimization patch) اختبارَي الوحدات والمرجع، يُمرَّر عبر صفحة اختبارات الأداء لدينا. وJavaScript وحشٌ فضولي، ويمكن في الواقع أن تكون الشيفرة الجميلة أبطأ بمقدار عدّة رتب حجم (orders of magnitude) من شيفرة تتكرّر فيها الأسطر نفسها عدّة مرات عبر شيفرة مضمّنة (inline) بدلًا من نداءات الدوال. وهذا يجعل اختبار الأداء بالغ الأهمية. فقد تمكّنّا من تسريع أجزاء معيّنة من المكتبة بمقدار ثلاث رتب حجم، لمجرد اكتشاف الحلقات الساخنة (hot loops) أثناء اختبار الأداء، وتقليل عدد نداءات الدوال عبر تضمين الشيفرة، وجعل الدوال تُعيد القيمة في اللحظة التي تعرف فيها ما ينبغي أن تكونه قيمة الإعادة، بدلًا من الاكتفاء بإعادة واحدة في نهاية الدالة تمامًا.

ومن طرائق أخرى التي نحاول بها تسريع Processing.js، النظر فيما يشغّله. وبما أن Processing.js يعتمد بشدّة على كفاءة محرّكات JavaScript، فمن المنطقي أيضًا أن ننظر في المزايا التي تقدّمها المحرّكات المختلفة لتسريع الأمور. وخاصةً الآن بعد أن بدأت المتصفحات في دعم الرسوميات المسرَّعة بالعتاد (hardware-accelerated graphics)، فإن تسريعًا فوريًا يصبح ممكنًا عندما تقدّم المحرّكات أنواع بيانات ودوالًا جديدة وأكثر كفاءة لتنفيذ العمليات منخفضة المستوى التي يعتمد عليها Processing.js. فمثلًا، لا تملك JavaScript تقنيًا تنميطًا ساكنًا (static typing)، لكن بيئات برمجة عتاد الرسوميات تملكه. ومن خلال إتاحة بنى البيانات المستخدَمة في مخاطبة العتاد مباشرةً أمام JavaScript، من الممكن تسريع أجزاء من الشيفرة تسريعًا ملموسًا إن كنا نعرف أنها لن تستعمل سوى قيم محدّدة.

اجعله صغيرًا

هناك طريقتان لتصغير الشيفرة. الأولى، كتابة شيفرة مضغوطة. فإذا كنت تعدّل متغيّرًا عدّة مرّات، فاضغط التعديلات في عملية واحدة (إن أمكن). وإذا كنت تصل إلى خاصية كائن عدّة مرّات، فاخزّنها مؤقتًا (cache). وإذا كنت تستدعي دالة عدّة مرّات، فاخزّن نتيجتها مؤقتًا. وأعِد القيمة متى جمعت كل المعلومات التي تحتاجها، وطبّق عمومًا كل الحيل التي يطبّقها مُحسِّن الشيفرة بنفسه. وJavaScript لغة جميلة على نحو خاص في هذا، إذ تأتي بمرونة مذهلة. فمثلًا، بدلًا من استخدام:

if ((result = functionresult)!==null) {
  var = result;
} else {
  var = default;
}

يصبح في JavaScript:

var = functionresult || default

وهناك أيضًا شكل آخر من الشيفرة الصغيرة، ويتعلق بشيفرة وقت التشغيل (runtime). ولأن JavaScript تتيح لك تغيير ارتباطات الدوال (function bindings) أثناء التنفيذ، يصغر شيفرة التشغيل كثيرًا إن استطعت أن تقول «اربط دالة line2D بنداء دالة line» لمّا تعرف أن البرنامج يعمل في وضع الأبعادين لا الأبعاد الثلاثة، بحيث لا تضطر إلى تنفيذ:

if(mode==2D) { line2D() } else { line3D() }

مع كل نداء دالة قد يكون في وضع الأبعادين أو الأبعاد الثلاثة.

وأخيرًا، هناك عملية التصغير (minification). وهناك عدد من الأنظمة الجيدة التي تتيح ضغط شيفرة JavaScript عبر إعادة تسمية المتغيّرات، وإزالة المسافات البيضاء (whitespace)، وتطبيق تحسينات شيفرة معيّنة يصعب تنفيذها يدويًا مع الإبقاء على الشيفرة قابلة للقراءة. ومن أمثلتها مُصغِّر YUI ومُصرِّف الإغلاق (closure compiler) من Google. ونستخدم هذه التقنيات في Processing.js لتوفير راحة في استهلاك النطاق الترددي (bandwidth) للمستخدمين النهائيين—فالتصغير بعد إزالة التعليقات يمكن أن يصغّر المكتبة بما يصل إلى 50%، ومن خلال الاستفادة من التفاعل الحديث بين المتصفح والخادم في تقديم محتوى مضغوط (gzipped)، يمكننا تقديم مكتبة Processing.js كاملةً في صورة مضغوطة ضمن 65 كيلوبايت.

إن فشل كل شيء آخر، أخبر الناس

ليس كل ما يمكن إنجازه اليوم في Processing قابلًا للتنفيذ في المتصفح. فالنماذج الأمنية (security models) تمنع أمورًا معينة مثل حفظ الملفات على القرص الصلب وإجراء إدخال/إخراج عبر USB أو المنفذ التسلسلي (serial port)، وغياب التنميط في JavaScript قد تكون له عواقب غير متوقّعة (مثل أن تكون كل الحسابات حسابية عشرية متحركة). وأحيانًا نواجه خيارًا بين إضافة كمية هائلة من الشيفرة لتفعيل حالة حدّية (edge case)، أو وسم التذكرة بأنها مسألة «won’t fix» لن تُصلَح. وفي هذه الحالات، تُفتح تذكرة جديدة، يكون عنوانها عادةً «Add documentation that explains why…».

وحرصًا على ألا تضيع هذه الأمور، لدينا وثائق لمن يبدأ باستخدام Processing.js بخلفية في Processing، وآخر لمن يبدأ باستخدام Processing.js بخلفية في JavaScript، تغطّي الفروق بين ما هو متوقَّع وما يحدث فعلًا. فبعض الأمور تستحقّ ذكرًا خاصًا، لأننا مهما بذلنا من جهد في Processing.js، هناك أمور لا يمكننا إضافتها من دون التضحية بقابلية الاستخدام. فالعمارة الجيدة لا تغطّي الطريقة التي تسير بها الأمور فحسب، بل تغطّي أيضًا سببها؛ وبدون ذلك، ستنتهي بك المطابع بين حين وآخر في مناقشة شكل الشيفرة وما إذا كان ينبغي أن تكون مختلفة.

17.5. الدروس المستفادة

أهم درس تعلّمناه أثناء كتابة Processing.js هو أن نقل لغة (porting) يعني أن ما يهم هو أن تكون النتيجة صحيحة، لا أن تكون الشيفرة المستخدمة في النقل شبيهة بالأصل أو مختلفة عنه. فرغم أن صياغة Java و JavaScript متقاربة إلى حدٍّ كبير، وأن تعديل شيفرة Java ليصبح شيفرة JavaScript سليمة أمر سهل نوعًا ما، فإنك غالبًا تستفيد من النظر في ما يمكن لـ JavaScript أداؤه أصلًا واستغلاله للحصول على النتيجة الوظيفية ذاتها. فاستغلال غياب التنميط عبر إعادة استخدام المتغيّرات، أو استعمال دوال مدمجة معيّنة سريعة في JavaScript وبطيئة في Java، أو تجنّب أنماط سريعة في Java وبطيئة في JavaScript، معناه أن شيفرتك قد تبدو مختلفة جذريًا، لكن أثرها هو نفسه بالضبط. وكثيرًا ما تسمع الناس يقولون لا تعِد اختراع العجلة، لكن ذلك ينطبق فقط على العمل بلغة برمجة واحدة. أما حين تكون في صدد النقل، فأعِد اختراع ما تشاء من العجلات إلى الحدّ الذي تحصل به على الأداء الذي تحتاجه.

الدرس المهم الآخر هو أن تُعيد مبكّرًا، وتُعيد كثيرًا، وتتفادى التفريع قدر الإمكان. فجملة if/then يليها return يمكن جعلها أسرع (أحيانًا بدرجة هائلة) باستخدام صيغة if-return/return بدلًا من ذلك، مع استعمال جملة return كاختصار مشروط (conditional shortcut). ورغم أن تجميع حالة دالتك بالكامل قبل استدعاء جملة الإعادة النهائية لتلك الدالة يبدو جميلًا من الناحية المفاهيمية، فإن ذلك يعني أيضًا أن مسار شيفرتك قد يمرّ بشيفرة لا علاقة لها البتّة بما ستعيده. لا تُهدِر الدورات؛ أعِد القيمة متى جمعت كل المعلومات التي تحتاجها.

الدرس الثالث يتعلق باختبار شيفرتك. ففي Processing.js حظينا بميزة البدء بوثائق جيدة جدًّا تشرح كيف كان Processing «مفترض» أن يعمل، وبمجموعة كبيرة من حالات الاختبار، معظمها بدأ بوصفه «حالة معروف أنها تفشل» (known fail). وقد مكّننا ذلك من فعل أمرين: 1) كتابة شيفرة استنادًا إلى الاختبارات، و2) إنشاء الاختبارات قبل كتابة الشيفرة. فالإجراء المعتاد، الذي تُكتب فيه الشيفرة ثم تُكتب حالات الاختبار لها، يُنشئ في الواقع اختبارات متحيّزة. فبدلًا من اختبار ما إذا كانت شيفرتك تفعل ما ينبغي أن تفعله وفق المواصفة، فأنت لا تختبر سوى أن شيفرتك خالية من الأخطاء. أما في Processing.js فنبدأ بدلًا من ذلك بإنشاء حالات اختبار بناءً على المتطلّبات الوظيفية لدالة ما أو مجموعة دوال، بالاستناد إلى وثائقها. ومع هذه الاختبارات غير المتحيّزة يمكننا كتابة شيفرة مكتملة وظيفيًا، لا مجرّد شيفرة خالية من الأخطاء لكنها قد تكون ناقصة.

والدرس الأخير هو أيضًا الأكثر شمولًا: طبِّق قواعد التطوير الرشيق (agile development) على الإصلاحات الفردية أيضًا. فلا يستفيد أحد من انطوائك على وضع المبرمج وغياب صوتك عن الأنفس ثلاثة أيام متتالية وأنت تكتب الحل المثالي. وبدلًا من ذلك، أوصل حلولك إلى النقطة التي تعمل عندها — ولا يلزم حتى أن تعمل مع جميع حالات الاختبار — ثم اطلب تغذية راجعة. فالعمل وحدك، مع مجموعة اختبارات لالتقاط الأخطاء، ليس ضمانًا لشيفرة جيدة أو مكتملة. ولا مبلغًا من الاختبارات الآلية سيُنبّهك إلى أنك نسيت كتابة اختبارات لحالات حدّية معيّنة، أو أن هناك خوارزمية أفضل من التي اخترتها، أو أنك كان بإمكانك إعادة ترتيب جملك لتجعل الشيفرة أكثر ملاءمة للتصريف الفوري (JIT). تعامل مع الإصلاحات كالإصدارات: اعرض الإصلاحات مبكرًا، وحدّث كثيرًا، وادمج التغذية الراجعة في تحسيناتك.