در مفهوم پیشین اشاره شد که برچسبهای محلی و توابع هر دو فقط آدرسهایی در بخشی هستند که کد اجرایی دارد، مانند 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
به آدرس تابعی که بهعنوان یک مقدار دستبهدست میشود، «thunk» میگویند. thunkها یکی از اجزای سازندهی «برنامهنویسی مرتبهبالا» در اسمبلیاند: کدی که روی کد دیگر عمل میکند.
آدرس توابع را نیز میتوان در حافظه ذخیره کرد و بعداً بازیابی کرد:
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
thunkای که بین فراخوانیها حافظهای ماندگار را میخواند یا بهروزرسانی میکند، بسته به آنچه پیشتر رخ داده است ممکن است رفتار متفاوتی داشته باشد. ممکن است نتیجهی آن به چیزی بیش از تنها آرگومانهایش بستگی داشته باشد.
برای نمونه، یک شمارنده که تابعی را میگیرد و آن را با شمارش جاری فراخوانی میکند و هر بار شمارش را یک واحد جلو میبرد:
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 فراخوانی شود، تابع گرفتهشده را با آرگومان ذخیرهشده اجرا میکند.
بسیاری از الگوهای رایج در زبانهای سطح بالاتر، مانند callbackها، متدهای مجازی، generatorها، currying، ترکیب توابع و بسیاری موارد دیگر، بر پایهی thunkهایی ساخته شدهاند که با حالت ماندگار جفت شدهاند.
شما حسابدار یک بانک کوچک روستایی هستید. هر مشتری یک حساب دارد و شما موجودی آن را در دفتر کل خود نگه میدارید. در طول سال، تراکنشها روی این موجودیها اعمال میشوند: بهره واریز میشود، کارمزد کسر میشود، پاداش پرداخت میشود و جریمه دریافت میشود. هر تراکنش یک موجودی میگیرد و موجودی تازهای تولید میکند.
چهار وظیفه دارید.
میتوانید فرض کنید که هر «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 پنج واحد اضافه میکند و service_fee دو واحد کم میکند:
process_statement(100, transactions, 3);
// add_interest(100) = 105
// service_fee(105) = 103
// add_interest(103) = 108
// => 108
آرگومان اول یک عدد صحیح نامنفی ۶۴بیتی است. آرگومان دوم آدرس حافظهی یک آرایه از تراکنشها است. آرگومان سوم یک عدد صحیح نامنفی ۶۴بیتی است (طول آرایه). مقدار بازگشتی یک عدد صحیح نامنفی ۶۴بیتی است.
سیاست بانک ایجاب میکند که برخی تراکنشها پیش از ثبت شدن بررسی شوند. نگهبان تابعی است که موجودی پیشنهادی را بررسی میکند و تصمیم میگیرد آیا قابل قبول است یا نه. این تابع نگهبان برای تأیید مقدار ناصفر و برای رد، صفر برمیگرداند.
تابع 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 تراکنشی است که پنج واحد اضافه میکند و service_fee تراکنش دیگری است که دو واحد کم میکندat_least_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 تمرین و مربیگری انسانی واقعی یاد بگیرید و در آن استاد شوید، همهی اینها رایگان.