ما الذي يجعل ليسب macros خاص جدا؟




lisp homoiconicity (10)

قراءة مقالات بول جراهام على لغات البرمجة قد يظن المرء أن ليسب macros هي الطريقة الوحيدة للذهاب. كمطور مشغول ، والعمل على منصات أخرى ، لم يكن لدي امتياز استخدام وحدات الماكرو Lisp. بصفتك شخصًا يريد أن يفهم الضجة ، يرجى توضيح ما يجعل هذه الميزة قوية جدًا.

يرجى أيضا ربط هذا بشيء أفهمه من عوالم بايثون ، جافا ، جيم # أو جيم.


التفكير في ما يمكنك القيام به في C أو C ++ مع وحدات الماكرو والقوالب. إنها أدوات مفيدة جدًا لإدارة الشفرة المتكررة ، ولكنها محدودة بطرق شديدة.

  • تقييد بناء الجملة الماكرو / القالب تقييد استخدامها. على سبيل المثال ، لا يمكنك كتابة قالب يتوسع إلى شيء آخر غير الفئة أو الوظيفة. لا تستطيع وحدات الماكرو والقوالب الاحتفاظ بالبيانات الداخلية بسهولة.
  • يجعل بناء جملة معقد جداً غير منتظم من C و C ++ صعوبة في كتابة وحدات ماكرو عامة جداً.

اللوزة و Lisp macros حل هذه المشاكل.

  • تتم كتابة Lisp macros في Lisp. لديك القدرة الكاملة لـ Lisp لكتابة الماكرو.
  • ﻟﯾﺳﺑﮫ ﻟﮫ ﺻﯾﻐﺔ ﻣﻧﺗظﻣﺔ ﺟدا.

تحدث إلى أي شخص يتقن C ++ ويسألهم عن المدة التي قضوا فيها في تعلم كل ما يحتاجه القالب من عمل يحتاجون إليه للقيام ببرنامج metaprogramming. أو كل الحيل المجنونة في كتب (ممتازة) مثل Modern C ++ Design ، والتي لا تزال صعبة التصويب و (في الممارسة العملية) غير المحمولة بين المجمعين الحقيقيين على الرغم من أن اللغة قد تم توحيدها لمدة عشر سنوات. كل ذلك يذوب بعيدا إذا كان النطاق الذي تستخدمه في البرمجة النصية هو نفس اللغة التي تستخدمها للبرمجة!


باختصار ، وحدات الماكرو هي تحويلات رمز. أنها تسمح بإدخال العديد من بنيات بناء الجملة الجديدة. على سبيل المثال ، ضع في الاعتبار LINQ في C #. في lisp ، توجد امتدادات لغة مشابهة يتم تنفيذها بواسطة وحدات الماكرو (على سبيل المثال ، إنشاء الحلقة المضمنة ، التكرار). وحدات الماكرو تقلل إلى حد كبير تكرار التعليمات البرمجية. تسمح وحدات الماكرو بتضمين "لغات قليلة" (على سبيل المثال ، حيث في c # / java واحد يمكن استخدام xml لتكوين ، في lisp يمكن تحقيق الشيء نفسه مع وحدات الماكرو). قد تخفي وحدات الماكرو صعوبات في استخدام المكتبات.

على سبيل المثال ، في lisp يمكنك الكتابة

(iter (for (id name) in-clsql-query "select id, name from users" on-database *users-database*)
      (format t "User with ID of ~A has name ~A.~%" id name))

ويخفي هذا كافة قاعدة البيانات (المعاملات ، إغلاق الاتصال الصحيح ، إحضار البيانات ، إلخ) بينما في C # يتطلب إنشاء SqlConnections ، SqlCommands ، إضافة SqlParameters إلى SqlCommands ، حلقات على SqlDataReaders ، إغلاقها بشكل صحيح.


تتيح لك وحدات الماكرو Lisp تحديد متى سيتم تقييم أي جزء أو تعبير (إذا حدث). لوضع مثال بسيط ، فكر في C:

expr1 && expr2 && expr3 ...

ما يقوله هذا: قيّم expr1 ، و إذا كان صحيحًا ، expr2 ، وما إلى ذلك.

حاول الآن جعل هذا && في وظيفة ... هذا صحيح ، لا يمكنك ذلك. الاتصال بشيء مثل:

and(expr1, expr2, expr3)

سيتم تقييم جميع exprs الثلاثة قبل تقديم إجابة بغض النظر عما إذا كان expr1 كاذبة!

مع وحدات الماكرو lisp يمكنك رمز شيء مثل:

(defmacro && (expr1 &rest exprs)
    `(if ,expr1                     ;` Warning: I have not tested
         (&& ,@exprs)               ;   this and might be wrong!
         nil))

الآن لديك && ، والتي يمكنك الاتصال بها تمامًا مثل وظيفة ، ولن تقوم بتقييم أي نماذج تمر عليها ما لم تكن كلها صحيحة.

لمعرفة كيف يكون ذلك مفيدًا ، يتباين التباين:

(&& (very-cheap-operation)
    (very-expensive-operation)
    (operation-with-serious-side-effects))

و:

and(very_cheap_operation(),
    very_expensive_operation(),
    operation_with_serious_side_effects());

الأشياء الأخرى التي يمكنك القيام بها مع وحدات الماكرو هي إنشاء كلمات رئيسية جديدة و / أو لغات مصغرة (تحقق من الماكرو (loop ...) على سبيل المثال) ، ودمج لغات أخرى في lisp ، على سبيل المثال ، يمكنك كتابة ماكرو يتيح لك قل شيئًا مثل:

(setvar *rows* (sql select count(*)
                      from some-table
                     where column1 = "Yes"
                       and column2 like "some%string%")

وليس حتى الوصول إلى وحدات الماكرو القارئ .

أتمنى أن يساعدك هذا.


تمثل وحدات ماكرو Lisp نمطًا يحدث في أي مشروع برمجة كبير تقريبًا. في نهاية المطاف في برنامج كبير لديك قسم معين من التعليمات البرمجية حيث تدرك أنه سيكون من الأسهل والأقل عرضة للخطأ لكتابة البرنامج الذي يخرج رمز المصدر كنص والذي يمكنك فقط لصق في.

في بايثون الكائنات لها طريقتان __repr__ و __str__ . __str__ هو ببساطة تمثيل الإنسان المقروء. __repr__ تُرجع تمثيلًا يمثل رمز Python صالحًا ، وهو ما يمكن إدخاله في المترجم على أنه بايثون صالح. بهذه الطريقة ، يمكنك إنشاء قصاصات صغيرة من Python تُنشئ شفرة صالحة يمكن لصقها في مصدرك الفعلي.

في Lisp تمت إضفاء الطابع الرسمي على هذه العملية برمتها من خلال النظام الكلي. من المؤكد أنها تمكنك من إنشاء إضافات للبناء والقيام بكل أنواع الأشياء الفاخرة ، ولكن يتم تلخيص الفائدة الفعلية عن طريق ما سبق. بالطبع أنه يساعد على أن نظام الماكرو Lisp يتيح لك التعامل مع هذه "القصاصات" مع القوة الكاملة للغة بأكملها.


حصلت على هذا من كتاب الطبخ المشترك المعتدل لكنني أعتقد أنه يفسر لماذا ماكروس الليمون جيدة بطريقة لطيفة.

"إن الماكرو هو جزء عادي من كود Lisp الذي يعمل على جزء آخر من شفرة Lisp المفترضة ، ويرجمها إلى (نسخة أقرب إلى) Lisp القابل للتنفيذ. قد يبدو ذلك معقدًا نوعًا ما ، لذا دعنا نقدم مثالًا بسيطًا. لنفترض أنك تريد إصدار setq الذي يحدد متغيرين إلى نفس القيمة ، لذا إذا كتبت

(setq2 x y (+ z 3))

عندما يتم ضبط z=8 كل من x و y على 11. (لا أستطيع التفكير في أي استخدام لهذا ، ولكنه مجرد مثال).

يجب أن يكون واضحًا أننا لا نستطيع تعريف setq2 كدالة. إذا كانت x=50 و y=-5 ، ستتلقى هذه الدالة القيم 50 و -5 و 11 ؛ لن يكون لديها معرفة بالمتغيرات التي من المفترض أن يتم ضبطها. ما نريد أن نقوله حقاً هو ، عندما ترى (نظام Lisp) (setq2 v1 v2 e) ، تعامله على أنه مكافئ لـ (progn (setq v1 e) (setq v2 e)) . في الواقع ، هذا ليس صحيحًا تمامًا ، لكنه سيفعل الآن. يسمح لنا الماكرو بالقيام بذلك على وجه التحديد ، من خلال تحديد برنامج لتحويل نمط الإدخال (setq2 v1 v2 e) "إلى نمط الإخراج (progn ...) ."

إذا كنت تعتقد أن هذا أمر رائع ، يمكنك الاستمرار في القراءة هنا: http://cl-cookbook.sourceforge.net/macros.html


سوف تجد مناقشة شاملة حول ماكرو الليمب هنا .

مجموعة فرعية مثيرة للاهتمام من تلك المادة:

في معظم لغات البرمجة ، يكون بناء الجملة معقدًا. يجب أن تفحص وحدات الماكرو بنية البرنامج ، وتحليلها ، وإعادة تجميعها. لا يمكنهم الوصول إلى محلل البرنامج ، لذلك يجب عليهم الاعتماد على الاستدلال وأفضل التخمينات. في بعض الأحيان يكون تحليل أسعار الفائدة خاطئًا ، ثم ينكسر.

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

هنا مثال ممتد. لدى Lisp ماكرو يسمى "setf" ، والذي يؤدي مهمة. أبسط شكل من أشكال setf هو

  (setf x whatever)

والتي تحدد قيمة الرمز "x" إلى قيمة التعبير "أيا كان".

ليسب لديها أيضا قوائم. يمكنك استخدام الدالتين "car" و "cdr" للحصول على العنصر الأول من القائمة أو بقية القائمة ، على التوالي.

ماذا لو كنت تريد استبدال العنصر الأول من القائمة بقيمة جديدة؟ هناك وظيفة قياسية للقيام بذلك ، وبشكل لا يصدق ، فإن اسمها أسوأ من "السيارة". إنها "ربلاكا". لكن ليس عليك أن تتذكر "rplaca" ، لأنك تستطيع أن تكتب

  (setf (car somelist) whatever)

لتعيين سيارة من somelist.

ما يحدث هنا هو أن "setf" هو ماكرو. في وقت الترجمة ، فإنه يفحص حججها ، ويرى أن أول واحد لديه الشكل (سيارة SOMETHING). تقول لنفسها "أوه ، مبرمج يحاول تعيين سيارة somthing. الدالة لاستخدام ذلك هو" rplaca "." ويعيد كتابة الرمز في مكانه بهدوء:

  (rplaca somelist whatever)

في حين أن كل ما سبق يشرح ما هي وحدات الماكرو ولديها أمثلة رائعة ، أعتقد أن الفرق الرئيسي بين الماكرو والوظيفة العادية هو أن LISP تقوم بتقييم جميع المعلمات أولاً قبل استدعاء الدالة. مع الماكرو هو عكس ذلك ، يمرر LISP المعلمات غير مترجم إلى الماكرو. على سبيل المثال ، إذا قمت بتمرير (+ 1 2) إلى دالة ، فستتلقى الدالة القيمة 3. إذا قمت بتمرير هذا إلى ماكرو ، فستتلقى قائمة (+ 1 2). هذا يمكن استخدامه للقيام بكل أنواع الأشياء المفيدة بشكل لا يصدق.

  • إضافة بنية تحكم جديدة ، على سبيل المثال حلقة أو تفكيك قائمة
  • قياس الوقت المستغرق لتنفيذ وظيفة تم تمريرها. مع وظيفة سيتم تقييم المعلمة قبل أن يتم تمرير التحكم إلى الوظيفة. باستخدام الماكرو ، يمكنك لصق الشفرة بين بداية ساعة التوقف وإيقافها. يحتوي أدناه على نفس التعليمة البرمجية نفسها في ماكرو ودالة ويكون الإخراج مختلفًا جدًا. ملاحظة: هذا مثال مفتعل وتم اختيار التنفيذ بحيث يكون متطابقًا لإبراز الفرق بشكل أفضل.

    (defmacro working-timer (b) 
      (let (
            (start (get-universal-time))
            (result (eval b))) ;; not splicing here to keep stuff simple
        ((- (get-universal-time) start))))
    
    (defun my-broken-timer (b)
      (let (
            (start (get-universal-time))
            (result (eval b)))    ;; doesn't even need eval
        ((- (get-universal-time) start))))
    
    (working-timer (sleep 10)) => 10
    
    (broken-timer (sleep 10)) => 0
    

لإعطاء إجابة مختصرة ، يتم استخدام وحدات الماكرو لتعريف ملحقات قواعد اللغة إلى لغة اللوتس المشتركة أو لغات محددة النطاق (DSL). يتم تضمين هذه اللغات مباشرة في رمز Lisp الموجود. الآن ، يمكن أن يكون ل DSLs صيغة مشابهة لـ Lisp (مثل مترجم برول Norvig's Prol Interpreter for Common Lisp) أو مختلف تمامًا (مثال: Infix Notation Math for Clojure).

في ما يلي مثال أكثر واقعية:
لدى بايثون قائمة بالفهم مدمجة في اللغة. هذا يعطي بناء جملة بسيط لحالة شائعة. الخط

divisibleByTwo = [x for x in range(10) if x % 2 == 0]

ينتج قائمة تحتوي على جميع الأرقام الزوجية بين 0 و 9. في بيثون 1.5 يوم لم يكن هناك مثل هذا النحو ؛ ستستخدم شيئًا كهذا:

divisibleByTwo = []
for x in range( 10 ):
   if x % 2 == 0:
      divisibleByTwo.append( x )

كلاهما مكافئ وظيفيا. دعونا نستحضر تعليقنا لعدم التصديق ونتظاهر بأن لسب لديها ماكرو متكرر للغاية لا يفعل سوى تكرار ولا توجد طريقة سهلة للقيام بما يعادل فهم القائمة.

في Lisp يمكنك كتابة ما يلي. يجب أن أشير إلى أن هذا المثال المفتعل قد تم اختياره ليكون متطابقًا مع شفرة Python وليس مثالًا جيدًا على شفرة Lisp.

;; the following two functions just make equivalent of Python's range function
;; you can safely ignore them unless you are running this code
(defun range-helper (x)
  (if (= x 0)
      (list x)
      (cons x (range-helper (- x 1)))))

(defun range (x)
  (reverse (range-helper (- x 1))))

;; equivalent to the python example:
;; define a variable
(defvar divisibleByTwo nil)

;; loop from 0 upto and including 9
(loop for x in (range 10)
   ;; test for divisibility by two
   if (= (mod x 2) 0) 
   ;; append to the list
   do (setq divisibleByTwo (append divisibleByTwo (list x))))

قبل أن أذهب إلى أبعد من ذلك ، يجب أن أشرح بشكل أفضل ما هو الماكرو. إنه تحويل يتم على الكود بواسطة الكود. أي ، قطعة من الكود ، يقرأها المترجم (أو المترجم) ، الذي يأخذ في الكود كوسيطة ، يعالج ويعيد النتيجة ، ثم يتم تشغيلها في المكان.

بالطبع هذا كثير من الكتابة والمبرمجين كسالى. لذا يمكننا تعريف DSL للقيام بفهم القوائم. في الواقع ، نحن نستخدم ماكروًا واحدًا بالفعل (الماكرو الحلقي).

يعرّف Lisp بعض نماذج التركيب الخاصة. الاقتباس ( ' ) يشير إلى الرمز التالي هو حرفي. يشير quasiquote أو backtick ( ` ) إلى الرمز المميز التالي هو حرفي مع الهروب. يشار إلى الهروب من قبل عامل الفاصلة. الحرف '(1 2 3) هو ما يعادل بايثون [1, 2, 3] . يمكنك تخصيصه لمتغير آخر أو استخدامه في مكانه. يمكنك التفكير في `(1 2 ,x) كمكافئ لـ Python's [1, 2, x] حيث x هو متغير تم تعريفه مسبقًا. تدوين هذه القائمة جزء من السحر الذي ينتقل إلى وحدات الماكرو. الجزء الثاني هو قارئ Lisp الذي يحل بذكاء وحدات الماكرو ليحل محل الشفرة ولكن أفضل ما هو موضح أدناه:

حتى نتمكن من تحديد ماكرو يسمى lcomp (اختصار لفهم القائمة). سيكون تركيب الجملة بالضبط مثل python الذي استخدمناه في المثال [x for x in range(10) if x % 2 == 0] - (lcomp x for x in (range 10) if (= (% x 2) 0))

(defmacro lcomp (expression for var in list conditional conditional-test)
  ;; create a unique variable name for the result
  (let ((result (gensym)))
    ;; the arguments are really code so we can substitute them 
    ;; store nil in the unique variable name generated above
    `(let ((,result nil))
       ;; var is a variable name
       ;; list is the list literal we are suppose to iterate over
       (loop for ,var in ,list
            ;; conditional is if or unless
            ;; conditional-test is (= (mod x 2) 0) in our examples
            ,conditional ,conditional-test
            ;; and this is the action from the earlier lisp example
            ;; result = result + [x] in python
            do (setq ,result (append ,result (list ,expression))))
           ;; return the result 
       ,result)))

الآن يمكننا تنفيذ الأمر في سطر الأوامر:

CL-USER> (lcomp x for x in (range 10) if (= (mod x 2) 0))
(0 2 4 6 8)

أنيق جدا ، هاه؟ الآن لا تتوقف عند هذا الحد. لديك آلية ، أو فرشاة للرسم ، إذا أردت. يمكنك الحصول على أي صيغة قد تحتاجها. مثل بايثون أو سي # with بناء الجملة. أو بناء جملة LINQ .NET. في النهاية ، هذا ما يجذب الناس إلى Lisp - المرونة القصوى.


لست متأكدًا من أنني أستطيع إضافة بعض الإحصاءات إلى المشاركات (الممتازة) للجميع ، ولكن ...

تعمل وحدات الماكرو Lisp رائعة بسبب طبيعة تركيب Lisp.

Lisp هي لغة عادية للغاية (فكر في كل شيء هو قائمة ) ؛ تمكّنك وحدات الماكرو من معالجة البيانات والتعليمة البرمجية على النحو نفسه (لا يلزم إجراء تحليل للخيط أو غير ذلك من الاختصارات لتعديل تعبيرات lisp). يمكنك الجمع بين هاتين الميزتين ولديك طريقة نظيفة جدًا لتعديل الشفرة.

تحرير: ما كنت أحاول أن أقوله هو أن Lisp هو homoiconic ، مما يعني أن بنية البيانات لبرنامج lisp مكتوبة في lisp نفسها.

لذلك ، ينتهي الأمر بكيفية إنشاء مولد التعليمات البرمجية الخاص بك أعلى اللغة باستخدام اللغة نفسها بكل قوتها (على سبيل المثال في جافا ، عليك اختراق طريقك باستخدام النسق bytecode ، على الرغم من أن بعض أطر العمل مثل AspectJ تسمح لك القيام بذلك باستخدام نهج مختلف ، هو في الأساس الاختراق).

في الممارسة العملية ، مع وحدات الماكرو ، ينتهي الأمر بك ببناء اللغة المصغرة الخاصة بك على رأس lisp ، دون الحاجة إلى تعلم لغات إضافية أو الأدوات ، ومع استخدام القوة الكاملة للغة نفسها.


يأخذ ماكرو lisp جزء برنامج كمدخل. يمثل جزء البرنامج هذا بنية بيانات يمكن التلاعب بها وتحويلها بالطريقة التي تريدها. في النهاية يخرج الماكرو جزء برنامج آخر ، وهذا الجزء هو ما يتم تنفيذه في وقت التشغيل.

لا يحتوي C # على وحدة ماكرو ، ومع ذلك سيكون مكافئًا إذا قام المحلل اللغوي بتحليل الشفرة إلى شجرة CodeDOM ، وتمرير ذلك إلى طريقة ، والتي حولت ذلك إلى CodeDOM آخر ، والذي تم تجميعه بعد ذلك في IL.

يمكن استخدام هذا لتطبيق بناء الجملة "للسكر" مثل for each جملة using -clause و linq select -expressions وما إلى ذلك ، مثل وحدات الماكرو التي تتحول إلى الكود الأساسي.

إذا كانت Java تحتوي على وحدات ماكرو ، يمكنك تنفيذ بناء جملة Linq في Java ، دون الحاجة إلى Sun لتغيير اللغة الأساسية.

فيما يلي التعليمة البرمجية الزائفة لكيفية ظهور ماكرو نمط lisp في C # لتطبيق using :

define macro "using":
    using ($type $varname = $expression) $block
into:
    $type $varname;
    try {
       $varname = $expression;
       $block;
    } finally {
       $varname.Dispose();
    }