في هذه المذكرة القصيرة ، سننظر في كيفية تنفيذ Lisp مثل آليات البرامج الشائعة جدًا الآن مثل التطبيق الجزئي وتنظيف الوظائف. عند القيام بذلك ، سأستخدم تطبيق Homelisp الخاص بي (هذا مترجم Lisp خالص مع بعض الميزات الإضافية).
من المحتمل أن يكون استخدام التطبيق الجزئي في Common Lisp أمرًا صعبًا (لأنك تحتاج إلى استخدام funcall / application لاستدعاء كائن دالة محسوبة) ؛ في المخطط يجب أن يكون أسهل. أخطط لنشر إصدار جديد من HomeLisp قريبًا ، والذي لا يتطلب وظيفة / تطبيق لاستدعاء كائن دالة محسوبة. في الحالات التي يكون فيها سلوك الكود مختلفًا ، سأشدد على ذلك.
التطبيق الجزئي والكري عمليات حسابية صارمة. لكننا سنعتبرها "على الأصابع" ، دون اللجوء إلى حساب لامدا والمنطق الاندماجي.
يتم الحصول على تطبيق جزئي للدالة f من الدالة f دالة جديدة f 'التي اتخذت بالفعل الحجج المعطاة وهي جاهزة لقبول الباقي. ما هو التطبيق الجزئي ل؟ على سبيل المثال ، بحيث يمكن إرجاع قيمة وظيفية من دالة.
لنفكر في تطبيق جزئي بمثال بسيط. دع الدالة f تُعطى بالصيغة:
f (x، y، z) = x + y ** 2 + z ** 3
ثم التطبيق الجزئي لهذه الوظيفة مع الوسيطات x = 1 و y = 2 يجب أن يولد الوظيفة:
f '(z) = 1 + 4 + z ** 3 = 5 + z ** 3
في هاسكل ، التطبيق الجزئي لا يكلف المبرمج شيئًا:
Prelude> f x y z = x+y**2+z**3 --
f :: Floating a => a -> a -> a -> a
Prelude> f 1 2 3 -- ...
32.0 -- !
it :: Floating a => a
Prelude> f' = f 1 2 -- x=1 y=2 f'
f' :: Floating a => a -> a
Prelude> f' 3 -- f'
32.0 --
it :: Floating a => a
ومع ذلك ، في Lisp ستفشل هذه المحاولة:
(defun f (x y z) (+ x (* y y) (* z z z))) ;;
==> F
(f 1 2 3) ;;
==> 32 ;;
(f 1 2) ;; ...
PairLis:
: F
: (X Y Z)
: (1 2)
==> ERRSTATE
بالطبع ، لدى Lisp آلية لإنشاء وظائف بأي عدد من الوسائط (بنية & بقية) ؛ يمكنك إنشاء دالة تتطلب معلمتين أو ثلاثة (أو عشرة):
(defun s (&rest x) ;; - x
(apply '+ x)) ;;
==> S
(s 1 2) ;;
==> 3
(s 1 2 3) ;;
==> 6
لكن من المهم فهم الاختلاف: تعالج هذه الوظيفة جميع المعلمات وتعيد نتيجة الحساب ، بينما ينتج عن التطبيق الجزئي وظيفة جديدة "جاهزة لمتابعة الحساب".
دعونا نرى كيف يمكننا تنفيذ آلية تطبيق الوظيفة الجزئية في Lisp. وسوف يساعدنا في هذا ... نعم ، جهاز الوظائف المجهولة (lambda). يعتقد بعض المبرمجين أن الوظائف المجهولة مطلوبة فقط لحفظ الأسماء (يقولون ، مكانهم في أي تيار-خريطة-مرشح-تقليل من أجل أداء إجراء قصير على عنصر تسلسل). في الواقع ، الوظائف المجهولة قادرة على القيام بأكثر من ذلك بكثير ، وهو ما سنراه الآن.
لنبدأ بالنظر في كيفية قيام دالة بإرجاع دالة أخرى نتيجة لذلك. في Lisp ، الأمر بسيط جدًا:
(defun func-gen (x)
(lambda (y) (+ x (* y y))))
==> FUNC-GEN
(func-gen 5)
==> (CLOSURE (Y) ((+ X (* Y Y))) ((X 5)))
كما ترى ، ترجع الدالة إغلاقًا تكون فيه قيمة المتغير الحر x ثابتة (تساوي 5). يمكن استدعاء نتيجة الدالة مثل الوظيفة. للقيام بذلك ، في Common Lisp و HomeLisp (مع مراجعة kernel <= 13.53) ، سيتعين عليك استخدام funcall:
(funcall (func-gen 5) 7)
==> 54
من الواضح الآن كيف يمكننا المضي قدمًا إذا أردنا أخذ دالة f من وسيطات n وقيمة واحدة لـ x ، ونتيجة لذلك نحصل على دالة من الوسيطات (n-1). دعنا نشير إلى قائمة المعلمات الرسمية لوظيفتنا بواسطة plist. ثم كل ما عليك فعله هو بناء تعبير لامدا مثل هذا:
(lambda (-plist) (apply f (cons x -plist)))
الفكرة بسيطة للغاية: نحن نبني تعبير لامدا ، في جسمه نسمي ببساطة الوظيفة الأصلية في قائمة المعلمات ، حيث يتم استبدال الرأس بقيمة x.
هذا يعني أنه بالنسبة للتطبيق الجزئي للوظيفة ، فأنت بحاجة إلى بناء تعبير لامدا بناءً على هذه الوظيفة ، والتي ستحتوي على عدد أقل من الوسيطات ، وسيتم "أخذ الحجة المحددة في التطبيق الجزئي في الاعتبار" في الاستدعاء الداخلي لوظيفتنا في جسم تعبير لامدا.
كيف يتم تنفيذ هذا؟ سهل ... يحتوي HomeLisp على وظيفة getd التي توفر الوصول إلى تعبير تعريف الوظيفة أو الماكرو:
(defun g (x y z) (+ x (* x y) (* x y z)))
==> G
(getd 'g)
==> (EXPR (X Y Z) (+ X (* X Y) (* X Y Z)))
كما ترى ، تُرجع الدالة getd التعبير المُعرِّف للدالة ، حيث تكون لامدا "بدلاً من" ذرة EXPR خاصة. يمكننا أخذ قائمة المعلمات الخاصة بوظيفتنا (العنصر الثاني من النتيجة) وإنشاء تعبير lambda ، وستكون معلماته ذيل قائمة المعلمات الأصلية ، وفي جسم تعبير lambda سنسمي الوظيفة الأصلية مع القائمة الكاملة للمعلمات.
(defun part-apply (f a)
(let* ((plist (cadr (getd f))) ;;
(rlist (cdr plist)) ;;
(clist (cons a rlist))) ;; a
`(lambda ,rlist (apply (quote ,f) (list ,@clist)))))
من السهل أن نفهم من الكود أعلاه أن plist هي القائمة الأصلية للوظيفة f التي نريد تطبيقها جزئيًا. rlist هي القائمة الأصلية بدون العنصر الأول ، و clist هي القائمة الكاملة للمعلمات ، مع استبدال العنصر الأول بـ x. على وجه التحديد ، للدالة g أعلاه ، plist = (xyz) ، rlist = (yz) ، و clist = (ayz). الآن دعنا نتحقق من كيفية عمل التطبيق الجزئي:
(part-apply 'g 111)
==> (LAMBDA (Y Z) (APPLY (QUOTE G) (LIST 111 Y Z)))
يمكنك أن ترى أن هذا هو بالضبط ما تم التخطيط له: أعاد التطبيق الجزئي وظيفة جديدة ، بعدد أقل من الوسائط. إذا تم استدعاء هذه الوظيفة الجديدة باستخدام المعلمتين y و z ، فستكون النتيجة مماثلة تمامًا لاستدعاء الوظيفة الأصلية g بثلاث وسيطات: 111 y و z:
(g 111 1 2)
==> 444 ;;
(funcall (part-apply 'g 111) 1 2) ;; "--"
==> 444
((part-apply 'g 111) 1 2) ;; "-"
==> 444
أدناه I (للإيجاز) لن أحدد المكالمة الوظيفية. في النواة التالية لـ HomeLisp ، سيتم تغيير مخطط حساب الوظائف - ستصبح صيغة "المخطط" متاحة.
لنذهب أبعد من ذلك. ستكون هناك الآن رغبة طبيعية في إعادة تطبيق نتيجة إعادة التقديم. اتضح أن هذا بسيط للغاية - فنتيجة التطبيق الجزئي هي عبارة عن تعبير lambda ، وهي منظمة تقريبًا مطابقة للنتيجة التي تم إرجاعها بواسطة getd. قائمة معلمات تعبير lambda هي العنصر الثاني في القائمة. كل هذه الاعتبارات تسمح لنا ببناء الحل النهائي للمشكلة:
(defun part-apply (f a)
(cond ((and (atom f) (member 'expr (proplist f))) ;; *** ***
(let* ((plist (cadr (getd f))) ;;
(rlist (cdr plist)) ;;
(clist (cons a rlist))) ;; a
`(lambda ,rlist (apply (quote ,f) (list ,@clist)))))
((eq 'lambda (car f)) ;; *** - ***
(let* ((plist (cadr f)) ;;
(rlist (cdr plist)) ;;
(clist (cons x rlist))) ;; x
`(lambda ,rlist (apply (quote ,f) (list ,@clist)))))
(t (raiseerror "part-apply: "))))
هنا يضاف تحليل المعلمة الأولى: إذا كانت ذرة ، فيجب أن "تمثل" وظيفة (من نوع EXPR) ؛ إذا لم يكن المعامل الأول عبارة عن ذرة "صالحة" أو تعبير لامدا ، فسيتم رفع شرط الخطأ. يمكن بالطبع اختصار الكود أكثر ، ويُترك فرعين متطابقين تقريبًا للتوضيح. الآن دعنا نرى ما تستطيع هذه الوظيفة القيام به:
(part-apply (part-apply 'g 1) 2) ;;
==> (LAMBDA (Z) (APPLY (QUOTE (LAMBDA (Y Z) (APPLY (QUOTE G) (LIST 1 Y Z)))) (LIST 2 Z)))
((part-apply (part-apply 'g 1) 2) 3) ;;
==> 9
(part-apply (part-apply (part-apply 'g 111) 1) 2) ;;
==> (LAMBDA NIL (APPLY (QUOTE (LAMBDA (Z) (APPLY (QUOTE (LAMBDA (Y Z) (APPLY (QUOTE G) (LIST 111 Y Z)))) (LIST 1 Z)))) (LIST 2)))
((part-apply (part-apply (part-apply 'g 111) 1) 2)) ;; ...
==> 444
(setq u (part-apply 'g 111)) ;;
==> (LAMBDA (Y Z) (APPLY (QUOTE G) (LIST 111 Y Z)))
;; U
(part-apply u 1) ;;
==> (LAMBDA (Z) (APPLY (QUOTE (LAMBDA (Y Z) (APPLY (QUOTE G) (LIST 111 Y Z)))) (LIST 1 Z)))
((part-apply u 1) 2) ;;
==> 444
الآن دعونا نفعل الكاري. تقوم الدالة curried ، بقبول عدد غير كافٍ من الوسائط ، بإرجاع دالة يمكنها معالجة الباقي. الغرض الرئيسي من الكاري هو توفير تطبيق جزئي. ذهبنا من الطرف الآخر: الآن دعونا ننظر في كيفية تنفيذ الكاري إذا كان هناك تطبيق جزئي.
دع الدالة g من عدة حجج تُعطى. سنقوم ببناء نسخة محمصة منه تحت اسم مختلف! سيكون جوهر البناء كما يلي: الوظيفة! G سوف تأخذ عددًا غير محدد من الحجج. بعد الاستدعاء ، يجب أن يتحقق من عدد الوسائط التي تم تمريرها. إذا كان هذا الرقم يساوي عدد وسيطات الوظيفة الأصلية g ، فيجب تمرير from إلى الإدخال g. إذا كان هناك وسيطات أكثر مما يمكن أن تقبله g ، فيجب رفع شرط الخطأ. ولكن إذا كان عدد الوسائط أقل من عدد الدالة g ، إذن ... نعم ، نعم - تحتاج إلى تنفيذ تطبيق جزئي متسلسل. إرجاع نتيجة هذا التطبيق.
في هذه الحالة من المناسب استخدام الماكرو الرمز مع التعليقات أدناه:
(defmacro curry (f)
(let* ((parmlist (cadr (getd f))) ;; f
(body (cddr (getd f))) ;; f
(lp (length parmlist)) ;; f
(cf (implode (cons '! (explode f))))) ;;
`(defun ,cf (&rest x)
(let ((lx (length x))) ;;
(cond ((= lx ,lp) ;;
(let ((e `(lambda ,parmlist ,@body)))
(apply e x)))
((> lx ,lp) ;;
(raiseerror "curry: "))
(t (let ((tmp nil)) ;;
(iter (for a in x) ;;
(setq tmp (if (null tmp) (part-apply (quote ,f) a) (part-apply tmp a))))
tmp)))))))
يجدر التعليق على بناء اسم الوظيفة الجديد هنا. للقيام بذلك ، استخدم وظائف HomeLisp ، والتي تنشئ قائمة بالرموز المكونة لها من الذرة ، وتنهار من الداخل ، والتي تضغط رموز القائمة في واحدة ، مما يؤدي إلى إنشاء ذرة جديدة.
الآن دعنا نتحقق من الماكرو الخاص بنا أثناء العمل:
(curry g) ;; g
==> !G ;;
(!g 1 2 3) ;;
==> 9 ;; g
(!g 1 2) ;; -
==> (LAMBDA (Z) (APPLY (QUOTE (LAMBDA (Y Z) (APPLY (QUOTE G) (LIST 1 Y Z)))) (LIST 2 Z)))
;; :
((!g 1 2) 3) ;;
==> 9
((!g 1) 2 3) ;;
==> 9
(!g 1 2 3 4) ;;
curry:
==> ERRSTATE
تم عمل الكاري بدون تحكم إضافي. ونحن لم ننفذ تعبير لامدا الكارى. إذا كنت ترغب في ذلك ، يمكنك القيام بكل هذا ...
هذه هي الطريقة التي يمكنك بها تطبيق الوظيفة الجزئية والكاري في Lisp دون بذل الكثير من الجهد. اللثغة لغة رائعة!
شكرآ لك على أهتمامك.