fennel-reference(5) Fennel Reference fennel-reference(5)

fennel-reference - مرجع جامع زبان برنامه‌نویسی فنل

این سند سینتکس، ماکروهای داخلی، و فرم‌های ویژه مورد تایید کامپایلر فنل (Fennel) را پوشش می‌دهد. این راهنما شامل توابع داخلی لوان (Lua) نمی‌شود؛ برای آن‌ها به دفترچه راهنمای مرجع Lua یا مقدمات Lua مراجعه کنید. این یک متن مقدماتی نیست؛ برای یادگیری اولیه به آموزش مقدماتی مراجعه کنید. اگر از قبل کدهای Lua دارید و مایلید ترجمه آن‌ها به Fennel را ببینید، از antifennel استفاده کنید.

ماکرو تابعی است که در زمان کامپایل اجرا می‌شود و کدهای Fennel را به کدهای متفاوت دیگری از Fennel تبدیل می‌کند. یک فرم ویژه (یا special) یک ساختار بنیادین اولیه است که کدهای Lua را مستقیماً تولید می‌کند. هنگام کدنویسی، نیازی نیست نگران تفاوت میان ماکروهای داخلی و فرم‌های ویژه باشید؛ این یک جزئیات پیاده‌سازی است.

به یاد داشته باشید که Fennel برای محیط زمان اجرای خود کاملاً به Lua وابسته است. هر کاری که Fennel انجام می‌دهد در زمان کامپایل رخ می‌دهد، بنابراین نیاز است با توابع کتابخانه استاندارد Lua آشنا شوید. خوشبختانه این کتابخانه از تقریباً هر زبان دیگری بسیار کوچک‌تر است.

تنها استثنا برای این قاعده زمان کامپایل، تابع fennel.view است که نمایشی متنی از هر داده Fennel را به شکل مناسب برای چاپ بازمی‌گرداند. اما این تابع بخشی از خود زبان نیست؛ یک تابع کتابخانه‌ای است که به همان آسانی از درون Lua نیز قابل استفاده است.

کد منبع Fennel باید متنی با انکودینگ UTF-8 باشد.

(پرانتزها): برای محدود کردن لیست‌ها استفاده می‌شود که اساساً برای نشان دادن فراخوانی توابع، ماکروها و فرم‌های ویژه به کار می‌روند. لیست‌ها ساختاری مربوط به زمان کامپایل هستند؛ در زمان اجرا استفاده نمی‌شوند. برای مثال: (print "hello world")

{آکولادها}: برای نشان دادن مقادیر صریح جدول‌های کلید/مقدار (که با نام دیکشنری نیز شناخته می‌شوند) استفاده می‌شود. برای مثال: {:a 1 :b 2} در یک جدول اگر کلید رشته‌ای داشته باشید که پس از آن نمادی با همان نام رشته آمده باشد، می‌توانید از : به عنوان کلید استفاده کنید و این کلید به رشته‌ای حاوی نام نماد بعدی گسترش می‌یابد.

{: this} ; is shorthand for {:this this}

[براکت‌ها]: برای نشان دادن جدول‌های ترتیبی (آرایه‌ها) استفاده می‌شود که هم برای ساختارهای داده‌ای صریح و هم در ماکروها و فرم‌های ویژه برای مشخص کردن محل معرفی شناسه‌های جدید (مانند لیست آرگومان‌ها یا مقیدسازی‌های let) به کار می‌روند. برای مثال: [1 2 3]

سینتکس اعداد مشابه با Luaاست، با این تفاوت که می‌توان از زیرخط (underscore) برای خوانایی بیشتر ارقام استفاده کرد. ارقام غیر ASCII هنوز پشتیبانی نمی‌شوند. بی‌نهایت و منفی بی‌نهایت به صورت .inf و -.inf نمایش داده می‌شوند. مقادیر NaN و منفی NaN به صورت .nan و -.nan هستند.

سینتکس رشته‌ها از نقل‌قول دوگانه " در دو طرف محتوای رشته استفاده می‌کند. نقل‌قول‌های دوگانه درون رشته باید با بک‌اسلش اسکیپ شوند. سینتکس این رشته‌ها همانند Luaاست، به جز اینکه رشته‌ها می‌توانند حاوی کاراکترهای خط جدید باشند. رشته‌های با نقل‌قول تکی یا براکت بلند پشتیبانی نمی‌شوند.

زبان Fennel محدودیت‌های بسیار کمتری نسبت به Lua بر روی شناسه‌ها دارد. شناسه‌ها با نمادها (symbols) نمایش داده می‌شوند، اما شناسه‌ها دقیقاً یکسان با نمادها نیستند؛ برخی نمادها توسط ماکروها برای مواردی غیر از شناسه‌ها استفاده می‌شوند. نمادها نباید با ارقام یا دو نقطه (:) شروع شوند، اما می‌توانند در هر جای دیگری ارقام داشته باشند. فراتر از آن، هر کاراکتر یونیکد پذیرفته می‌شود تا زمانی که غیرقابل‌چاپ یا فاصله خالی نباشد، یا یکی از کاراکترهای جداکننده ذکر شده در بالا، یکی از پیشوندهای فهرست شده در زیر یا یکی از این کاراکترهای رزرو شده نباشد:

•
نقل‌قول تکی: '
•
مدک (tilde): ~
•
نقطه‌ویرگول: ;
•
ات‌ساین: @

استفاده از زیرخط در نام شناسه‌ها مجاز است، اما خط تیره (-) به عنوان جداکننده کلمات ترجیح داده می‌شود. طبق قرارداد، شناسه‌هایی که با زیرخط شروع می‌شوند نشان می‌دهند که یک متغیر محلی مقید شده اما قرار نیست استفاده شود.

کاراکتر آمپرسند & در نمادها مجاز است اما در شناسه‌ها مجاز نیست. این ویژگی اجازه می‌دهد که برای ماکروها رزرو بماند، مانند رفتار &as در تخریب ساختار (destructuring).

نمادهایی که حاوی یک نقطه . یا دو نقطه : هستند، "چند نماد" (multi symbols) در نظر گرفته می‌شوند. بخش نماد قبل از اولین نقطه یا دو نقطه به عنوان شناسه استفاده می‌شود و بخش بعد از نقطه یا دو نقطه، فیلدی است که روی متغیر محلی مشخص‌شده جستجو می‌شود. دو نقطه فقط قبل از بخش پایانی یک چند نماد مجاز است، بنابراین x.y:z معتبر است اما a:b.c معتبر نیست. چند نمادهای دارای دو نقطه تنها برای فراخوانی متدها قابل استفاده هستند.

زبان Fennel همچنین از انواع خاصی از رشته‌ها که با دو نقطه شروع می‌شوند پشتیبانی می‌کند، تا زمانی که شامل کاراکترهایی نباشند که در نمادها مجاز نیست؛ به عنوان مثال :fennel-lang.org روش دیگری برای نوشتن رشته "fennel-lang.org" است.

فاصله‌ها، تب‌ها، خطوط جدید، تب‌های عمودی، form feedها و carriage returnها به عنوان فاصله خالی (whitespace) به حساب می‌آیند. کاراکترهای فاصله خالی غیر ASCII هنوز پشتیبانی نمی‌شوند.

پیشوندهای خاصی توسط پارسر به معادل‌های طولانی‌تر خود گسترش می‌یابند:

•
#foo به (hashfn foo) گسترش می‌یابد
•
`foo به (quote foo) گسترش می‌یابد
•
,foo به (unquote foo) گسترش می‌یابد

یک نقطه‌ویرگول و هر چه پس از آن تا انتهای خط بیاید یک کامنت است.

تابعی ایجاد می‌کند که آرگومان‌های داده شده در داخل براکت‌ها را مقید می‌سازد. هر تعداد آرگومان را می‌پذیرد؛ آرگومان‌های مازاد بر موارد تعریف‌شده نادیده گرفته می‌شوند و اگر آرگومان‌های کافی برای پوشش موارد تعریف‌شده ارائه نشود، به موارد باقیمانده مقدار nil داده می‌شود.

مثال:

(fn pxy [x y]
  (print (+ x y)))

نام‌گذاری تابع اختیاری است؛ اگر نامی ارائه شود به عنوان یک متغیر محلی مقید می‌شود. دو مورد زیر دقیقاً به یک معنا هستند؛ مورد اول بیشتر به دلایل تورفتگی ترجیح داده می‌شود، اما همچنین امکان بازگشت (recursion) را فراهم می‌کند:

(fn pxy [x y]
  (print (+ x y)))
(local pxy (fn [x y]
             (print (+ x y))))

ارائه نامی که یک فیلد جدول باشد باعث می‌شود به جای مقید شدن به عنوان متغیر محلی، درون یک جدول درج شود:

(local functions {})
(fn functions.p [x y z]
  (print (* x (+ y z))))
;; equivalent to:
(set functions.p (fn [x y z]
                   (print (* x (+ y z)))))

توابع در Fennel نیز همانند Lua از بهینه‌سازی فراخوانی دم (tail-call optimization) پشتیبانی می‌کنند که (از جمله موارد دیگر) به توابع اجازه می‌دهد بدون سرریز پشته تا بی‌نهایت بازگشت کنند، به شرطی که فراخوانی در موقعیت دم (tail position) قرار داشته باشد.

فرم نهایی در این ساختار و تمام فرم‌های توابع دیگر به عنوان مقدار بازگشتی استفاده می‌شود.

تابعی مشابه fn ایجاد می‌کند، اما اگر هر یک از آرگومان‌های فهرست‌شده مقدار nil داشته باشند در زمان اجرا خطا پرتاب می‌کند، مگر اینکه شناسه آن با ? شروع شود.

مثال:

(lambda [x ?y z]
  (print (- x (* (or ?y 1) z))))

توجه داشته باشید که محیط زمان اجرای Lua آرگومان‌های مفقود را در صورتی که توسط فراخواننده ارائه نشوند با nil پر می‌کند، بنابراین ارائه صریح آرگومان nil معمولاً تفاوتی با حذف یک آرگومان ندارد.

برنامه‌نویسانی که از زبان‌های دیگر می‌آیند و در آن‌ها فراخوانی تابع با تعداد آرگومانی متفاوت از آنچه تعریف شده خطا محسوب می‌شود، اغلب با رفتار fn دچار سردرگمی می‌شوند. اینجاست که lambda بیشترین کاربرد را دارد.

فرم‌های lambda، case، case-try، match و match-try تنها جاهایی هستند که نشانه‌گذاری ?foo توسط کامپایلر برای نشان دادن مجاز بودن مقدار nil استفاده می‌شود، اما این نشانه‌گذاری در هر جای دیگری که یک متغیر محلی جدید معرفی می‌شود برای انتقال قصد برنامه‌نویس مفید است.

فرم λ یک نام مستعار برای lambda است و رفتار کاملاً یکسانی دارد.

فرم‌های fn، lambda، λ و macro یک docstring اختیاری می‌پذیرند.

(fn pxy [x y]
  "Print the sum of x and y"
  (print (+ x y)))
(λ pxyz [x ?y z]
  "Print the sum of x, y, and z. If y is not provided, defaults to 0."
  (print (+ x (or ?y 0) z)))

این رشته‌ها به صورت پیش‌فرض در خارج از REPL نادیده گرفته می‌شوند، مگر اینکه متادیتا از خط فرمان (---metadata) یا گزینه‌های کامپایلر {useMetadata=true} فعال شده باشد؛ در این صورت در یک جدول متادیتا به همراه لیست آرگومان‌ها ذخیره می‌شوند و امکان مشاهده مستندات تابع از طریق ماکروی doc را فراهم می‌سازند.

;; this only works in the repl
>> ,doc pxy
(pxy x y)
  Print the sum of x and y

رشته‌های مستندات و سایر متادیتاها همچنین می‌توانند از طریق توابع روی API زبان با fennel.doc و fennel.metadata در دسترس قرار گیرند.

(از نسخه 1.1.0)

تمام فرم‌هایی که docstring می‌پذیرند، یک جدول متادیتا را نیز در همان محل قبول می‌کنند:

(fn add [...]
  {:fnl/docstring "Add arbitrary amount of numbers."
   :fnl/arglist [a b & more]}
  (case (values (select :# ...) ...)
    (0) 0
    (1 a) a
    (2 a b) (+ a b)
    (_ a b) (add (+ a b) (select 3 ...))))

در اینجا لیست آرگومان‌ها توسط آنچه در جدول متادیتا قرار دارد رونویسی می‌شود (توجه داشته باشید که محتویات جدول به طور ضمنی نقل‌قول می‌شوند). فراخوانی دستور ,doc در REPL، لیست آرگومان‌های مشخص‌شده در فرم بعدی را چاپ می‌کند:

>> ,doc add
(add a b & more)
  Add arbitrary amount of numbers.

(از نسخه 1.3.0)

کلیدهای متادیتای دلخواه در سینتکس جدول متادیتا مجاز هستند:

(fn foo []
  {:deprecated "v1.9.0"
   :fnl/docstring "*DEPRECATED* use foo2"}
  ;; old way to do stuff
  )
(fn foo2 [x]
  {:added "v2.0.0"
   :fnl/docstring "Incompatible but better version of foo!"}
  ;; do stuff better, now with x!
  x)

در این مثال، کلیدهای deprecated و added برای ذخیره نسخه یک کتابخانه فرضی که در آن توابع منسوخ یا اضافه شده‌اند استفاده می‌شوند. ابزارهای خارجی سپس می‌توانند با استفاده از API متادیتای Fennel از این اطلاعات بهره ببرند:

>> (local {: metadata} (require :fennel))
>> (metadata:get foo :deprecated)
"v1.9.0"
>> (metadata:get foo2 :added)
"v2.0.0"

چنین متادیتایی می‌تواند هر مقدار صریح داده‌ای از جمله جدول‌ها باشد، با این تنها محدودیت که فاقد عوارض جانبی (side effects) باشد. لیست‌های Fennel به عنوان مقادیر متادیتا مجاز نیستند.

(از نسخه 1.3.1)

برای راحتی ویرایش، مقادیر صریح جدول متادیتا بعد از docstringها نیز مجاز هستند:

(fn some-function [x ...]
  "Docstring for some-function."
  {:fnl/arglist [x & xs]
   :other :metadata}
  (let [xs [...]]
    ;; ...
    ))

در این حالت، رشته مستندات به طور خودکار توسط کامپایلر در جدول متادیتا درج می‌شود.

کل جدول متادیتا را می‌توان با فراخوانی metadata:get بدون آرگومان key به دست آورد:

>> (local {: metadata} (require :fennel))
>> (metadata:get some-function)
{:fnl/arglist ["x" "&" "xs"]
 :fnl/docstring "Docstring for some-function."
 :other "metadata"}

خود Fennel فقط از کلیدهای متادیتای fnl/docstring و fnl/arglist استفاده می‌کند، اما کدهای شخص ثالث می‌توانند از کلیدهای دلخواه بهره ببرند..SS "کوته‌نوشت لفظی توابع هش (Hash function literal shorthand)" ایجاد توابع لفظی بسیار ساده است، اما Fennel فرم حتی کوتاه‌تری از توابع را ارائه می‌دهد. توابع هش، توابع بی‌نام تک‌فرمی با آرگومان‌های نام‌گذاری‌شده به طور ضمنی هستند. همه توابع زیر از نظر عملکردی معادل هستند:

(fn [a b] (+ a b))
(hashfn (+ $1 $2)) ; implementation detail; don't use directly
#(+ $1 $2)

این سبک از توابع بی‌نام به عنوان پارامتر برای توابع مرتبه بالاتر بسیار مفید است. این روش فقط برای توابع تک‌خطی ساده که به عنوان آرگومان به سایر توابع ارسال می‌شوند توصیه می‌شود.

پیاده‌سازی فعلی تنها به توابع هش اجازه می‌دهد تا حداکثر از ۹ آرگومان استفاده کنند که هر کدام از $1 تا $9 نام‌گذاری شده‌اند، یا توابعی با تعداد متغیر آرگومان (varargs) که با $... به جای ... معمول مشخص می‌شوند. یک $ تنها در یک تابع هش به عنوان نام مستعاری برای $1 در نظر گرفته می‌شود.

توابع هش با ماکروی hashfn یا کاراکتر ویژه # تعریف می‌شوند که آرگومان منفرد خود را در یک تابع لفظی می‌پیچد. برای مثال،

#$3               ; same as (fn [x y z] z)
#[$1 $2 $3]       ; same as (fn [a b c] [a b c])
#{:a $1 :b $2}    ; same as (fn [a b] {:a a :b b})
#$                ; same as (fn [x] x) (aka the identity function)
#val              ; same as (fn [] val)
#[:one :two $...] ; same as (fn [...] ["one" "two" ...])

آرگومان‌های هش می‌توانند به عنوان بخش‌هایی از چندنمادها (multisyms) نیز استفاده شوند. برای نمونه، #$.foo تابعی است که مقدار کلید "foo" را در نخستین آرگومان خود بازمی‌گرداند.

برخلاف توابع معمولی، هیچ do ضمنی در یک تابع هش وجود ندارد و بنابراین نمی‌تواند شامل چندین فرم بدون یک do صریح باشد. خود بدنه مستقیماً به عنوان مقدار بازگشتی استفاده می‌شود، نه آخرین عنصر در بدنه.

تابع جدیدی بازمی‌گرداند که مانند نخستین آرگومان خود عمل می‌کند، اما چند آرگومان اول را با آرگومان‌های داده شده پر می‌کند. این با کری‌ینگ (currying) مرتبط است اما متفاوت است، زیرا فراخوانی آن تابع زیرین را فراخوانی می‌کند به جای اینکه منتظر بماند تا تعداد آرگومان‌های "صحیح" را دریافت کند.

مثال:

(fn add-print [x y] (print (+ x y)))
(partial add-print 2)

این مثال تابعی را بازمی‌گرداند که عددی را چاپ می‌کند که ۲ واحد بزرگتر از آرگومان ارسالی به آن است.

حوزه (scope) جدیدی را معرفی می‌کند که در آن مجموعه‌ای از پیوندهای محلی استفاده می‌شوند.

مثال:

(let [x 89
      y 198]
  (print (+ x y 12))) ; => 299

این متغیرهای محلی را نمی‌توان با set تغییر داد، اما می‌توانند توسط یک let یا local درونی سایه‌اندازی (shadow) شوند. در خارج از بدنه let، پیوندهایی که معرفی می‌کند دیگر قابل مشاهده نیستند. آخرین فرم در بدنه به عنوان مقدار بازگشتی استفاده می‌شود.

هر زمان که یک متغیر محلی را مقید می‌کنید، در صورتی که مقدار آن جدول باشد می‌توانید آن را تخریب ساختار (destructure) کنید:

مثال:

(let [[a b c] [1 2 3]]
  (+ a b c)) ; => 6

(از نسخه 1.5.0): اگر سمت چپ و سمت راست هر دو مقادیر صریح جدول باشند، تخصیص واقعی جدول بهینه‌سازی شده و حذف می‌شود و a مستقیماً بدون هیچ تخصیص حافظه‌ای به 1 مقید می‌شود.

اگر کلید جدول رشته‌ای با همان نام متغیر محلی باشد که می‌خواهید به آن مقید کنید، می‌توانید از کوته‌نوشت فقط : برای نام کلید و به دنبال آن نام متغیر محلی استفاده کنید. این قابلیت هم برای ساخت جدول‌ها و هم برای تخریب ساختار آن‌ها کار می‌کند.

مثال:

(let [{:msg message : val} {:msg "hello there" :val 19}]
  (print message)
  val) ; prints "hello there" and returns 19

هنگام تخریب ساختار یک جدول ترتیبی، می‌توانید تمام باقیمانده جدول را با استفاده از & در یک متغیر محلی ضبط کنید:

مثال:

(let [[a b & c] [1 2 3 4 5 6]]
  (table.concat c ",")) ; => "3,4,5,6"

(از نسخه 1.3.0): این ویژگی روی لیست آرگومان‌های تابع نیز کار می‌کند، اما بار سربار پردازشی کمی دارد، بنابراین توصیه می‌شود در مواردی که نسبت به سربار حساس هستند از ... استفاده شود.

هنگام تخریب ساختار یک جدول غیرترتیبی، می‌توانید جدول اصلی را در کنار تخریب ساختار با استفاده از &as ثبت کنید:

مثال:

(let [{:a a :b b &as all} {:a 1 :b 2 :c 3 :d 4}]
  (+ a b all.c all.d)) ; => 10

برای سازگاری با گذشته، می‌توانید چندین مقدار را با پرانتز در هر زمینه‌ای که از تخریب ساختار پشتیبانی می‌کند مقید کنید. این در نسخه‌های فعلی Fennel ضروری نیست، اما نسخه‌های قدیمی‌تر قبل از 1.5.0 جدول‌ها را در تخریب ساختار بهینه‌سازی نمی‌کردند، بنابراین برای مقیدسازی کارآمد لازم بود.

مثال:

(let [(x y z) (table.unpack [10 9 8])]
  (+ x y z)) ; => 27

متغیر محلی جدیدی را در یک حوزه موجود معرفی می‌کند. مشابه let اما بدون آرگومان بدنه. استفاده از آن در سطح بالای یک فایل برای متغیرهای محلی که در سراسر فایل استفاده خواهند شد توصیه می‌شود.

مثال:

(local tau-approx 6.28318)

از تخریب ساختار پشتیبانی می‌کند.

(از نسخه 1.3.0)

آرگومان اول خود را ارزیابی می‌کند، سپس در بندهای الگو/بدنه بعدی جستجو می‌کند تا بندی را پیدا کند که الگوی آن با مقدار مطابقت دارد و بدنه متناظر را ارزیابی می‌کند. تطبیق الگو را می‌توان ترکیبی از تخریب ساختار و دستورات شرطی در نظر گرفت.

توجه: زبان Lua نیز دارای "الگوهایی" است که با رشته‌ها مطابقت داده می‌شوند، شبیه به نحوه عملکرد عبارات منظم در سایر زبان‌ها؛ این دو مفاهیمی مجزا با نام‌های مشابه هستند.

مثال:

(case mytable
  59      :will-never-match-hopefully
  [9 q 5] (print :q q)
  [1 a b] (+ a b))

در مثال بالا، ما یک مقدار mytable داریم که سه بند الگو/بدنه به دنبال آن آمده است.

بند اول تنها در صورتی مطابقت دارد که mytable برابر با 59 باشد.

بند دوم در صورتی مطابقت خواهد داشت که mytable جدولی با 9 به عنوان عنصر اول، هر مقدار غیر nil به عنوان مقدار دوم و 5 به عنوان عنصر سوم باشد؛ اگر مطابقت داشته باشد، آنگاه (print :q q) را با q مقید به عنصر دوم mytable ارزیابی می‌کند.

بند پایانی تنها در صورتی مطابقت دارد که mytable دارای 1 به عنوان عنصر اول و دو مقدار غیر nil پس از آن باشد؛ در این صورت عناصر دوم و سوم را با هم جمع می‌کند.

اگر هیچ بندی مطابقت نداشته باشد، فرم به مقدار nil ارزیابی می‌شود.

الگوها می‌توانند جدول‌ها، مقادیر لفظی یا نمادها باشند. هر نمادی که با _ یا ? شروع نشود، به طور ضمنی بررسی می‌شود که nil نباشد. نمادها را می‌توان در یک عبارت تکرار کرد تا یکسان بودن مقدار بررسی شود.

مثال:

(case mytable
  ;; the first and second values of mytable are the same non-nil value
  [a a] (* a 2)
  ;; the first and second values are not nil and are not the same value
  [a b] (+ a b))

نکته مهم این است که عبارت‌ها به ترتیب بررسی می‌شوند! در مثال بالا، از آنجا که [a a] ابتدا بررسی می‌شود، می‌توانیم مطمئن باشیم زمانی که [a b] بررسی می‌شود، این دو مقدار حتماً متفاوت هستند. اگر ترتیب برعکس بود، [a b] همیشه تا زمانی که مقادیر nil نباشند مطابقت داشت - حتی اگر مقدار یکسانی داشته باشند!

شما می‌توانید با اضافه کردن پیشوند ? به یک نماد، اجازه دهید مقدار آن به صورت اختیاری nil باشد.

مثال:

(case mytable
  ;; not-nil, maybe-nil
  [a ?b] :maybe-one-maybe-two-values
  ;; maybe-nil == maybe-nil, both are nil or both are the same value
  [?a ?a] :maybe-none-maybe-two-same-values
  ;; maybe-nil, maybe-nil
  [?a ?b] :maybe-none-maybe-one-maybe-two-values)

نمادهایی که با _ شروع می‌شوند نادیده گرفته می‌شوند و می‌توانند به عنوان متغیرهای موقت مکانی یا نشانگرهایی برای "هر" مقداری - از جمله مقدار nil - عمل کنند. یک _ منفرد نیز اغلب در انتهای یک عبارت case برای تعریف یک مقدار فراگیر به سبک "else" استفاده می‌شود تا نشان دهد که متغیر محلی باید غیر nil باشد اما مقدار آن فراتر از آن استفاده نمی‌شود.

مثال:

(case mytable
  ;; not-nil, anything
  [a _b] :maybe-one-maybe-two-values
  ;; anything, anything (different to the previous ?a example!)
  ;; note this is effectively the same as []
  [_a _a] :maybe-none-maybe-one-maybe-two-values
  ;; anything, anything
  ;; this is identical to [_a _a] and in this example would never actually match.
  [_a _b] :maybe-none-maybe-one-maybe-two-values
  ;; when no other clause matched, in this case any non-table value
  _ :no-match)

جدول‌ها می‌توانند تودرتو باشند و می‌توانند از نوع ترتیبی (سبک []) یا کلید/مقدار (سبک {}) باشند. جدول‌های ترتیبی در صورتی مطابقت خواهند داشت که حداقل به اندازه الگو دارای عنصر باشند. (برای مجاز دانستن nil بودن یک عنصر، توضیحات ? و _ را در بالا ببینید.) جدول‌ها هرگز به دلیل داشتن عناصر زیاد در مطابقت شکست نمی‌خورند - این بدان معناست که [] با هر جدولی مطابقت دارد، نه فقط با یک جدول خالی. می‌توانید درست همانند let از & برای ضبط تمام عناصر باقیمانده یک جدول ترتیبی استفاده کنید.

(case mytable
  {:subtable [a b ?c] :depth depth} (* b depth)
  _ :unknown)

همچنین می‌توانید با استفاده از پرانتز با چندین مقدار بازگشتی مطابقت دهید. (این موارد نمی‌توانند تودرتو باشند، اما می‌توانند حاوی جدول‌ها باشند.) این قابلیت برای بررسی خطاها می‌تواند بسیار مفید باشد.

(case (io.open "/some/file")
  (nil msg) (report-error msg)
  f (read-file f))

گاهی اوقات نیاز دارید بر روی چیزی کلی‌تر از یک ساختار یا مقدار خاص مطابقت انجام دهید. در این موارد می‌توانید از بندهای محافظ استفاده کنید:

(case [91 12 53]
  (where [a b c] (= 5 a)) :will-not-match
  (where [a b c] (= 0 (math.fmod (+ a b c) 2)) (= 91 a)) c) ; -> 53

در این حالت الگو باید در پرانتز قرار گیرد اما اولین چیز در پرانتز نماد where است. هر فرم بعد از الگو یک شرط است؛ تمام شرایط باید به درستی ارزیابی شوند تا آن الگو مطابقت یابد.

اگر چندین الگو بدنه و محافظ‌های یکسانی داشته باشند، چنین الگوهایی را می‌توان با فرم ویژه or در بند where ترکیب کرد:

(case [5 1 2]
  (where (or [a 3 9] [a 1 2]) (= 5 a)) "Either [5 3 9] or [5 1 2]"
  _ "anything else")

این اساساً معادل مورد زیر است:

(case [5 1 2]
  (where [a 3 9] (= 5 a)) "Either [5 3 9] or [5 1 2]"
  (where [a 1 2] (= 5 a)) "Either [5 3 9] or [5 1 2]"
  _ "anything else")

با این حال، الگوهایی که متغیرها را مقید می‌کنند نباید با or ترکیب شوند اگر متغیرهای متفاوتی در الگوهای مختلف مقید شوند یا برخی متغیرها وجود نداشته باشند:

;; bad
(case [1 2 3]
  ;; Will fail to compile because `b' is not present in the first
  ;; pattern but the guard still uses it.
  (where (or [a 1 2] [a b 3]) (< a 0) (< b 1))
  :body)
;; ok
(case [1 2 3]
  (where (or [a b 2] [a b 3]) (< a 0) (<= b 1))
  :body)

نمادهای مقید شده در داخل یک الگوی case از هر نماد موجود در حوزه فعلی مستقل هستند، به این معنی که نام‌ها را می‌توان بدون هیچ عواقبی دوباره استفاده کرد.

مثال:

(let [x 1]
  (case [:hello]
    ;; `x` is simply bound to the first value of [:hello]
    [x] x)) ; -> :hello

گاهی ممکن است تطبیق با یک مقدار موجود در حوزه بیرونی مطلوب باشد. برای انجام این کار می‌توانیم یک پیوند را در داخل الگو با فرم یگانی (= binding-name) به یک پیوند بیرونی موجود "پین" کنیم. فرم یگانی (= binding-name) تنها در یک الگوی case معتبر است و باید در داخل یک محافظ (where) قرار گیرد.

مثال:.IP

(let [x 1]
  (case [:hello]
    ;; 1 != :hello
    (where [(= x)]) x
    _ :no-match)) ; -> no-match
(let [x 1]
  (case [1]
    ;; 1 == 1
    (where [(= x)]) x
    _ :no-match)) ; -> 1
(let [pass :hunter2]
  (case (user-input)
    (where (= pass)) :login
    _ :try-again!))

پین کردن فقط در داخل الگو لازم است. پیوندهای بیرونی به طور خودکار در داخل محافظ‌ها و بدنه‌ها در دسترس هستند، مادامی که نام در الگو دوباره مقید نشده باشد.

توجه: ماکروی case می‌تواند به جای ماکروی if-let از کلوژر (Clojure) استفاده شود. دلیل اینکه Fennel ماکروی if-let را ندارد این است که case آن را زائد و بی‌نیاز می‌سازد.

ماکروی match از نظر مفهومی معادل case است، با این تفاوت که نمادها در الگوها همیشه با نمادهای حوزه بیرونی در صورت وجود پین می‌شوند.

از تمامی سینتکس‌های مشابه‌ای که در case توضیح داده شد پشتیبانی می‌کند به جز عبارت پین ((= binding-name))، چرا که این پین کردن همیشه به طور خودکار انجام می‌شود.

هنگام استفاده از match مراقب باشید که نمادهای شما تصادفاً همانند هیچ‌یک از نمادهای موجود نباشند! اگر می‌دانید که قصد پین کردن نمادهای موجود را ندارید، باید از عبارت case استفاده کنید.

(let [x 95]
 (match [52 85 95]
   [b a a] :no ; because a=85 and a=95
   [x y z] :no ; because x=95 and x=52
   [a b x] :yes)) ; a and b are fresh values while x=95 and x=95

برخلاف case، اگر یک پیوند موجود مقدار nil داشته باشد، پیشوند ? لازم نیست - چرا که در عوض یک پیوند جدید و غیرپین‌شده ایجاد خواهد کرد!

مثال:

(let [name nil
      get-input (fn [] "Dave")]
  (match (get-input)
    ;; name already exists as nil, "Dave" != nil so this *wont* match
    name (.. "Hello " name)
    ?no-input (.. "Hello anonymous"))) ; -> "Hello anonymous"

توجه: پیش از نسخه 0.9.0 در Fennel، ماکروی match از عملگر میان‌وند ? برای آزمودن الگوها در برابر محافظ‌ها استفاده می‌کرد. گرچه این سینتکس هنوز پشتیبانی می‌شود، اما باید where ترجیح داده شود:

(match [1 2 3]
  (where [a 2 3] (< 0 a)) "new guard syntax"
  ([a 2 3] ? (< 0 a)) "obsolete guard syntax")

مجموعه‌ای از مراحل تطبیق الگو را ارزیابی می‌کند. مقدار عبارت اول با الگوی اول مطابقت داده می‌شود. اگر مطابقت داشته باشد، بدنه اول ارزیابی می‌شود و مقدار آن با الگوی دوم مطابقت داده می‌شود و به همین ترتیب ادامه می‌یابد.

اگر یک فرم (catch pat1 body1 pat2 body2 ...) در انتها وجود داشته باشد، هر عدم تطابقی از مراحل به عنوان یک جایگزین مانند یک case معمولی در برابر این الگوها به ترتیب آزموده می‌شود. اگر هیچ الگوی catch مطابقت نداشته باشد، nil بازگردانده می‌شود.

اگر هیچ catch وجود نداشته باشد، مقدار عدم تطابق به عنوان مقدار کل عبارت بازگردانده خواهد شد.

(fn handle [conn token]
  (case-try (conn:receive :*l)
    input (parse input)
    (where (command-name params (= token))) (commands.get command-name)
    command (pcall command (table.unpack params))
    (catch
     (_ :timeout) nil
     (_ :closed) (pcall disconnect conn "connection closed")
     (_ msg) (print "Error handling input" msg))))

این فرم زمانی مفید است که می‌خواهید مجموعه‌ای از مراحل را انجام دهید که هر یک از آن‌ها ممکن است با شکست مواجه شود. بند catch به شما امکان می‌دهد تمام مدیریت خطای خود را در یک مکان متمرکز نگه دارید. توجه داشته باشید که دو روش برای نشان دادن شکست در Fennel و Lua وجود دارد: استفاده از توابع assert/error یا بازگرداندن nil به همراه داده‌هایی که بیانگر شکست هستند. این فرم فقط روی حالت دوم کار می‌کند، اما می‌توانید از pcall برای تبدیل فراخوانی‌های error به مقادیر استفاده کنید.

معادل case-try است اما به طور داخلی از match استفاده می‌کند. برای جزئیات تفاوت‌های این دو فرم، توضیحات case و match را ببینید.

برخلاف case-try، ماکروی match-try مقادیر یک بلوک catch داده شده را با مقادیر مراحل اصلی پین می‌کند.

(fn handle [conn token]
  (match-try (conn:receive :*l)
    input (parse input)
    (command-name params token) (commands.get command-name)
    command (pcall command (table.unpack params))
    (catch
      (_ :timeout) nil
      (_ :closed) (pcall disconnect conn "connection closed")
      (_ msg) (print "Error handling input" msg))))

یک متغیر محلی جدید را در یک حوزه موجود معرفی می‌کند که می‌تواند مقدار آن تغییر کند. به جز امکان کار کردن set بر روی آن، کاملاً همانند local است.

مثال:

(var x 83)

از تخریب ساختار پشتیبانی می‌کند.

مقدار یک متغیر معرفی‌شده با var را تغییر می‌دهد. روی متغیرهای سراسری (globals) یا متغیرهای محلی مقید به let/local کار نخواهد کرد. همچنین می‌تواند برای تغییر فیلد یک جدول استفاده شود، حتی اگر آن جدول با let یا local مقید شده باشد. اگر نام فیلد جدول ایستا باشد، از tbl.field استفاده کنید؛ اگر نام فیلد پویا باشد، از (. tbl field) استفاده کنید.

مثال‌ها:

(set x (+ x 91)) ; var
(let [t {:a 4 :b 8}] ; static table field
  (set t.a 2) t) ; => {:a 2 :b 8}
(let [t {:supported-chars {:x true}}
      field1 :supported-chars
      field2 :y] ; dynamic table field
  (set (. t field1 field2) true) t) ; => {:supported-chars {:x true :y true}}

این دستور نیز از تخریب ساختار پشتیبانی می‌کند.

فیلد یک جدول داده شده را روی مقدار جدیدی تنظیم می‌کند.

مثال:

(let [tbl {:d 32} field :d]
  (tset tbl field 19) tbl) ; => {:d 19}

می‌توانید چندین نام فیلد متوالی را برای انجام تنظیم تودرتو ارائه دهید. برای مثال:

(let [tbl {:a {:b {}}} field :c]
  (tset tbl :a :b field "d") tbl) ; => {:a {:b {:c "d"}}}

از نسخه 1.5.0، دستور tset تا حد زیادی زائد است زیرا set می‌تواند برای فیلدهای جدول استفاده شود. استثنای اصلی این است که tset با doto کار می‌کند ولی set کار نمی‌کند.

اگرچه Lua هنگام جمع‌آوری زباله (garbage collection) به طور خودکار هندل یک فایل باز را می‌بندد، اما GC ممکن است بلافاصله اجرا نشود؛ with-open تضمین می‌کند که هندل‌ها بلافاصله، چه با خطا و چه بدون خطا، بدون نیاز به کدهای تکراری بسته می‌شوند.

نحوه استفاده مشابه let است، به جز اینکه:

•
تخریب ساختار مجاز نیست (فقط نمادها در سمت چپ قرار می‌گیرند)
•
هر پیوند باید یک هندل فایل یا مقدار دیگری با متد :close باشد.

پس از اجرای بدنه، یا در صورت برخورد با خطا، with-open پیش از بازگرداندن نتایج، (value:close) را روی هر متغیر مقید شده فراخوانی می‌کند.

به طور معمول بدنه به صورت ضمنی در یک تابع پیچیده شده و با xpcall اجرا می‌شود تا تمام هندل‌های مقید قبل از انتشار مجدد خطا بسته شوند. با این حال می‌توانید از --to-be-closed استفاده کنید تا از قابلیت بومی در نسخه Lua 5.4+ استفاده کند که همان کار را بدون تداخل xpcall با ردگیری پشته (stack trace) انجام می‌دهد.

مثال:

;; Basic usage
(with-open [fout (io.open :output.txt :w) fin (io.open :input.txt)]
  (fout:write "Here is some text!\n")
  ((fin:lines))) ; => first line of input.txt
;; This demonstrates that the file will also be closed upon error.
(var fh nil)
(local [ok err]
  [(pcall #(with-open [file (io.open :test.txt :w)]
             (set fh file) ; you would normally never do this
             (error :whoops!)))])
(io.type fh) ; => "closed file"
[ok err]     ; => [false "<error message and stacktrace>"]

هنگام کار با چند مقداری‌ها (...) و بازگشت‌های چندگانه، تمام مقادیر بعد از n مقدار اول را نادیده می‌گیرد. برای ترکیب توابعی که چندین مقدار بازمی‌گردانند با توابع با تعداد متغیر آرگومان (variadic) مفید است. به عبارت let گسترش می‌یابد که دقیقاً n مقدار را مقید کرده و مجدداً منتشر می‌کند، برای مثال:

(pick-values 2 (func))

گسترش می‌یابد به:

(let [[_0_ _1_] [(func)]] (values _0_ _1_))

مثال:

(pick-values 0 :a :b :c :d :e) ; => nil
[(pick-values 2 (table.unpack [:a :b :c]))] ;-> ["a" "b"]
(fn add [x y ...]
  (let [sum (+ (or x 0) (or y 0))]
    (if (= ... nil)
      sum
      (add sum ...))))
(add (pick-values 2 10 10 10 10)) ; => 20
(->> [1 2 3 4 5] (table.unpack) (pick-values 3) (add)) ; => 6

توجه: اگر n بزرگتر از تعداد مقادیر ارائه شده باشد، همچنان n مقدار منتشر خواهد شد. این موضوع هنگام استفاده از (select "#" ...) برای شمارش varargs منعکس می‌شود، اما جدول‌های [...] مقادیر nil انتهایی را نادیده می‌گیرند:

(select :# (pick-values 5 "one" "two")) ; => 5
[(pick-values 5 "one" "two")]           ; => ["one" "two"]

شرطی را بررسی می‌کند و بدنه متناظر را ارزیابی می‌نماید. هر تعداد جفت شرط/بدنه را می‌پذیرد؛ اگر تعداد آرگومان‌های ارسالی فرد باشد، مقدار آخر به عنوان یک "else" فراگیر در نظر گرفته می‌شود. مشابه cond در سایر گویش‌های لیسپ.

مثال:

(let [x (math.random 64)]
  (if (= 0 (% x 10))
      "multiple of ten"
      (= 0 (% x 2))
      "even"
      "I dunno, something else"))

تمام مقادیری به جز nil یا false به عنوان true در نظر گرفته می‌شوند.

یک شرط منفرد دریافت می‌کند و اگر آن شرط nil یا false نباشد، باقیمانده را به عنوان یک بدنه ارزیابی می‌کند. این فرم برای ایجاد عوارض جانبی در نظر گرفته شده است. آخرین فرم در بدنه به عنوان مقدار بازگشتی استفاده می‌شود.

مثال:

(when launch-missiles?
  (power-on)
  (open-doors)
  (fire))

بدنه را یک بار برای هر مقدار ارائه‌شده توسط پیمایشگر (iterator) اجرا می‌کند. معمولاً با ipairs (برای جدول‌های ترتیبی) یا pairs (برای هر جدولی با ترتیب نامشخص) استفاده می‌شود، اما می‌تواند با هر پیمایشگری به کار رود. مقدار nil را بازمی‌گرداند.

مثال:

(each [key value (pairs mytbl)]
  (print "executing key")
  (print (f value)))

هر حلقه‌ای را می‌توان با قرار دادن یک بند &until در انتهای پیوندها زودتر متوقف کرد:

(local out [])
(each [_ value (pairs tbl) &until (< max-len (length out))]
  (table.insert out value))

توجه: قبل از نسخه 1.2.0 در fennel، از :until به جای &until استفاده می‌شد؛ سینتکس قدیمی هنوز برای سازگاری با گذشته پشتیبانی می‌شود.

بیشتر پیمایشگرها دو مقدار بازمی‌گردانند، اما each هر تعداد مقداری را مقید خواهد کرد. برای آگاهی از جزئیات نحوه عملکرد پیمایشگرها به برنامه‌نویسی در Lua مراجعه کنید.

یک عدد را از نقطه شروع تا نقطه پایان (شامل خود آن) می‌شمارد و بدنه را یک بار به ازای هر مقدار ارزیابی می‌کند. یک گام شمارش (step) اختیاری را می‌پذیرد. مقدار nil بازمی‌گرداند.

مثال:.IP

(for [i 1 10 2]
  (log-number i)
  (print i))

این مثال تمامی اعداد فرد کمتر از ده را چاپ می‌کند.

همانند each، حلقه‌های با استفاده از for را نیز می‌توان با یک بند &until زودتر متوقف کرد. این بند قبل از هر تکرار بدنه بررسی می‌شود؛ اگر در ابتدا مقدار آن درست باشد، بدنه اصلاً اجرا نخواهد شد.

(var x 0)
(for [i 1 128 &until (maxed-out? x)]
  (set x (+ x i)))

تا زمانی که شرط برقرار باشد روی یک بدنه تکرار انجام می‌دهد. از یک حلقه while بومی Lua استفاده می‌کند. مقدار nil بازمی‌گرداند.

مثال:

(var done? false)
(while (not done?)
  (print :not-done)
  (when (< 0.95 (math.random))
    (set done? true)))

هر تعداد فرم را می‌پذیرد و همه آن‌ها را به ترتیب ارزیابی کرده و آخرین مقدار را بازمی‌گرداند. این دستور برای درج عوارض جانبی در فرمی که فقط یک مقدار واحد را می‌پذیرد استفاده می‌شود، مانند بدنه‌ای از if زمانی که چندین بند وجود دارد به طوری که نمی‌توانید از when استفاده کنید. برخی گویش‌های لیسپ این را begin یا progn می‌نامند.

(if launch-missiles?
    (do
      (power-on)
      (open-doors)
      (fire))
    false-alarm?
    (promote lt-petrov))

برخی فرم‌های دیگر مانند fn و let دارای یک do ضمنی هستند.

•
and، or، not: بولی (boolean)
•
+، -، *، /، //، %، ^: حسابی (arithmetic)
•
>، <، >=، <=، =، not=: مقایسه‌ای (comparison)
•
lshift، rshift، band، bor، bxor، bnot: عملیات بیتی (bitwise operations)

همه این‌ها همان‌طور که انتظار دارید کار می‌کنند، با چند نکته قابل توجه. عملگرهای بیتی تنها در نسخه Lua 5.3+ در دسترس هستند، مگر اینکه از فلگ --use-bit-lib یا فلگ useBitLib در جدول گزینه‌ها استفاده کنید که امکان استفاده از آن‌ها را در LuaJIT فراهم می‌سازد. عملگر تقسیم صحیح (//) تنها در نسخه Lua 5.3+ در دسترس است.

همه آن‌ها هر تعداد آرگومان را می‌پذیرند، تا زمانی که این تعداد در زمان کامپایل ثابت باشد. برای نمونه، (= 2 2 (table.unpack [2 5])) به true ارزیابی می‌شود زیرا تعداد مقادیر مورد مقایسه در زمان کامپایل ۳ است. مقادیر چندگانه در زمان اجرا به حساب نمی‌آیند.

توجه داشته باشید که این‌ها همگی فرم‌های ویژه هستند و نمی‌توانند به عنوان توابع مرتبه بالاتر استفاده شوند.

آرگومان‌های خود را در یک رشته واحد الحاق می‌کند. اعداد را به رشته تبدیل (coerce) می‌کند، اما سایر انواع داده را خیر.

مثال:

(.. "Hello" " " "world" 7 "!!!") ; => "Hello world7!!!"

الحاق رشته مشمول همان محدودیت زمان کامپایل عملگرهای بالا است؛ از مقادیر چندگانه در زمان اجرا مطلع نیست.

طول یک رشته یا جدول را بازمی‌گرداند. توجه داشته باشید که طول یک جدول که دارای شکاف (nilها) باشد تعریف‌نشده است؛ می‌تواند عددی متناظر با هر یک از موقعیت‌های "مرزی" جدول بین مقادیر nil و غیر nil را بازگرداند. اگر جدولی دارای nil باشد و بخواهید آخرین شاخص عددی متوالی که از ۱ شروع می‌شود را بدانید، باید خودتان آن را با ipairs محاسبه کنید؛ اگر می‌خواهید حداکثر کلید عددی را در یک جدول دارای nil بدانید، می‌توانید در Lua <= 5.2 از table.maxn استفاده کنید.

مثال:

(+ (length [1 2 3 nil 8]) (length "abc")) ; => 6 or 8

یک کلید داده شده را در جدول جستجو می‌کند. چندین آرگومان جستجوی تودرتو را انجام خواهند داد.

مثال:

(. mytbl myfield)

مثال:

(let [t {:a [2 3 4]}] (. t :a 2)) ; => 3

توجه داشته باشید که اگر نام فیلد رشته‌ای باشد که در زمان کامپایل مشخص است، نیازی به این کار ندارید و می‌توانید به سادگی از mytbl.field استفاده کنید.

یک کلید داده شده را در جدول جستجو می‌کند. چندین آرگومان جستجوی تودرتو انجام می‌دهند. اگر هر یک از کلیدهای بعدی وجود نداشته باشد، ارزیابی زودرس انجام داده و مقدار nil را بازمی‌گرداند.

مثال:

(?. mytbl myfield)

مثال:

(let [t {:a [2 3 4]}] (?. t :a 4 :b)) ; => nil
(let [t {:a [2 3 4 {:b 42}]}] (?. t :a 4 :b)) ; => 42

ماکروی icollect یک "جدول مقیدسازی پیمایشگر" به فرمتی که each می‌پذیرد دریافت می‌کند و یک جدول ترتیبی شامل تمام مقادیر تولید شده توسط هر تکرار از بدنه ماکرو بازمی‌گرداند. این شبیه نحوه عملکرد map در چندین زبان دیگر است، اما یک ماکرو است، نه یک تابع.

اگر مقدار nil باشد، از جدول بازگشتی حذف می‌شود. این مشابه filter در سایر زبان‌ها است.

(icollect [_ v (ipairs [1 2 3 4 5 6])]
  (if (< 2 v) (* v v)))
;; -> [9 16 25 36]
;; equivalent to:
(let [tbl []]
  (each [_ v (ipairs [1 2 3 4 5 6])]
    (tset tbl (+ (length tbl) 1) (if (< 2 v) (* v v))))
  tbl)

ماکروی collect تقریباً یکسان است، با این تفاوت که بدنه باید دو چیز بازگرداند: یک کلید و یک مقدار.

(collect [k v (pairs {:apple "red" :orange "orange" :lemon "yellow"})]
  (if (not= v "yellow")
      (values (.. "color-" v) k)))
;; -> {:color-orange "orange" :color-red "apple"}
;; equivalent to:
(let [tbl {}]
  (each [k v (pairs {:apple "red" :orange "orange"})]
    (if (not= v "yellow")
      (match [(.. "color-" v) k]
        [key value] (tset tbl key value))))
  tbl)

اگر کلید و مقدار مستقیماً در بدنه collect داده شوند و در یک فرم بیرونی تودرتو نباشند، برای اختصار می‌توان values را حذف کرد:

(collect [k v (pairs {:a 85 :b 52 :c 621 :d 44})]
  k (* v 5))

همانند each و for، ماکروهای درک جدول از یک بند &until برای پایان زودهنگام پشتیبانی می‌کنند.

هر دو ماکروی icollect و collect یک بند &into می‌پذیرند که به شما امکان می‌دهد نتایج خود را در یک جدول موجود قرار دهید به جای اینکه با یک جدول خالی شروع کنید:

(icollect [_ x (ipairs [2 3]) &into [9]]
  (* x 11))
;; -> [9 22 33]

توجه: پیش از نسخه 1.2.0 در fennel، از :into به جای &into استفاده می‌شد؛ سینتکس قدیمی هنوز برای سازگاری با گذشته پشتیبانی می‌شود.

پیمایشگری را اجرا می‌کند و عمل تجمع را انجام می‌دهد، شبیه به fold و reduce که معمولاً در زبان‌های برنامه‌نویسی تابعی استفاده می‌شوند. مانند collect و icollect، یک جدول مقیدسازی پیمایشگر و یک عبارت را به عنوان آرگومان‌های خود می‌پذیرد. تفاوت در این است که در accumulate، دو مورد اول در جدول مقیدسازی به عنوان یک متغیر "انباشتگر" (accumulator) و مقدار اولیه آن استفاده می‌شوند. به ازای هر مرحله تکرار، عبارت داده شده را ارزیابی می‌کند و مقدار آن به متغیر انباشتگر بعدی تبدیل می‌شود. ماکروی accumulate مقدار نهایی متغیر انباشتگر را بازمی‌گرداند.

مثال:

(accumulate [sum 0
             i n (ipairs [10 20 30 40])]
    (+ sum n)) ; -> 100

بند &until در اینجا نیز برای توقف زودهنگام پشتیبانی می‌شود.

(از نسخه 1.3.0)

همانند accumulate است، اما به جای دریافت یک پیمایشگر و همان پیوندهای each، همان پیوندهای for را می‌پذیرد و بازه عددی را پیمایش می‌کند. درست مانند for و accumulate، بند &until را می‌پذیرد.

مثال:

(faccumulate [n 0 i 1 5] (+ n i)) ; => 15

(از نسخه 1.1.1)

مشابه با icollect، ماکروی fcollect روشی برای ساخت یک جدول ترتیبی ارائه می‌دهد. برخلاف icollect، به جای یک پیمایشگر، یک بازه عددی را طبق آنچه فرم ویژه for می‌پذیرد می‌پیماید. بندهای &into و &until همانند icollect عمل می‌کنند.

مثال:

(fcollect [i 0 10 2]
  (if (> i 2) (* i i)))
;; -> [16 36 64 100]
;; equivalent to:
(let [tbl {}]
  (for [i 0 10 2]
    (if (> i 2)
        (table.insert tbl (* i i))))
  tbl)

چندین مقدار را از یک تابع بازمی‌گرداند. معمولاً برای اعلام شکست با بازگرداندن nil به همراه یک پیام خطا استفاده می‌شود.

مثال:

(fn [filename]
  (if (valid-file-name? filename)
      (open-file filename)
      (values nil (.. "Invalid filename: " filename))))

تابعی را در یک جدول جستجو می‌کند و آن را با جدول به عنوان اولین آرگومانش فراخوانی می‌نماید. این یک اصطلاح رایج در بسیاری از APIهای Lua، از جمله برخی از موارد داخلی است.

دقیقاً مانند Lua، می‌توانید با فراخوانی نام تابعی که در آن : متغیر جدول و نام متد را از هم جدا می‌کند، یک فراخوانی متد انجام دهید.

مثال:

(let [f (assert (io.open "hello" "w"))]
  (f:write "world")
  (f:close))

در مثال بالا، f:write یک چندنماد منفرد است. اگر نام متد یا جدولی که آن را در بر دارد ثابت نباشد، می‌توانید از : و به دنبال آن جدول و سپس نام متد استفاده کنید تا بتواند در عوض یک رشته پویا باشد:

مثال:

(let [f (assert (io.open "hello" "w"))
      method1 :write
      method2 :close]
  (: f method1 "world")
  (: f method2))

هر دوی این مثال‌ها معادل مورد زیر هستند:

(let [f (assert (io.open "hello" "w"))]
  (f.write f "world")
  (f.close f))

برخلاف Lua، هیچ چیز خاصی در مورد تعریف توابعی که به این روش فراخوانی می‌شوند وجود ندارد؛ معمولاً یک آرگومان اضافی به نام self به آن داده می‌شود اما این فقط یک قرارداد است؛ می‌توانید هر نامی روی آن بگذارید.

(local t {})
(fn t.enable [self]
  (set self.enabled? true))
(t:enable)

ماکروی -> مقدار اول خود را می‌گیرد و آن را به عنوان اولین آرگومان در فرم دوم قرار می‌دهد. نتیجه ارزیابی فرم دوم به عنوان اولین آرگومان فرم سوم درج می‌شود و به همین ترتیب ادامه می‌یابد.

مثال:

(-> 52
    (+ 91 2) ; (+ 52 91 2)
    (- 8)    ; (- (+ 52 91 2) 8)
    (print "is the answer")) ; (print (- (+ 52 91 2) 8) "is the answer")

ماکروی ->> نیز به همین صورت عمل می‌کند، با این تفاوت که مقدار را در آخرین موقعیت هر فرم قرار می‌دهد نه موقعیت اول.

ماکروهای -?> و -?>> (ماکروهای ریسه‌سازی شرطی یا thread maybe)، شبیه به -> و ->> هستند، اما پس از ارزیابی هر فرم ریسه‌شده بررسی نیز انجام می‌دهند. اگر نتیجه false یا nil باشد، ریسه‌سازی متوقف شده و همان نتیجه بازگردانده می‌شود. -?> مقدار ریسه‌شده را مانند -> در جایگاه نخستین آرگومان قرار می‌دهد و -?>> آن را مانند ->> در آخرین جایگاه قرار می‌دهد.

این مثال نشان می‌دهد که چگونه می‌توان از آن‌ها برای جلوگیری از نمایه‌سازی تصادفی روی مقدار nil استفاده کرد:.IP

(-?> {:a {:b {:c 42}}}
     (. :a)
     (. :missing)
     (. :c)) ; -> nil
(-?>> :a
      (. {:a :b})
      (. {:b :missing})
      (. {:c 42})) ; -> nil

در حالی که -> و ->> چندین مقدار را بدون هیچ مشکلی عبور می‌دهند، بررسی‌ها در -?> و -?>> بدون بار اضافی بر عملکرد از رخ دادن همین حالت جلوگیری می‌کنند، بنابراین این خطوط لوله به یک مقدار واحد محدود هستند.

توجه داشته باشید که این‌ها هیچ ارتباطی با "نخ‌ها" (threads) که برای همزمانی استفاده می‌شوند ندارند؛ آن‌ها بر اساس نخی که در خیاطی استفاده می‌شود نام‌گذاری شده‌اند. این مشابه روشی است که |> در OCaml و Elixir کار می‌کند.

به طور مشابه، ماکروی doto مقدار اول را در فرم‌های بعدی قرار می‌دهد. با این حال، همان مقدار را حفظ می‌کند و به طور مداوم همان چیز را در فرم‌ها قرار می‌دهد به جای اینکه از مقدار فرم قبلی برای فرم بعدی استفاده کند.

(doto (io.open "/tmp/err.log")
  (: :write contents)
  (: :close))
;; equivalent to:
(let [x (io.open "/tmp/err.log")]
  (: x :write contents)
  (: x :close)
  x)

فرم اول به مقدار بازگشتی برای کل عبارت تبدیل می‌شود و فرم‌های بعدی صرفاً برای عوارض جانبی ارزیابی می‌شوند.

فراخوانی‌های دم به طور خودکار بهینه‌سازی می‌شوند. با این حال، فرم tail! تایید می‌کند که آرگومان آن در موقعیت دم (tail position) فراخوانی شده است. می‌توانید از این زمانی استفاده کنید که کد به بهینه‌سازی فراخوانی دم وابسته است؛ به این ترتیب اگر کد تغییر کند به طوری که فراخوانی بازگشتی دیگر در موقعیت دم نباشد، باعث ایجاد خطای کامپایل می‌شود به جای اینکه بعداً روی مجموعه‌های داده بزرگ با سرریز پشته مواجه شوید.

(fn process-all [data i]
  (case (process (. data i))
    :done (print "Process completed.")
    :next (process-all data (+ i 1))
    :skip (do (tail! (process-all data (+ i 2)))
;;             ^^^^^ Compile error: Must be in tail position
              (print "Skipped" (+ i 1)))))

(include :my.embedded.module)

کد ماژول Fennel/Lua را در زمان کامپایل بارگذاری کرده و در خروجی کامپایل‌شده جاسازی می‌کند. نام ماژول در حین کامپایل باید به یک رشته صریح حل شود. کد بسته‌بندی‌شده در یک فراخوانی تابع در Lua تولیدشده پیچیده می‌شود و روی package.preload[modulename] تنظیم می‌گردد؛ سپس یک require معمولی در جایی که include برای بارگذاری آن بر حسب تقاضا به عنوان یک ماژول معمولی استفاده شده بود، منتشر می‌شود.

در بیشتر موارد بهتر است از require در کد خود استفاده کنید و گزینه requireAsInclude در مستندات API و فلگ خط فرمان --require-as-include (fennel --help) را برای دستیابی به این هدف به کار ببرید.

تابع require بخشی از Fennel نیست؛ این تابع از Lua می‌آید. با این حال، برای بارگذاری کد Fennel به خوبی کار می‌کند. برای اطلاعات بیشتر درباره require، بخش ماژول‌ها و چندین فایل را در آموزش و برنامه‌نویسی در Lua ببینید.

ماکروی include و در نتیجه --require-as-include مشابه import-macros از حل نیمه‌پویای مسیرهای ماژول در زمان کامپایل پشتیبانی می‌کنند. برای اطلاعات بیشتر بخش require نسبی را در آموزش مقدماتی ببینید.

(از نسخه 1.4.0)

گاهی اوقات برای اهداف دیباگ مفید است که یک REPL را دقیقاً در وسط کد خود بیاندازید تا ببینید واقعاً چه خبر است. می‌توانید از ماکروی assert-repl برای این کار استفاده کنید:

(let [input (get-input)
      value []]
  (fn helper [x]
    (table.insert value (calculate x)))
  (assert-repl (transform helper value) "could not transform"))

این دستور به عنوان یک جایگزین مستقیم برای تابع داخلی assert عمل می‌کند، اما زمانی که شرط false یا nil باشد، به جای خطا وارد یک REPL می‌شود که به تمامی متغیرهای محلی موجود در حوزه دسترسی دارد (input، value و helper در مثال بالا).

توجه داشته باشید که این برای استفاده در هنگام توسعه در نظر گرفته شده است و با کامپایل پیش از زمان (AOT) کار نخواهد کرد مگر اینکه بیلد شما Fennel را نیز به عنوان یک کتابخانه در بر داشته باشد.

اگر هنگام اجرای Fennel از فلگ --assert-as-repl استفاده کنید، فراخوانی‌های assert به طور خودکار با assert-repl جایگزین می‌شوند.

توجه: در Fennel 1.4.0، دستور assert-repl یک جدول گزینه‌ها برای fennel.repl را به عنوان سومین آرگومان اختیاری می‌پذیرفت. این مورد به عنوان یک باگ در 1.4.1 حذف شد، زیرا سازگاری با assert را نقض می‌کرد.

محیط REPL ایجاد شده توسط assert-repl همان گزینه‌های پیش‌فرض fennel.repl را اعمال می‌کند که از نسخه 1.4.1 به بعد می‌توان آن را از طریق API پیکربندی کرد. برای جزئیات به مستندات مرجع API زبان Fennel مراجعه نمایید.

شما می‌توانید از طریق REPL دستور ,return EXPRESSION را اجرا کنید تا شرط ناموفق اولیه را با یک مقدار دلخواه دیگر جایگزین نمایید. بازگرداندن false یا nil باعث ایجاد یک شکست معمولی assert خواهد شد.

توجه: در حال حاضر، تنها یک مقدار واحد می‌تواند از این طریق از REPL بازگردانده شود. اگرچه می‌توان از ,return برای بازیابی از یک ارزیابی ناموفق استفاده کرد، اما اگر کد فراخواننده انتظار چندین مقدار بازگشتی را داشته باشد، ممکن است رفتاری غیرمنتظره بروز دهد.

تمام فرم‌هایی که ماکروها را معرفی می‌کنند این کار را در داخل حوزه فعلی انجام می‌دهند. این معمولاً بالاترین سطح برای یک فایل مشخص است، اما می‌توانید ماکروها را در حوزه‌های تودرتو نیز معرفی کنید. توجه داشته باشید که ماکروها ساختاری در زمان کامپایل هستند؛ آن‌ها در زمان اجرا وجود ندارند. بنابراین ماکروها نمی‌توانند مانند توابع و سایر مقادیر در انتهای یک ماژول اکسپورت شوند.

یک ماژول را در زمان کامپایل بارگذاری کرده و توابع آن را به عنوان ماکروهای محلی مقید می‌کند.

یک ماژول ماکرو هر تعداد تابعی را که فرم‌های کد را به عنوان آرگومان در زمان کامپایل دریافت می‌کنند اکسپورت می‌نماید و لیست‌هایی را تولید می‌کند که به عنوان کد به کامپایلر بازگردانده می‌شوند. ماژول‌های ماکرو در نام‌فایل‌هایی با پسوند .fnl یا .fnlm جستجو می‌شوند. ماژولی که import-macros را فراخوانی می‌کند، هر تابعی را که برای استفاده به عنوان ماکرو اکسپورت شده باشد دریافت می‌نماید. برای نمونه، در اینجا یک ماژول ماکرو آورده شده است که when2 را بر اساس if و do پیاده‌سازی می‌کند:

(fn when2 [condition body1 & rest-body]
  (assert body1 "expected body")
  `(if ,condition
     (do ,body1 ,(unpack rest-body))))
{:when2 when2}

برای توضیح کامل درباره نحوه کارکرد این مورد، راهنمای ماکرو را ببینید. تمام فرم‌ها در Fennel جدول‌های معمولی هستند که می‌توانید از table.insert، ipairs، تخریب ساختار و غیره روی آن‌ها استفاده کنید. بک‌تیک (backtick) در خط سوم یک لیست الگویی برای کد تولید شده توسط ماکرو ایجاد می‌کند و کاما به عنوان "unquote" عمل می‌کند که مقادیر را درون الگو جایگذاری می‌نماید.

با فرض اینکه کد بالا در فایل "my-macros.fnl" قرار دارد، این ورودی را دریافت می‌کند:

(import-macros {: when2} :my-macros)
(when2 (= 3 (+ 2 a))
  (print "yes")
  (finish-calculation))

و با درج آرگومان‌ها در الگوی بک‌تیک، آن را در زمان کامپایل به این کد تبدیل می‌کند:

(if (= 3 (+ 2 a))
  (do
    (print "yes")
    (finish-calculation)))

ماکروی import-macros می‌تواند هر تعداد جفت پیوند/نام ماژول را بپذیرد. همچنین می‌تواند به جای تخریب ساختار ماژول، کل ماژول ماکرو را به یک نام واحد مقید کند. در این حالت می‌توانید از یک نقطه برای فراخوانی ماکروهای جداگانه درون ماژول استفاده کنید:

(import-macros mine :my-macros)
(mine.when2 (= 3 (+ 2 a))
  (print "yes")
  (finish-calculation))

توجه داشته باشید که تمام کدهای ماکرو در زمان کامپایل اجرا می‌شوند که قبل از زمان اجرا رخ می‌دهد. متغیرهای محلی که در زمان اجرا در حوزه قرار دارند، در طول زمان کامپایل قابل مشاهده نیستند. بنابراین این کد کار نخواهد کرد:

(local (module-name file-name) ...)
(import-macros mymacros (.. module-name ".macros"))

با این حال، این کد به شرطی که ماژول مورد نظر وجود داشته باشد، کار خواهد کرد:

(import-macros mymacros (.. ... ".macros"))

برای آگاهی از جزئیات توابع اضافی قابل مشاهده در حوزه کامپایلر که ماکروها در آن اجرا می‌شوند، بخش "API کامپایلر" را در ادامه ببینید.

به طور پیش‌فرض، Fennel ماژول‌های ماکرو را شبیه به نحوه جستجوی ماژول‌های معمولی زمان اجرا جستجو می‌کند: با پیمایش ورودی‌ها در fennel.macro-path و بررسی سیستم فایل برای موارد منطبق. با این حال در برخی موارد ممکن است این روش مناسب نباشد، برای نمونه اگر برنامه Fennel شما در نوعی فایل آرشیو بسته‌بندی شده باشد و ماژول‌ها به صورت فایل‌های مجزا روی دیسک وجود نداشته باشند.

برای پشتیبانی از این حالت می‌توانید تابع جستجوگر خود را به جدول fennel.macro-searchers اضافه کنید. به عنوان مثال، با فرض اینکه find-in-archive تابعی است که با داشتن یک مسیر می‌تواند رشته‌ها را از آرشیو جستجو کند:

(local fennel (require :fennel))
(fn my-searcher [module-name]
  (let [filename (.. "src/" module-name ".fnl")]
    (match (find-in-archive filename)
      code (values (partial fennel.eval code {:env :_COMPILER})
                   filename))))
(table.insert fennel.macro-searchers my-searcher)

تابع جستجوگر باید نام ماژول را به عنوان یک رشته دریافت کند و در صورت یافتن ماژول ماکرو، دو مقدار بازگرداند: یک تابع بارگذار که هنگام فراخوانی جدول ماکرو را بازمی‌گرداند، و یک نام فایل اختیاری. تابع بارگذار، نام ماژول و نام فایل را به عنوان آرگومان دریافت خواهد کرد.

یک جدول از ماکروها را تعریف می‌کند. توجه داشته باشید که در داخل تعاریف ماکرو، نمی‌توانید به متغیرها و پیوندهای کدهای اطراف دسترسی داشته باشید. ماکروها اساساً در محیط کامپایلر مخصوص به خود کامپایل می‌شوند. مجدداً برای جزئیات بیشتر در مورد توابع موجود در اینجا، بخش "API کامپایلر" را ببینید.

(macros {:my-max (fn [x y]
                   `(let [x# ,x y# ,y]
                      (if (< x# y#) y# x#)))})
(print (my-max 10 20))
(print (my-max 20 10))
(print (my-max 20 20))

(macro my-max [x y]
  `(let [x# ,x y# ,y]
     (if (< x# y#) y# x#)))

اگر فقط یک ماکروی تکی تعریف می‌کنید، این مورد معادل مثال قبلی است. سینتکس آن از fn تقلید می‌کند.

(macrodebug (-> abc
                (+ 99)
                (< 0)
                (when (os.exit))))
; -> (if (< (+ abc 99) 0) (do (os.exit)))

ماکروی macrodebug را با یک فرم فراخوانی کنید تا به طور مکرر ماکروهای سطح بالا را در آن فرم گسترش دهد و فرم حاصل را چاپ کند. توجه داشته باشید که فرم حاصل معمولاً به طور منظم تورفتگی نخواهد داشت، بنابراین ممکن است لازم باشد آن را کپی کرده و به فرمتی خواناتر بازآرایی کنید.

توجه داشته باشید که این دستور در زمان کامپایل چاپ می‌کند، زیرا macrodebug یک ماکرو است.

ساخت ماکروهایی که تصادفاً آرگومان‌های خود را بیش از یک بار ارزیابی می‌کنند آسان است. اگر مقادیر صریح به آن‌ها ارسال شود مشکلی پیش نمی‌آید، اما اگر فرمی با عوارض جانبی به آن‌ها داده شود، نتیجه غیرمنتظره خواهد بود:

(var v 1)
(macros {:my-max (fn [x y]
                   `(if (< ,x ,y) ,y ,x))})
(fn f [] (set v (+ v 1)) v)
(print (my-max (f) 2)) ; -> 3 since (f) is called twice in the macro body above

به منظور جلوگیری از گرفتاری تصادفی نمادها (symbol capture)، نباید یک نماد ساده را درون بک‌تیک به عنوان یک شناسه مقید کنید. افزودن یک # در انتهای نام شناسه همانند بالا، قابلیت تولید خودکار نماد منحصربه‌فرد (auto gensym) را فراخوانی می‌کند که تضمین می‌نماید نام محلی یکتا است.

(macros {:my-max (fn [x y]
                   `(let [x2 ,x y2 ,y]
                      (if (< x2 y2) y2 x2)))})
(print (my-max 10 20))
; Compile error in 'x2' unknown:?: macro tried to bind x2 without gensym; try x2# instead

دستور macros برای ماکروهای سریع و تک‌کاربردی، یا حتی برخی ماکروهای پیچیده‌تر مفید است، اما احتیاط کنید. استفاده از تابعی که قبلاً تعریف کرده‌اید ممکن است وسوسه‌انگیز باشد، اما اگر به چنین قابلیتی نیاز دارید، احتمالاً باید از import-macros استفاده کنید.

به عنوان مثال، این کد در حالت سخت‌گیرانه (strict mode) کامپایل نخواهد شد! حتی زمانی که اجازه فراخوانی ماکرو را بدهد، در هنگام اجرای کد در تلاش برای فراخوانی یک تابع سراسری my-fn با شکست مواجه خواهد شد:

(fn my-fn [] (print "hi!"))
(macros {:my-max (fn [x y]
                   (my-fn)
                   `(let [x# ,x y# ,y]
                      (if (< x# y#) y# x#)))})
; Compile error in 'my-max': attempt to call global '__fnl_global__my_2dfn' (a nil value)

برای جزئیات بیشتر در مورد نوشتن ماکروها به راهنمای ماکرو مراجعه فرمایید..SS eval-compiler ارزیابی یک بلوک کد در طول زمان کامپایل با دسترسی به حوزه کامپایلر. این قابلیت مجموعه‌ای گسترده‌تر از ویژگی‌هایی را که می‌توانید با ماکروها به دست آورید در اختیارتان می‌گذارد، اما اگر می‌توانید باید از ماکروها استفاده کنید.

مثال:

(eval-compiler
  (each [name (pairs _G)]
    (print name)))

این دستور تمام توابع موجود در حوزه کامپایلر را چاپ می‌کند.

در داخل بلوک‌های eval-compiler، macros یا macro و همچنین ماژول‌های import-macros، توابع فهرست‌شده در زیر برای کد شما قابل مشاهده هستند:

•
list - یک لیست بازمی‌گرداند که نوع خاصی از جدول مورد استفاده برای کد است.
•
sym - یک رشته را به یک نماد تبدیل می‌کند.
•
gensym - یک نماد منحصربه‌فرد برای استفاده در ماکروها تولید می‌کند؛ یک رشته پیشوند اختیاری را می‌پذیرد.
•
list? - آیا آرگومان یک لیست است؟ خود آرگومان یا false را بازمی‌گرداند.
•
sym? - آیا آرگومان یک نماد است؟ خود آرگومان یا false را بازمی‌گرداند.
•
table? - آیا آرگومان یک جدول غیر لیستی است؟ خود آرگومان یا false را بازمی‌گرداند.
•
sequence? - آیا آرگومان یک جدول ترتیبی غیر لیستی (ایجاد شده با [] در مقابل {}) است؟ خود آرگومان یا false را بازمی‌گرداند.
•
varg? - آیا این یک نماد ... است که نشان‌دهنده تعداد متغیر آرگومان‌ها است؟ یک جدول خاص توصیف‌کننده نوع یا false را بازمی‌گرداند.
•
multi-sym? - چندنماد یک نماد دارای نقطه است که به فیلد یک جدول اشاره دارد. جدولی حاوی هر نماد مجزا یا false را بازمی‌گرداند.
•
comment? - آیا آرگومان یک کامنت است؟ کامنت‌ها تنها زمانی گنجانده می‌شوند که opts.comments درست باشد.
•
view - سریال‌ساز جدول fennel.view.
•
assert-compile - مانند assert کار می‌کند اما یک لیست/نماد را به عنوان سومین آرگومان خود دریافت می‌کند تا پیام‌های خطای دقیق ارائه دهد.

توابع زیر متغیرهای سراسری Lua را که بین نسخه‌های 5.1 تا 5.4 تغییر می‌کنند استانداردسازی می‌نمایند. برای محدود کردن کدهای تکراری رایج سازگاری با Lua مانند (local unpack (or _G.unpack table.unpack)) از کد ماکرو، توابع کمکی زیر در محیط ماکرو وجود دارند:

•
unpack - معادل _G.unpack در Lua 5.1/LuaJit، و table.unpack در Lua >= 5.2
•
pack - معادل table.pack موجود در Lua 5.2 به بالا. (pack :a nil :c nil nil) -> {1 :a 3 :c :n 5}. برای ذخیره مطمئن و بازتولید صحیح مقادیر چندگانه حاوی nil بسیار مفید است.

این توابع فقط از درون ماکروها قابل استفاده هستند و از هیچ فراخوانی eval-compiler در دسترس نیستند:

•
in-scope? - آیا این نماد به یک متغیر محلی در حوزه اشاره دارد؟ نماد یا nil را بازمی‌گرداند.
•
macroexpand - گسترش ماکرو را روی فرم آرگومان خود انجام می‌دهد؛ یک درخت نحو انتزاعی (AST) بازمی‌گرداند.

توجه داشته باشید که لیست‌ها مفاهیم زمان کامپایل هستند که در زمان اجرا وجود ندارند؛ آن‌ها به عنوان جدول‌هایی پیاده‌سازی می‌شوند که متاتیبل خاصی دارند تا آن‌ها را از جدول‌های معمولی تعریف‌شده با براکت یا آکولاد متمایز کند. به طور مشابه، نمادها جدول‌هایی با یک ورودی رشته‌ای برای نام خود و یک متاتیبل نشانگر هستند. می‌توانید از tostring برای به دست آوردن نام یک نماد استفاده کنید.

در داخل ماکروها یا eval-compiler، به طور پیش‌فرض تنها دو روش برای تعامل کد با "دنیای بیرون" وجود دارد: می‌توانید print را برای دیباگ فراخوانی کنید، و می‌توانید io.open را در حالت خواندن روی فایل‌های داخل دایرکتوری جاری یا زیرشاخه‌های آن فراخوانی نمایید. بقیه جدول io و کل جدول os غیرقابل دسترسی هستند.

می‌توانید این محدودیت‌ها را با ارسال {:compiler-env _G} در جدول گزینه‌ها هنگام استفاده از API کامپایلر، یا تنظیم فلگ --no-compiler-sandbox در خط فرمان برای دسترسی کامل کاهش دهید.

لطفاً توجه داشته باشید که این محیط محدودشده برای استفاده به عنوان یک مکانیزم امنیتی قوی مناسب نیست. ممیزی امنیتی نشده است و نباید برای محافظت در برابر اجرای کدهای غیرقابل اعتماد به آن اتکا شود.

توجه داشته باشید که سایر بخش‌های داخلی کامپایلر که در حوزه کامپایلر ارائه شده‌اند اما در بالا ذکر نشده‌اند ممکن است در آینده تغییر کنند.

در برخی موارد ممکن است لازم باشد خروجی Lua را از Fennel به روش‌هایی تولید کنید که با معناشناسی Fennel مطابقت ندارند. برای مثال، اگر در حال انتقال الگوریتمی از Lua هستید که از بازگشت‌های زودهنگام (early returns) استفاده می‌کند، ممکن است بخواهید ابتدا پورت را تا حد امکان به معنای واقعی کلمه انجام دهید و سپس بعداً برگردید تا آن را به سبک اصیل Fennel تبدیل کنید. می‌توانید از فرم ویژه lua برای انجام این کار استفاده نمایید:

(fn find [tbl pred]
  (each [key val (pairs tbl)]
    (when (pred val)
      (lua "return key"))))

کد Lua در داخل رشته می‌تواند به متغیرهای محلی که در حوزه هستند اشاره کند؛ با این حال توجه داشته باشید که باید به نام‌ها پس از انجام تغییر نام (mangling) اشاره کند، زیرا شناسه‌ها باید Lua معتبر باشند. کامپایلر Fennel نام foo-bar را به foo_bar در خروجی Lua تغییر می‌دهد تا معتبر باشد، و همین‌طور تبدیل‌های دیگر. در صورت تردید، خروجی کامپایلر را بررسی کنید تا ببینید چگونه به نظر می‌رسد. به عنوان مثال کد Fennel زیر:

(local foo-bar 3)
(let [foo-bar :hello]
  (lua "print(foo_bar0 .. \" world\")"))

این کد Lua را تولید خواهد کرد:

local foo_bar = 3
local foo_bar0 = "hello"
print(foo_bar0 .. " world")
return nil

به طور معمول در این موارد شما مایلید یک دستور (statement) تولید کنید، که در این حالت یک رشته از کد Lua را به عنوان اولین آرگومان ارسال می‌کنید. اما اگر رشته‌ای را به عنوان آرگومان دوم ارسال کنید، می‌توانید از آن برای انتشار یک عبارت (expression) نیز استفاده نمایید.

توجه داشته باشید که این قابلیت فقط باید در شرایط استثنایی یا موقت استفاده شود، و اگر می‌توانید از آن اجتناب کنید، حتماً این کار را انجام دهید.

فرم # یک نام مستعار منسوخ‌شده برای length است و ~= یک نام مستعار منسوخ‌شده برای not= است که برای سازگاری با گذشته نگه داشته شده‌اند.

(منسوخ‌شده از نسخه 0.4.0)

فرم require-macros مشابه import-macros است، به جز اینکه تمام ماکروها را بدون مشخص کردن اینکه چه شناسه‌های جدیدی به حوزه وارد می‌شوند، ایمپورت می‌کند. اکیداً توصیه می‌شود در عوض از import-macros استفاده کنید.

(منسوخ‌شده از نسخه 0.10.0)

مشابه با pick-values، اما یک عدد صحیح n و یک تابع/عملگر f را می‌گیرد و تابع جدیدی ایجاد می‌کند که دقیقاً n آرگومان را روی f اعمال می‌نماید.

(منسوخ‌شده از نسخه 1.1.0)

یک متغیر سراسری را روی مقدار جدیدی تنظیم می‌کند. توجه داشته باشید که هیچ تمایزی بین معرفی یک متغیر سراسری جدید و تغییر مقدار یک متغیر موجود وجود ندارد. این فرم از تخریب ساختار پشتیبانی می‌کند.

مثال:

(global prettyprint (fn [x] (print (fennel.view x))))

استفاده از global شناسه مورد نظر را به لیست متغیرهای سراسری مجاز اضافه می‌کند به طوری که ارجاع به آن در ادامه باعث خطای کامپایلر نخواهد شد. با این حال متغیرهای سراسری در جدول _G نیز در دسترس هستند و دسترسی به آن‌ها از آن طریق برای شفافیت بیشتر توصیه می‌شود.

(منسوخ‌شده از نسخه 1.4.1، در نسخه‌های آینده حذف خواهد شد)

اگر یک جدول متامتود __fennelrest را پیاده‌سازی کند، برای ضبط باقیمانده جدول استفاده می‌شود. می‌توان از آن با ساختارهای داده سفارشی پیاده‌سازی‌شده بر پایه جدول‌ها که مایل به ارائه تخریب ساختار سفارشی برای باقیمانده هستند استفاده کرد. متامتود جدول را به عنوان اولین آرگومان و مقدار اعدادی را که باید از ابتدای جدول حذف شوند، بسیار شبیه به table.unpack دریافت می‌کند.

مثال:

(local t [1 2 3 4 5 6])
(setmetatable
 t
 {:__fennelrest (fn [t k]
                  (let [res {}]
                    (for [i k (length t)]
                      (tset res (tostring (. t i)) (. t i)))
                  res))})
(let [[a b & c] t]
  c) ;; => {:3 3 :4 4 :5 5 :6 6}

نگه‌دارندگان زبان فنل (Fennel Maintainers).

2025-12-30 fennel 1.6.1