5.2.1. ارزیابی
ارزیابی فرآیند تبدیل یک عبارت نیکس (Nix expression) به یک مقدار نیکس (Nix value) است.
این کار طبق تعدادی قانون انجام میشود، از جمله:
- ساخت مقادیر از مقادیر ثابت (literals).
برای مثال، مقدار ثابت عددی
1به مقدار عددی1تبدیل میشود. - اعمال عملگرها
برای مثال، عملگر جمع
+روی دو مقدار عددی اعمال میشود تا یک مقدار عددی جدید تولید کند. - اعمال توابع توکار (built-in)
برای مثال، عبارت
builtins.isInt 1بهtrueارزیابی میشود. - اعمال توابع تعریفشده توسط کاربر
برای مثال، عبارت
(x: x + 1) 10را میتوان* اینگونه تصور کرد کهxدر بدنه تابع با آرگومان جایگزین میشود، یعنی10 + 1، و سپس به11ارزیابی میشود.
این قوانین بر اساس نیاز و با توجه به کاربرد خاص عبارت اعمال میشوند. برای مثال، این فرآیند میتواند در رابط خط فرمان Nix یا بهصورت تعاملی با استفاده از repl (حلقه خواندن-ارزیابی-چاپ) رخ دهد که ابزاری مفید هنگام یادگیری درباره ارزیابی است.
جزئیات
مقادیر
مقادیر نیکس را میتوان زیرمجموعهای از عبارتهای Nix در نظر گرفت.
برای مثال، عبارت 1 + 2 یک مقدار نیست، زیرا میتوان آن را به 3 تقلیل داد. عبارت 3 یک مقدار است، زیرا دیگر نمیتوان آن را بیشتر از این تقلیل داد.
ارزیابی معمولاً با اعمال قوانین روی «بخش اصلی» (head) عبارت انجام میشود که بیرونیترین بخش عبارت است. بخش اصلی عبارتی مانند [ 1 2 ] همان مقدار ثابت فهرست ([ a1 a2 ]) است، برای 1 + 2 عملگر جمع (+) است، و برای f 1 «عملگر» فراخوانی تابع () است.
پس از اعمال تمام قوانین ممکن روی بخش اصلی تا زمانی که هیچ قانون دیگری قابل اعمال نباشد، عبارت در «فرم نرمال سر ضعیف» (WHNF) قرار دارد. این بدان معناست که بیرونیترین سازنده (constructor) عبارت ارزیابی شده است، اما مقادیر درونی ممکن است ارزیابی شده باشند یا نشده باشند. واژه «ضعیف» (Weak) صرفاً نشاندهنده این است که عبارت ممکن است یک تابع باشد. این یک میراث تاریخی یا آکادمیک است و Nix هیچ استفادهای از «فرم نرمال سر» غیرضعیف ندارد.
تنبلی و تانکها (Thunks)
زبان Nix رویکرد فراخوانی بر حسب نیاز (call by need) را پیادهسازی میکند (در تقابل با فراخوانی بر حسب مقدار یا فراخوانی بر حسب مرجع). فراخوانی بر حسب نیاز در برنامهنویسی تابعی معمولاً با نام تنبلی (laziness) شناخته میشود، زیرا پیادهسازی خاصی از این مفهوم است که در آن ارزیابی تا زمان نیاز به نتیجه به تعویق میافتد، با این هدف که فقط بخشهایی از یک عبارت که برای تولید نتیجه نهایی لازم هستند، ارزیابی شوند.
علاوه بر این، نتیجه ارزیابی در مقادیر، در اتصالات let، و در پارامترهای تابع حفظ میشود؛ پارامترهایی که بسیار شبیه به اتصالات let رفتار میکنند، اما با این استثنای قابلتوجه یعنی فراخوانیهای تابع. نتایج فراخوانیهای تابع برای استفاده مجدد، به قرار گرفتن در اتصالات let و غیره وابستهاند.
هنگامی که درباره فرآیند ارزیابی با اصطلاحات سطح پایینتر بحث میکنیم، ممکن است مقادیر را نه بهعنوان زیرمجموعهای از عبارتها، بلکه بهطور جداگانه تعریف کنیم؛ بهطوریکه هر «مقدار» یا یک سازنده داده (data constructor)، یک تابع، یا یک تانک (thunk) است. تانک یک محاسبات به تأخیر افتاده است که با یک ارجاع به عبارت و یک «کلوژر» (closure) ـ یعنی مقادیر مربوط به محدوده واژگانی (lexical scope) پیرامون عبارت به تأخیر افتاده ـ نمایش داده میشود.
بهعنوان یک کاربر زبان، معمولاً نیازی به فکر کردن درباره تانکها ندارید، زیرا آنها بخشی از معناشناسی (semantics) زبان نیستند، اما ممکن است آنها را در repl، در C API یا در بحثها ملاقات کنید.
سختگیری (Strictness)
بهجای فکر کردن به ثانکها (thunks)، اغلب سازندهتر است که بر حسب سختگیری (strictness) بیندیشیم. این اصطلاح در برنامهنویسی تابعی برای اشاره به نقطه مقابل تنبلی (laziness) استفاده میشود، یعنی صرفاً برای چیزی مانند انتشار خطا نیست. بلکه به نیاز به ارزیابی عبارتهای خاص قبل از اینکه ارزیابی بتواند نتیجهای تولید کند، اشاره دارد.
عبارتهای مربوط به سختگیری معمولاً بهطور ضمنی به فرم نرمال سر ضعیف (weak head normal form) اشاره میکنند. برای مثال، میتوانیم بگوییم که تابع زیر در آرگومان خود سختگیر است:
x: isAttrs x || isFunction x تابع بالا باید نسبت به آرگومان خود یعنی x سختگیر (strict) باشد، زیرا تعیین نوع آن مستلزم ارزیابی x حداقل تا حدودی است.
تابع زیر نسبت به آرگومان خود سختگیر نیست:
x: { isOk = isAttrs x || isFunction x; } این سختگیرانه نیست، زیرا میتواند مجموعه ویژگی را پیش از ارزیابی x بازگرداند.
مقدار ویژگی برای isOk نسبت به x سختگیرانه است.
یک تابع با یک الگوی مجموعه، به عنوان پیامد بررسی نوع آرگومان و/یا نامهای ویژگی، همیشه نسبت به آرگومان خود سختگیرانه است:
let f = { ... }: "ok";
in f (throw "kablam")
=> error: kablam با این حال، یک الگوی مشخص هیچ سختگیری فراتر از شکل نرمال ضعیف سر (WHNF) آرگومان مجموعه ویژگی اضافه نمیکند.
let f = orig@{ x, ... }: "ok";
in f { x = throw "error"; y = throw "error"; }
=> "ok" ترتیب ارزیابی
ترتیبی که در آن عبارتها ارزیابی میشوند بهطور کلی نامشخص است، زیرا تأثیری بر نتایج موفقیتآمیز ارزیابی ندارد. این امر آزادی عمل بیشتری به ارزیاب میدهد تا تکامل یابد و بهطور کارآمد ارزیابی را انجام دهد.
وابستگیهای داده بهطور طبیعی برخی محدودیتهای ترتیبی را اعمال میکنند: یک مقدار را نمی فراتر از زمان محاسبهاش نمیتوان استفاده کرد. فراتر از این محدودیتها، ارزیاب در انتخاب هر ترتیبی آزاد است.
ترتیبی که در آن اثرات جانبی مانند خروجی builtins.trace رخ میدهد، تعریف نشده است، اما میتوان انتظار داشت که از وابستگیهای داده پیروی کند.
در یک زبان تنبل (lazy)، ترتیب ارزیابی اغلب برعکس انتظارات در زبانهای سختگیرانه (strict) است.
برای مثال، در عبارت let wrap = x: {'{'} wrapped = x; {'}'}; in wrap (1 + 2)، بدنه تابع پیش از ارزیابی x، نتیجهای ({'{'} wrapped = ...; {'}'}) تولید میکند.
بازگشتی بینهایت و سرریز پشته
هنگام ارزیابی، زمانی که عبارتها به خودشان ارجاع دهند یا توابع را بیش از حد عمیق فراخوانی کنند، دو نوع خطا ممکن است رخ دهد:
بازگشتی بینهایت
این خطا زمانی رخ میدهد که یک مقدار از طریق یک چرخه به خودش وابسته باشد و محاسبه آن را غیرممکن سازد.
let x = x; in x
=> error: infinite recursion encountered بازگشت بیپایان (infinite recursion) در سطح مقدار زمانی رخ میدهد که ارزیابی یک عبارت مستلزم ارزیابی مجدد همان عبارت باشد.
بر خلاف نام آن، ارزیابی بازگشت بیپایان کمهزینه است و منجر به سرریز پشته (stack overflow) نمیشود. این چرخه متناهی است و تشخیص آن نسبتاً آسان است.
سرریز پشته
این خطا زمانی رخ میدهد که عمق فراخوانی از حداکثر حد مجاز تجاوز کند.
let f = x: f (x + 1);
in f 0
=> error: stack overflow; max-call-depth exceeded سرریز پشته (stack overflow) زمانی رخ میدهد که تعداد زیادی فراخوانی تابع بدون بازگشت تو در تو شوند.
حداکثر عمق فراخوانی توسط تنظیمات max-call-depth کنترل میشود.