مقدمه
سلام به همه! امیدوارم حالتان خوب باشد.
چند هفتهی واقعاً هیجانانگیز در Exercism داشتیم؛ با رونمایی از Exercism Premium و Exercism Insiders. چند جلسهی آنلاین خوب هم با انجمن داشتیم و کلی بهبود در سایت منتشر شد و کلی هم بهزودی منتشر میشود. این روزها چیزهای زیادی برای هیجانزده شدن وجود دارد، اما هیچچیز بهاندازهی ورودمان به ماه ۶ کمپین #12in23 هیجانانگیز نیست! تابستان S-expressionها، که با نام کوتاهشده و زیباتر «Summer of Sexps» شناخته میشود.
مثل همیشه، اریک، خردمندِ فرزانهی دنیای برنامهنویسی، همراه من است.
خب، این ماه پنج زبان داریم: Clojure، Common Lisp، Emacs Lisp، Racket و Scheme. هر یک از این زبانها گویشی از زبان Lisp است، بنابراین بهجای تمرکز بیشازحد روی تفاوت این زبانها در این ویدیو، کمی بیشتر به خود Lisp و چیزی که آن را یگانه میکند میپردازیم. بعد هم با مروری کوتاه بر زبانها کار را تمام میکنیم.
اما پیش از آن، چند نکتهی هماهنگی! برای گرفتن نشان Summer of Sexps باید در ماه ژوئن هر پنج تمرین را در یکی از این زبانها کامل کنید.
نشانها
نشان یکسالهی 12in23 هم وجود دارد. برای گرفتن آن، باید پنج تمرین از تمرینهای ویژهی ما را در آن زبان حل کنید. اگر این را بعد از ژوئن تماشا میکنید، میتوانید این بخش را در هر زمانی از سال انجام دهید، پس چیزی را از دست ندادهاید. چون بسیاری از افراد هرگز با Lisp کار نکردهاند، سعی کردیم تمرینهای نسبتاً سادهای انتخاب کنیم که به شما نشان دهد یک زبان Lisp چه شکلی است.
- پرش: کار با شرطهای منطقی و مفهوم درستبودن (و بهصورت اختیاری، حوزهبندی واژگانی)
- دو برای یکی: قالببندی یک رشته و کار با یک پارامتر اختیاری
- تفاضل مربعات: فراخوانی توابع تعریفشده توسط کاربر و انجام محاسبات در نماد پیشوندی
- اسم ربات: کار با تصادفیبودن، اتمها و دادههای ساختیافته
- تطبیق براکتها: استفاده از بازگشت برای اعتبارسنجی یک رشته
این تمرینها و تمرینهای ماههای قبل را میتوانید در صفحهی #12in23 پیدا کنید.
نگاه کلی
خب، زبانهای مبتنی بر Lisp. فکر میکنم بهتر است اول کمی Lisp را بفهمیم. بیایید با مقدمهای کوتاه بر Lisp بهطور کلی شروع کنیم.
Lisp
-
اولین نکته این است که Lisp یکی از قدیمیترین زبانهاست.
-
جان مککارتی آن را در سال ۱۹۵۸ در MIT ساخت، در زمانی که رایانهها هنوز یک اتاق تمام را از بالا تا پایین پر میکردند 🙂
-
نام Lisp مخفف LISt Processing (یا LISt Processor) است و اهمیت ساختمان دادهی لیست را نشان میدهد.
-
با هدف انجام پژوهش در AI طراحی شد.
-
Lisp بر حساب لامبدا بنا شده بود که آلونزو چرچ آن را ابداع کرد؛ حساب لامبدا یک سیستم صوری برای توصیف محاسبه در ریاضیات است (به زبان ساده).
-
به دلایل گوناگون، زبانی فوقالعاده تأثیرگذار است:
-
دومین زبان برنامهنویسی سطح بالای قدیمی است که هنوز بهطور گسترده به کار میرود (پس از Fortran)
-
نخستین زبان برنامهنویسی تابعی سطح بالا بود و بسیاری از ویژگیهایی را معرفی کرد که امروز با برنامهنویسی تابعی میشناسیم.
-
توجه کنید که Lisp هنوز از برنامهنویسی امری پشتیبانی میکرد
-
نخستین زبانی بود که جمعکنندهی زباله داشت و برنامهنویس را از مدیریت دستی حافظه آزاد میکرد
-
نحوهی نگارش نسبتاً کم و معناشناسی نسبتاً سادهاش آن را برای آموزش عالی میکند.
-
به همین دلیل Lisp (یا دقیقتر: یکی از گویشهایش) اغلب برای آموزش برنامهنویسی به کار میرود
-
گویشهای زبانی بسیار زیادی پدید آورده (و همچنان پدید میآورد) که دربارهی آنهایی که در Exercism پشتیبانی میشوند صحبت خواهیم کرد.
-
به بیان دیگر، در درخت زبانهای برنامهنویسی شاخهای جداگانه برای زبانهای Lispمانند وجود دارد (همانطور که شاخهای برای زبانهای Cمانند هست).
چندی پیش با سایمون پیتون جونز، یکی از سازندگان Haskell، صحبت کردم؛ او دربارهی تفاوت میان زبانهایی که حول ماشینهای تورینگ ساخته شدهاند و زبانهایی که حول حساب لامبدا بنا شدهاند حرف میزد. اگر میخواهید بیشتر بدانید، ارزش دارد آن مصاحبه را ببینید.
پرانتزها
در زبانهای Lisp پرانتزهای زیادی وجود دارد، اما این لزوماً چیز بدی نیست (همانطور که داشتن آکولادهای زیاد در زبانهای Cمانند لزوماً بد نیست). زبانهای Lisp حول مفهومی به نام S-expression بنا شدهاند. یک S-expression (کوتاهشدهی symbolic expression که به sexpr یا sexp مخفف میشود و نام چالش این ماه هم از همین میآید) عبارتی برای نمایش داده است. این مفهوم برای زبان Lisp اصلی ابداع شد و همان زبان آن را رواج داد. یک S-expression میتواند یکی از دو شکل زیر باشد:
- یک اتم (مثلاً
x). آنها را «مقدار»های غیرتودرتو یا برگهای درخت در نظر بگیرید - یک عبارت
x . yکه در آنxوyهر دو S-expression هستند. آنها را جفتهایی در نظر بگیرید کهyمیتواند عنصر بعدی در لیست باشد (اگر وجود داشته باشد)، یا گرههایی در یک درخت. توجه کنید که این یک تعریف بازگشتی است که در سطح برگ پایان مییابد. معمولاً برای این نوع S-expression از پرانتز استفاده میشود.
S-expressionها
S-expressionها در Lisp هم برای نمایش داده و هم برای نمایش لیست به کار میروند. بنابراین هر بار که لیستی تعریف میکنید، از پرانتز استفاده خواهید کرد. این را با این واقعیتها کنار هم بگذارید که: لیست ساختمان دادهی اصلی Lisp است (نامش هم از همین میآید)، در برخی از زبانهای Lisp تنها ساختمان داده است، نتیجه پرانتزهای زیادی است. برای اینکه ببینید لیستها چقدر بنیادیاند، بدانید که اگر بخواهید در Lisp تابعی را فراخوانی کنید، این کار را با ساختن یک لیست انجام میدهید.
نکتهی جالب اینکه عنصر اول لیست (که سر نامیده میشود) تابع در حال فراخوانی را نشان میدهد و بقیهی عناصر (که دُم نامیده میشوند) به عنوان آرگومانها پاس داده میشوند. این را نماد پیشوندی مینامند (که در آن عملگر پیش از عملوندها میآید)؛ در ابتدا کمی عجیب به نظر میرسد اما در واقع بسیار کاربردی است:
- میتوانید یک عملگر را روی چند آرگومان اعمال کنید بدون اینکه لازم باشد عملگر را تکرار کنید (مثلاً
(+ 1 2 3)) - تقدم عملگرها صریح میشود، چون در هر حال برای فراخوانی عملگری متفاوت باید یک S-expression جدید تعریف کنید
جالب است که حتی کد منبع هم با لیستها نمایش داده میشود، اما بعداً به این موضوع برمیگردیم.
بهطور کلی، بیشتر زبانهای Lisp نحوهی نگارش بسیار کم و معناشناسی نسبتاً سادهای دارند که یادگیری آنها را نسبتاً آسان میکند و فهم کد هم آسانتر میشود. این نحوهی نگارش کم، آنها را کمقدرتتر نمیکند! ترکیب این دو ویژگی (نحوهی نگارش کم + معناشناسی ساده) زبانهای Lisp را برای نوشتن کامپایلر و مفسر ایدهآل میکند. اگر روزی خواستید کامپایلر خودتان را بسازید، ساختن یک Lisp گزینهی خوبی است!
ویژگیهای جالب Lisp
همانطور که پیشتر اشاره شد، زبانهای Lisp برای نمایش کد از همان نوع دادهها و ساختمان دادهها استفاده میکنند. این ویژگی «هوموییکونیک» (یا هوموییکونیسیتی) نامیده میشود. به بیان دیگر، زبانی هوموییکونیک است اگر بتوان برنامهای که با آن نوشته شده را با خودِ همان زبان بهصورت داده دستکاری کرد؛ در این صورت تنها با خواندن خود برنامه میتوان نمایش درونی آن را استنتاج کرد. این ویژگی را اغلب اینطور خلاصه میکنند که زبان با کد مانند داده رفتار میکند.
زبانها
Scheme
- در دههی ۱۹۷۰ توسط گای استیل و جرالد ساسمن در آزمایشگاه AI دانشگاه MIT ساخته شد.
- در آغاز تلاشی بود برای فهم مدل Actor کارل هیویت از طریق یک مفسر Lisp کوچک.
- خود زبان در مجموعهای از یادداشتهای پژوهشی AI معرفی شد که امروز بهطور جمعی «مقالات لامبدا» نامیده میشوند.
- نخستین گویش Lisp که از حوزهبندی واژگانی استفاده کرد (مقدارها فقط در جایی که تعریف شدهاند در حوزهاند) و یکی از نخستین زبانهایی که از ادامههای درجهیک پشتیبانی کرد.
- استاندارد رسمی IEEE و یک استاندارد عملی به نام Revised Report on the Algorithmic Language Scheme (RnRS).
- پیادهسازیهای زیادی دارد: ChezScheme، Guile (هر دو در Exercism پشتیبانی میشوند)، MIT/GNU Scheme و Racket
- زبانی بسیار کمینه، با نحوهی نگارش اندک، اما این موضوع عمدی نبود.
- نویسندگان تلاش کردند چیزی پیچیده بسازند اما در نهایت چیزی بسیار سادهتر از آنچه در نظر داشتند طراحی کردند
- بازگشت دُمی درست. روش اصطلاحی برای تکرار، استفاده از بازگشت است.
- Scheme فراخوانیهای بازگشتی دُمی را بهینه میکند تا فضای پشته یا منابع دیگر را مصرف نکنند. این یعنی بازگشت را میتوان روی دادههای بسیار بزرگ یا برای محاسبات بسیار طولانی به کار برد
- نوع دادههای عددی قدرتمند، از جمله اعداد گویا و مختلط
- ارزیابی تعویقی، که شبیه
promiseها است. - سیستم ماکرویی قدرتمند.
- ماکروهای بهداشتی احتمال نتایج غیرمنتظره را هنگام تعریف ماکرو کاهش میدهند.
Common Lisp
- کار روی Common Lisp در سال ۱۹۸۱ پس از ابتکار باب انگلمور، مدیر ARPA، برای توسعهی یک گویش استاندارد همگانی Lisp آغاز شد، چون گویشهای مختلفی که به کار میرفتند اغلب ناسازگار بودند و این یعنی کد و دانش قابل اشتراکگذاری نبود
- نخستین استاندارد در سال ۱۹۸۴ و استاندارد نهایی در سال ۱۹۹۴ منتشر شد (مشخصات بسیار پایداری دارد)
- چون یک استاندارد است، پیادهسازیهای مختلفی از آن وجود دارد، مانند Steel Bank Common Lisp (که پیشفرض Exercism است) و CLisp.
- پیادهسازیهای تجاری هم هستند، مانند Allegro CL و LispWorks، و همچنین ECL (Embeddable Common Lisp) که میتوان آن را در برنامههای C جای داد و ABCL که روی Java Virtual Machine اجرا میشود.
- توسط یک استاندارد تعریف شده (ANSI INCITS 226-1994)، پس کدی که ۳۰ سال پیش نوشته شده امروز هم بدون مشکل اجرا میشود
- سیستم نوعِ غنی و قابل گسترش
- برای توسعهی مبتنی بر
imageو REPL طراحی شده، بنابراین قابلیت دروننگری بالایی دارد.
Emacs Lisp
- در سال ۱۹۸۵ با هدف داشتن زبانی کارآمد برای توسعهی یک ویرایشگر متن ساخته شد
- نوعبندی پویا دارد
- حدود ۸۰٪ از Emacs با Emacs Lisp نوشته شده است (۲۰٪ با C به دلایل کارایی)
- کمی با دیگر زبانهای Lisp تفاوت دارد:
- استاندارد نشده و هنوز بهکندی در حال تحول است
- حذف خودکار فراخوانی دُمی ندارد؛ پشتیبانی از طریق ماکروی
named-letانجام میشود (که بهwhile-loopتبدیل میشود) - بهطور پیشفرض حوزهبندی پویا، و برای کد جدید حوزهبندی واژگانی توصیه میشود
- مستندات خوب درون ویرایشگر
- چندسکویی (هرجا Emacs اجرا شود اجرا میشود)
- زبان را با خواندن کد قابلیتهایی که روزانه استفاده میکنید یاد بگیرید (Emacs Core + Packages)
- زیرمجموعهای از Common Lisp از طریق بستهی
cl-libدر دسترس است. Emacs Lisp نسبتاً کمینه است، در حالی که Common Lisp بسیار پرامکاناتتر است. بستهیcl-libزیرمجموعهای از CL را در دسترس قرار میدهد
Racket
- ماتیاس فلایزن شرکت PLT Inc. را بنیان گذاشت که در ژانویهی ۱۹۹۵ تصمیم گرفت یک محیط برنامهنویسی آموزشی مبتنی بر Scheme بسازد. نام آن در ابتدا PLT Scheme بود و بعدها به Racket تغییر کرد.
- علاوه بر اینکه یک محیط برنامهنویسی آموزشی است، به عنوان بستری برای طراحی و پیادهسازی زبانهای برنامهنویسی هم طراحی شد.
- Lisp مدرن و از نسل Scheme
- از برنامهنویسی منطقی پشتیبانی میکند!
- نحوهی نگارش ساده و رسا که هم برای مبتدیان ایدهآل است و هم در دست متخصصان قدرتمند
- از پارادایمهای برنامهنویسی زیادی پشتیبانی میکند: برنامهنویسی تابعی، برنامهنویسی شیءگرا، طراحی با قرارداد، برنامهنویسی منطقی، فرابرنامهنویسی
- یک کتابخانهی استاندارد جامع
- همراه با DrRacket عرضه میشود، یک IDE کامل که برای یادگیری و کاوش با کمترین دردسر طراحی شده است
- مستندات عالی همراه با اطلاعات زمینهای و مثالهای فراوان
Clojure
- ریچ هیکی آن را با هدف داشتن یک Lisp مدرن که روی JVM اجرا شود و همروندی عالی داشته باشد توسعه داد
- گویشی از Lisp است، اما به این معنا با دیگر زبانهای Lisp تفاوت دارد که بازگشت دُمی ضمنی را پشتیبانی نمیکند (اگر نمیدانید چیست نگران نباشید) و ساختمان دادههای بیشتری از فقط لیست دارد: map/set/vector. هر یک از این ساختمان دادهها نحوهی نگارش تحتاللفظی خودش را دارد.
- همهی آنها تغییرناپذیرند، اما با جستوجوی
O(log32 n)که «عملاً» زمان ثابت است، کارایی عالی دارند - چندریختی در زمان اجرا از طریق multimethodها و protocolها
- همکاری عالی با JVM
- سیستم مشخصات دادهی Clojure Spec (در زمان اجرا، نه زمان کامپایل) به شما امکان میدهد ساختار داده را تعریف کنید، داده تولید کنید، آزمون مبتنی بر ویژگی انجام دهید و بیشتر
موارد کاربرد
Scheme
- در آموزش برای کمک به تدریس علوم کامپیوتر به کار میرود (کتاب تأثیرگذار Structure and Interpretation of Computer Programs از Scheme استفاده میکند).
- در AI استفاده میشود. به عنوان زبان اسکریپتنویسی هم به کار میرود، مثلاً در GIMP (ویرایشگر گرافیکی)، ابزارهای CAD (طراحی به کمک کامپیوتر) و حتی فیلمها، در اسکریپتهای مدیریت موتور رندر Final Fantasy: The Spirit Within
Common Lisp
- Common Lisp در جاهای زیادی به کار میرود، مثلاً در هوش مصنوعی و پژوهش، و همچنین در کاربردهای تجاری: NASA نرمافزار خلبان خودکار سفینهی Deep Space One را با Common Lisp نوشت، Viaweb با Common Lisp نوشته شد که بعدها Yahoo آن را خرید و به Yahoo Store! تغییر نام داد، و نسخهی اول Reddit هم با آن نوشته شد
Emacs Lisp
- Emacs Lisp در خب، خود Emacs به کار میرود!
- در هستهی خود، Emacs یک مفسر برای Emacs Lisp است، گویشی از زبان برنامهنویسی Lisp که افزونههایی برای پشتیبانی از ویرایش متن به آن اضافه شده است
Racket
- در آموزش به کار میرود، چون Racket با تأکید بر پشتیبانی از ساخت، سادهسازی و تحلیل زبان طراحی شده است.
- در پژوهش به کار میرود، چون نحوهی نگارش و معناشناسی قابل گسترشش آن را برای طراحی و نمونهسازی زبانها و ویژگیهای زبانی جدید مناسب میکند.
- در بازیها به کار میرود، مثلاً جان کارمک (شناختهشده با Doom) از آن در محیط اسکریپتنویسی تعاملی برای VR استفاده کرد و توسعهدهنده Naughty Dog از آن برای اسکریپتنویسی بهره برد (مثلاً در Uncharted). Hacker News با Arc نوشته شده که آن هم یک Lisp است و خودش با Racket نوشته شده است.
Clojure
- Clojure برای کارهای گوناگونی به کار میرود، از جمله خرید شرکت Atomist توسط Docker در سال ۲۰۲۲؛ Atomist یک پلتفرم امنیت و اتوماسیون کانتینر است که با Clojure پیادهسازی شده.
- بزرگترین کاربر Clojure در جهان Nubank است، یک بانک جدید که چند سال پیش آن را خرید و اکنون تیم اصلی Clojure را استخدام کرده است.
- به دلیل پویا و بسیار تعاملی بودن، بهطور گسترده برای نمونهسازی سریع به کار میرود.
از دیدگاه برنامهنویسی
همهی این زبانها از پارادایمهای تابعی، امری و نمادین پشتیبانی میکنند. برخی از آنها از OOP هم پشتیبانی میکنند، بهویژه Common Lisp.
زبانهای Lisp بیشتر پویا هستند، هرچند Racket از نوعبندی ایستا هم پشتیبانی میکند.
این به معنای مفسری بودن همهی آنها نیست، چون ترکیبی از گزینهها وجود دارد: تفسیرشده (بدون هیچ مرحلهی کامپایل)، کامپایل به بایتکد و سپس تفسیر، و کامپایل مستقیم به کد ماشین.
Scheme
- کمینهگرا، با معناشناسی روشن و ساده و راههای اندکی برای ساختن عبارت.
- یادگیری زبان و فهم کد را آسان میکند.
- به همین دلیل، Scheme در بسیاری از دورههای مقدماتی علوم کامپیوتر هم زیاد به کار میرود
- ادامههای درجهیک.
- یک ادامه، نمایشی از وضعیت یک برنامه است.
- ادامهها را میتوان برای مدلسازی جریان کنترل (مثلاً ساختار
return) یا کوروتینها (که امکان چندکارگی را فراهم میکنند) به کار برد
Common Lisp
- سیستم شیءگرای قابل گسترش با ترکیب متدهای قابل برنامهریزی (هم در نحوهی ترکیب متدهای زیرکلاس/ابرکلاس، و هم متدهای before، after و around که امکان گسترش سیستمها را بدون تغییر دادن خودشان فراهم میکنند)
- سیستم شرط قابل برنامهریزی (ابر مجموعهای از «استثناها») که تشخیص شرطها را از انتخاب نحوهی رسیدگی به آنها جدا میکند. سیستم شرط از سیستمهای استثنا انعطافپذیرتر است، چون به جای تقسیم دوبخشی میان کدی که خطا۱ را علامت میدهد و کدی که آن را رسیدگی میکند،۲ مسئولیتها را به سه بخش تقسیم میکند: علامت دادن شرط، رسیدگی به آن، و راهاندازی مجدد.
- ماکروها اجازه میدهند نحوهی نگارش زبان گسترش یابد، نه فقط کدهای تکراری تولید شود. این کمک میکند زبانی متناسب با دامنه بسازید، نه برعکس.
Emacs Lisp
- پشتیبانی و یکپارچگی عالی با ویرایشگر
- میتوان از آن برای سفارشیسازی Emacs در حالی که در حال اجراست استفاده کرد («مثل انجام جراحی مغز روی خودتان» :))
- میتوان از آن در حالت دستهای استفاده کرد، حالتی که در آن همهی تواناییهای ویرایشگر برای پردازش متن (مانند bufferها و دستورهای جابهجایی) در اختیار شماست
Racket
- سیستم ماکرویی قدرتمند. شکر نحویای مانند ماکروهای threading روی آن ساخته شده است. ماکروها بهداشتی هم هستند، که پاسخ به یک پرسش ساده است: یک ماکرو کدی تولید میکند که جای دیگری قرار میگیرد. وقتی آن کد ارزیابی میشود، چگونه باید اتصالهای شناسههای درونش را تعیین کنیم؟ ماکروهای بهداشتی احتمال نتایج غیرمنتظره را هنگام تعریف ماکرو کاهش میدهند.
- زبانمحور.
- Racket ابزارهایی برای نوشتن زبان برنامهنویسی یا DSL خودتان دارد که بر ماکروهای Racket بنا شدهاند.
- چندین زبان توکار دارد، مانند typed Racket (با پشتیبانی از حاشیهنویسیهای نوعی که بهصورت ایستا بررسی میشوند)، datalog (زبانی شبیه Prolog) که IDE یعنی DrRacket از آن پشتیبانی میکند، و scribble، ابزاری برای ساختن اسناد متنی به شکل HTML یا PDF
- REPL بخش اصلی جریان کار توسعه است، نه فقط برای آزمایش کردن یا جستوجو در مستندات
Clojure
- سیستم ماکرویی قدرتمند.
- شکر نحویای مانند ماکروهای threading روی آن ساخته شده است
- REPL بخش اصلی جریان کار توسعه است، نه فقط برای آزمایش کردن یا جستوجو در مستندات
کدام را امتحان کنید
- اگر هرگز Lisp را امتحان نکردهاید، Scheme و Racket گزینههای عالیای هستند، چون هر دو نحوهی نگارش بسیار کمی دارند.
- با این حال، Common Lisp و Clojure هر دو حالت یادگیری دارند، پس احتمالاً بهترین گزینهها برای یادگیری در Exercism هستند.
- اگر همین حالا از Emacs استفاده میکنید، Emacs Lisp انتخاب طبیعی است.
- به همین ترتیب، اگر از زبانی روی JVM استفاده میکنید، Clojure گزینهی طبیعی است.
- Emacs Lisp (از طریق Emacs)، Clojure (از طریق IntelliJ) و Racket (از طریق DrRacket) همگی پشتیبانی عالی IDE دارند.
- البته برای Common Lisp و Scheme هم IDEهای خوبی وجود دارد.
- اگر یک Lisp واقعاً پرامکانات میخواهید، Common Lisp، Clojure و Racket بسیار گستردهاند
- اگر کمی Lisp متفاوت میخواهید، Clojure برای یک Lisp نحوهی نگارش نسبتاً یگانهای دارد.
- اگر به ماکروها و فرابرنامهنویسی علاقه دارید، اساساً همهی آنها گزینههای خوبی هستند! اما اگر میخواهید زبانهای جدید بسازید، Racket بهطور خاص عالی است
البته اگر وقت داشته باشید، پیشنهاد میکنم چند مورد را امتحان کنید! همچنین از پرانتزها نترسید! میدانم که من هم میترسیدم و به همین دلیل یادگیری Lisp را مدتها به تعویق انداختم. اما بهزودی به آنها عادت میکنید و شاید حتی مثل من قدرشان را بدانید. در واقع، من الان عاشق زبانهای Lisp هستم؛ با نحوهی نگارش کم، معناشناسی آسان و در عین حال بسیار رسا.