ذُكر في مفهوم سابق أن التسميات المحلية والدوال ليست في الحقيقة سوى عناوين داخل قسم يحوي كودًا قابلًا للتنفيذ، مثل section .text.
بل يمكن التعامل مع الدوال بالطريقة نفسها التي تُعامَل بها أي عناوين ذاكرة، أي يمكن تحميلها في السجلات وتمريرها وتخزينها في الذاكرة.
كما يمكن استخدام call أو jmp لنقل التنفيذ إلى دالة مخزَّنة في سجل أو في الذاكرة:
section .text
sum_op:
lea rax, [rdi + rsi] ; loads the sum rdi + rsi into rax
ret
apply_sum:
lea rax, [rel sum_op]
jmp rax ; tail call
ويُطلق على عنوان الدالة الذي يُمرَّر كقيمة اسم الثنك. والثنكات حجر أساس في البرمجة عالية الرتبة بلغة التجميع: كود يعمل على كود آخر.
يمكن أيضًا تخزين عناوين الدوال في الذاكرة واسترجاعها لاحقًا:
section .bss
cached_fn resq 1
section .text
save_op:
mov qword [rel cached_fn], rdi
ret
apply_op:
; arguments are already set up according to the ABI
jmp qword [rel cached_fn] ; tail call
تكتب الدالة save_op عنوان الدالة الذي تستقبله في cached_fn.
وتبقى القيمة موجودة بعد أن تُرجع الدالة save_op، لذا فإن أي استدعاء لاحق لـ apply_op يقفز قفزة ذيلية إلى العنوان الذي خُزِّن أخيرًا.
وهذا يجعل من الممكن تغيير الدالة التي تستدعيها apply_op أثناء التشغيل.
يتيح تخزين عناوين الدوال في مصفوفة اختيار دوال مختلفة وفق فهرس معيّن، قد يعتمد على شرط أثناء التشغيل. وهذا ما يُسمّى جدول إرسال:
section .data
dispatch_table dq add_op, sub_op, mul_op
section .text
dispatch:
; this function takes two arguments in rdi and rsi, and an index in rdx
; it then applies the function corresponding to the index in rdx to the arguments
lea rax, [rel dispatch_table]
jmp qword [rax + 8*rdx] ; tail-call the function address for the index
قد يتصرف الثنك الذي يقرأ أو يحدّث ذاكرة مستمرة بين الاستدعاءات بشكل مختلف تبعًا لما جرى قبله. وقد تعتمد نتيجته على أكثر من وسائطه وحدها.
على سبيل المثال، عدّاد يأخذ دالة ويستدعيها بالعدّ الحالي، مقدِّمًا العدّ في كل مرة:
section .data
count dq 0
section .text
tick:
mov rax, rdi ; saves the function address
mov rdi, [rel count] ; loads the current count as the function's argument
inc qword [rel count] ; advances the count
jmp rax ; tail-calls the function
تستدعي الدالة tick الدالة المعطاة بالعدّ الحالي كوسيط، ثم تقدّم العدّ.
لذا فإن أول استدعاء tick(square) يستدعي square(0)، والاستدعاء التالي tick(square) يستدعي square(1)، والذي يليه square(2)، وهكذا.
ومن الأمثلة الأخرى الحساب المؤجَّل:
section .bss
captured_fn resq 1
argument resq 1
section .text
delay:
mov qword [rel captured_fn], rdi ; saves the function
mov qword [rel argument], rsi ; saves the argument
lea rax, [rel invoke] ; returns the `invoke` function
ret
invoke:
mov rdi, qword [rel argument] ; loads the saved argument into `rdi`
jmp qword [rel captured_fn] ; tail-calls the saved function
تأخذ الدالة delay دالةً وقيمةً، وتخزّنهما، وتُرجع الدالة invoke.
وعند استدعاء invoke، تُشغّل الدالة المحتجَزة بالوسيط المخزَّن.
تستند كثير من الأنماط الشائعة في اللغات عالية المستوى، مثل الاستدعاءات الرجعية، والطرق الافتراضية، والمولّدات، والكاريّة، وتركيب الدوال، وغيرها الكثير، إلى الثنكات المقترنة بحالة مستمرة.
أنت أمين الحسابات في بنك صغير في قرية. لكل عميل حساب، وأنت تحتفظ برصيده في دفتر حساباتك. وعلى مدار السنة، تُطبَّق المعاملات على هذه الأرصدة: تُضاف الفوائد، وتُخصم الرسوم، وتُدفع المكافآت، وتُفرض الغرامات. كل معاملة تأخذ رصيدًا وتنتج رصيدًا جديدًا.
أمامك أربع مهام.
يمكنك افتراض أن كل thunk (أي المعاملات والحرّاس) في هذا التمرين هو دالة:
يتعلّم أمين الصندوق معاملة جديدة في بداية اليوم ويدوّنها حتى يمكن تطبيقها لاحقًا عندما يصل عميل.
عرّف دالتين:
remember_transaction معاملة وتخزّنها في الذاكرة.apply_remembered رصيدًا وتطبّق عليه المعاملة المخزّنة سابقًا.مثال، بافتراض أن add_interest معاملة تضيف خمس وحدات من الفائدة:
remember_transaction(add_interest);
apply_remembered(100);
// => 105
remember_transaction(service_fee);
apply_remembered(100);
// => 98 (assuming service_fee deducts 2)
بالنسبة إلى remember_transaction:
بالنسبة إلى apply_remembered:
يحتوي دليل البنك على قائمة بالمعاملات المتكررة مخزّنة في جدول توجيه. يحتفظ كل فرع بنسخته الخاصة من القائمة، ويمكنه تسجيل معاملات مختلفة حسب السياسة المحلية.
عرّف دالتين تعملان على جدول توجيه يمرّره المستدعي:
register_transaction عنوان ذاكرة جدول توجيه، وفهرسًا، ومعاملة.
وهي تخزّن هذه المعاملة عند الفهرس المعطى في الجدول.select_transaction عنوان ذاكرة جدول توجيه، وفهرسًا، ورصيدًا.
وهي تبحث عن المعاملة عند الفهرس المعطى وتطبّقها على الرصيد، وتُرجع الرصيد الجديد.ينبغي أن تصل select_transaction إلى المعاملة التي بحثت عنها باستدعاء ذيلي غير مباشر واحد.
مثال، بافتراض أن manual عنوان ذاكرة جدول توجيه فيه أربع خانات فارغة:
register_transaction(manual, 0, monthly_interest);
register_transaction(manual, 1, service_fee);
select_transaction(manual, 0, 100);
// applies monthly_interest to 100
select_transaction(manual, 1, 100);
// applies service_fee to 100
بالنسبة إلى register_transaction:
بالنسبة إلى select_transaction:
في نهاية الشهر، يُسوّى حساب العميل. تُطبَّق كل معاملة جرت خلال الشهر على الرصيد الابتدائي، واحدة تلو الأخرى، وتكون النتيجة هي الرصيد الجديد.
عرّف دالة process_statement تأخذ رصيدًا ابتدائيًا، وعنوان ذاكرة مصفوفة من المعاملات، وعدد المعاملات في المصفوفة.
لكل معاملة بالتتابع، ينبغي أن تطبّق المعاملة على الرصيد الجاري، ثم تستخدم النتيجة رصيدًا للمعاملة التالية.
وتُرجَع قيمة الرصيد النهائي.
في الكود الزائف، تحسب process_statement(balance, transactions, n) ما يلي:
for each transaction in transactions:
balance = transaction(balance)
return balance
مثال، بافتراض أن transactions عنوان ذاكرة مصفوفة تحتوي على المعاملات add_interest وservice_fee وadd_interest بهذا الترتيب، حيث تضيف add_interest العدد 5 وتخصم service_fee العدد 2:
process_statement(100, transactions, 3);
// add_interest(100) = 105
// service_fee(105) = 103
// add_interest(103) = 108
// => 108
الوسيط الأول عدد صحيح غير سالب بطول 64 بت. الوسيط الثاني عنوان ذاكرة مصفوفة من المعاملات. الوسيط الثالث عدد صحيح غير سالب بطول 64 بت (طول المصفوفة). القيمة المُرجَعة عدد صحيح غير سالب بطول 64 بت.
تتطلب سياسة البنك فحص بعض المعاملات قبل اعتمادها. الحارس دالة تفحص رصيدًا مقترحًا وتقرر ما إذا كان مقبولًا. تُرجع دالة الحارس هذه قيمة غير صفرية للموافقة، أو صفرًا للرفض.
عرّف process_with_guard التي تأخذ رصيدًا ابتدائيًا، وعنوان ذاكرة مصفوفة من المعاملات، وعدد المعاملات في المصفوفة، ودالة حارس.
لكل معاملة بالتتابع:
بعد معالجة كل المعاملات، أرجع الرصيد النهائي إلى جانب عدد المعاملات المعتمدة.
في الكود الزائف، تحسب process_with_guard(balance, transactions, n, guard) ما يلي:
approved = 0
for each transaction in transactions:
tentative = transaction(balance)
if guard(tentative) is non-zero:
balance = tentative
approved = approved + 1
return balance, approved
على سبيل المثال، افترض أن:
add_interest معاملة تضيف 5، وservice_fee معاملة أخرى تخصم 2at_least_10 حارس يُرجع قيمة غير صفرية عندما يكون الرصيد أكبر من أو يساوي 10إذن:
process_with_guard(5, {add_interest, service_fee, add_interest}, 3, at_least_10);
// add_interest(5) = 10; at_least_10(10) != 0;
// => balance = 10, approved = 1
//
// service_fee(10) = 8; at_least_10(8) = 0;
// => balance = 10, approved = 1
//
// add_interest(10) = 15; at_least_10(15) != 0;
// => balance = 15, approved = 2
//
// final balance (15) is returned in rax
// number of approved transactions (2) is returned in rdx
بالنسبة إلى process_with_guard:
rax، وعدد المعاملات المعتمدة في rdx.سجّل في Exercism لتتعلّم وتتقن x86-64 Assembly عبر 22 مفهومًا130 تمرينًا، وإرشاد بشري حقيقي، وكل ذلك مجانًا.