شما پیشاپیش میدانید چگونه یک quotation را از تکهها بسازید و آن را اجرا کنید. quotation چیزی جز دنبالهای از کد نیست، پس همان واژههای دنبالهای که میشناسید برای آن هم به کار میآیند:
USING: kernel math prettyprint quotations sequences ;
{ [ 1 ] [ 2 + ] [ 3 * ] } concat >quotation call . ! => 9
[ 1 + ] dup append . ! => [ 1 + 1 + ]
append دو quotation را به هم میچسباند، concat یک آرایه از آنها را مسطح میکند، map داده را به quotation تبدیل میکند و >quotation نتیجه را قابل فراخوانی میسازد. MACRO: دقیقاً همین مهارت را برمیدارد و آن را به زمان کامپایل منتقل میکند.
یک تعریف MACRO: ظاهری مثل یک واژهی معمولی دارد، اما بدنهی آن در حالی که برنامهتان در حال کامپایل است اجرا میشود، نه در حالی که اجرا میشود. بدنه باید یک quotation روی پشته باقی بگذارد. سپس کامپایلر آن quotation را در محل فراخوانی جاسازی میکند؛ یعنی فراخوانی ماکرو را با کدی که بدنه تولید کرده است جایگزین میکند. در زمان اجرا هیچ ماکرویی باقی نمیماند، فقط کد تولیدشده میماند.
USING: kernel macros sequences ;
MACRO: twice ( quot -- spliced )
dup append ;
هر جا [ 1 + ] twice را بنویسید، کامپایلر:
[ 1 + ] روی پشته اجرا میکند،dup append quotation [ 1 + 1 + ] را میسازد،1 + 1 +، را در همان جا جاسازی میکند.پس واژهای که twice را فراخوانی میکند، طوری کامپایل میشود که گویی 1 + 1 + را با دست نوشتهاید:
: inc-twice ( x -- y ) [ 1 + ] twice ;
5 inc-twice . ! => 7
تفاوت کلیدی این است: یک واژهی معمولی که quotation میگیرد، آن را در زمان اجرا call میکند. ماکرو هرگز چیزی را فراخوانی نمیکند؛ بلکه کد تازه را به کامپایلر میدهد تا در جای آن کامپایل کند.
چون بدنه در زمان کامپایل اجرا میشود، ورودیهای یک ماکرو باید در همان زمان معلوم باشند. آنها باید در محل فراخوانی مقدار ثابت باشند: یک quotation از نوع [ … ]، یک عدد، یک آرایه، یک رشته. مقداری که فقط در زمان اجرا وجود دارد را نمیتوان به یک ماکرو سپرد.
! DOCTEST: SKIP — illustrative call sites, not standalone code
[ 1 + ] twice ! ok: the quotation is a literal
[ 1 + ] 3 repeat-quot ! ok: the quotation and the number are literals
بدنهی یک ماکرو کد معمولیای است که یک quotation برمیگرداند. بیشتر کار را همان واژههایی انجام میدهند که پیشاپیش دیدهاید:
| برای ساختن… | از این استفاده کنید |
|---|---|
[ A B ] از [ A ] و [ B ]
|
append |
| یک quotation از یک آرایه از آنها | concat >quotation |
| یک quotation برای هر عنصر یک دنباله |
map، سپس concat >quotation
|
دو واژهی دیگر از sequences و kernel این جعبهابزار را کامل میکنند.
<repetition> (در sequences) دنبالهای از n نسخه از یک عنصر میسازد. اثر پشتهای آن ( length elt -- repetition ) است و تعداد اول میآید. نتیجه یک دنبالهی مجازی است، پس آن را به روش معمول مسطح کنید:
3 [ 1 + ] <repetition> concat >quotation . ! => [ 1 + 1 + 1 + ]
curry (در kernel) یک مقدار را به ابتدای یک quotation اضافه میکند و quotation تازهای تولید میکند که اول آن مقدار را روی پشته میگذارد:
3 [ + ] curry . ! => [ 3 + ]
{ 1 2 3 } [ . ] curry . ! => [ { 1 2 3 } . ]
curry همان راهی است که با آن یک عنصر مشخص را در یک quotation جای میدهید، دقیقاً همان چیزی که { 10 20 30 } و [ + ] را به [ 10 + ] [ 20 + ] [ 30 + ] تبدیل میکند.
در بعضی زبانها، ماکروها به نگارش ویژهی quote/unquote و سازوکار اضافی نیاز دارند تا از تداخل نامها جلوگیری کنند. Factor به هیچکدام از اینها نیاز ندارد: یک quotation از پیش دنبالهای از واژههای حلشده است؛ تجزیهگر هنگام خواندن [ … ] هر dup و + و append را جستوجو کرده است. پس بدنهی یک ماکرو فقط همان دستکاری دنباله است که از قبل انجام میدهید و تصادفاً یک quotation برمیگرداند.
$[ … ]
یک ماکرو quotationای برمیگرداند تا در یک فراخوانی جاسازی شود. اما وقتی در عوض میخواهید یک مقدار ساده در زمان تجزیه محاسبه شود، $[ code ] هنگام خوانده شدن فایل، code را اجرا میکند و نتیجه را به صورت یک مقدار ثابت در متن منبع جای میدهد:
! DOCTEST: SKIP — compile-time value, shown for contrast
USING: calendar literals ;
CONSTANT: build-time $[ now ]
$[ … ] از واژگان literals میآید که واژههای درج نتایج زمان تجزیه در مقدارهای ثابت را گرد آورده است. این را فقط برای کامل بودن مطلب آوردهایم. چهار شابلونی که خواهید نوشت، یعنی twice و repeat-quot و compose-many و each-literal، همه تعریفهای MACRO: هستند.
شما در حال نگهداری از جعبهابزار تولید کدِ یک ایستگاه سیگنال هستید. هر ماکرو یک «شابلون» است، یعنی قالبی کوچک که کامپایلر با آن در هر نقطهی فراخوانی، کد تخصصیافته تولید میکند.
شما فقط چهار ماکرو را مینویسید. فایل تست، واژههای پوششی کوچکی تعریف میکند که ماکروهای شما را فراخوانی میکنند (ماکرو را تنها از راه کدی که تولید میکند میتوان مشاهده کرد)، و سپس نتایج زمان اجرای این پوششها را بررسی میکند. پوششها را همانطور که هستند رها کنید.
در هر تمرین، بدنه ورودیهای لیترالِ نقطهی فراخوانی را روی پشته دریافت میکند و باید یک کوتیشن را برجای بگذارد. خطوطِ «کامپایل میشود به» کد خطیای را نشان میدهند که کوتیشن شما باید تولید کند.
twice یک کوتیشن لیترال میگیرد و کوتیشنی برمیگرداند که کوتیشن
اصلی را دو بار پشت سر هم اجرا میکند.
! : inc-twice ( x -- y ) [ 1 + ] twice ;
! body receives [ 1 + ] -> must build [ 1 + 1 + ]
5 inc-twice . ! => 7
repeat-quot یک کوتیشن لیترال و یک تعداد لیترال n میگیرد و
کوتیشنی برمیگرداند که کوتیشن اصلی را n بار پشت سر هم اجرا میکند.
حالت n = 0 کوتیشن خالی [ ] را برمیگرداند.
! : inc-thrice ( x -- y ) [ 1 + ] 3 repeat-quot ;
! body receives [ 1 + ] 3 -> must build [ 1 + 1 + 1 + ]
5 inc-thrice . ! => 8
compose-many یک آرایهی لیترال از کوتیشنها میگیرد و یک کوتیشن
واحد برمیگرداند که هرکدام را به نوبت اجرا میکند. آرایهی خالی،
کوتیشن خالی را برمیگرداند.
! : pipeline ( x -- y ) { [ 1 + ] [ 2 * ] [ 1 - ] } compose-many ;
! body receives { [ 1 + ] [ 2 * ] [ 1 - ] } -> must build [ 1 + 2 * 1 - ]
5 pipeline . ! => 11
each-literal یک دنبالهی لیترال و یک کوتیشن میگیرد. کوتیشنی
برمیگرداند که برای هر عنصر دنباله، آن عنصر را روی پشته میگذارد و
سپس کوتیشن را اجرا میکند؛ پیمایشی که در زمان کامپایل رخ میدهد،
بنابراین کد تولیدشده خطی است.
! : sum-three ( -- n ) 0 { 10 20 30 } [ + ] each-literal ;
! body receives { 10 20 30 } [ + ] -> must build [ 10 + 20 + 30 + ]
sum-three . ! => 60
این مورد از میان آن چهار، ساختاریترین است: هم طول دنباله و هم مقادیر عناصر آن، کد تولیدشده را شکل میدهند. راهنماییها گامبهگام نحوهی ساخت آن را توضیح میدهند.