نبود قابلیت‌های مختلف در گولنگ: جنریک

فهرست مطالب

مطالب پیش نیاز

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

مقدمه

بعضی اوقات ما به شکل خواسته یا ناخواسته، برای خودمان یک سری «چارچوب فکری» طراحی می کنیم و بی‌دلیل روی این چارچوب‌ها اصرار می‌ورزیم. این طور فکر می‌کنیم که «راه درست» فقط همانی است که در چارچوب فکری ما وجود دارد. اما واقعا نمی دانیم این ادعا را بر چه مبنایی مطرح می‌کنیم! چرا این راه، تنها راه درست است؟ چه کسی این را گفته؟ این موضوع از کجا ثابت شده؟

در آخر متوجه می‌شویم تنها چیزی که ذهن ما را به روی آلترناتیو‌های دیگر بسته، فقط خودمان هستیم و آن چارچوب‌های کذایی!

موضوع ساده است:

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

زبان‌های برنامه‌نویسی و ابزارهای پیرامون آن هیچ کدام بدون نقص و کاستی نیستند؛ این «ما» هستیم که باید با خود بیاندیشیم آیا می‌توانیم با این کاستی‌ها کنار بیاییم یا خیر. به عبارتی، زبان برنامه‌نویسی محبوب یک فرد، در واقع زبانی است که آن فرد توانسته با نقص‌هایش کنار بیاید!

در این میان باید توجه کنیم که بسته به تفکرات مختلف، یک «نقص» ممکن است به عنوان یک «مزیت» مطرح شود، و یک مزیت به عنوان یک نقص!

گولنگ جنریک ندارد؟

  • «چجوری با گولنگ که جنریک نداره برنامه نویسی میکنید؟ میشه مگه؟»
  • «تا زمانی که گولنگ جنریک رو اضافه نکنه من حتی بهش فکر هم نمیکنم… اینم شد زبان آخه؟»

هر بار که این حرف ها را می‌شنوم (که دفعات اش کم هم نیست!)، فورا افکار زیر از ذهنم عبور می‌کند:

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

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

برای اینکه درباره قابلیت‌های یک زبان بحث کنیم، اول باید به دو نکته توجه کنیم:

  • چرا فلان قابلیت، در فلان زبان حضور دارد.
  • چرا فلان قابلیت، در فلان زبان حضور ندارد!

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

وقتی هم تصمیم میگیرند قابلیت ای را به زبان اضافه نکنن، اصولا به این خاطر است که میخواسته‌اند از به وجود اومدن «یک سری مشکلات» جلوگیری کنند! گاهی قابلیت‌های مختلف یک زبان، خودشان باعث بروز مشکلات جدیدی در دراز مدت خواهند شد که طراحان مجبورند برای حل این مشکلات جدید، باز یک سری قابلیت‌های جدید به زبان اضافه کنند! و این قضیه ممکن است به صورت زنجیره وار ادامه پیدا کند!

در جامعه‌ی گولنگ، همه می‌دانند که «مهم‌ترین قابلیت گولنگ این است، که خیلی از قابلیت‌ها را ندارد!».

بعد از مدتی کار با گولنگ، به خاطر این قضیه شکرگذار خواهید بود! گولنگ همانند C، یک زبان خاتمه یافته است! (دقت کنید گولنگ را همان افرادی ساخته‌اند که C را سال‌ها پیش ساخته بودند!) یعنی مثل زبان‌های دیگر نیست که در هر نسخه‌ی جدیدشان چندین قابلیت جدید را به زبان اضافه می‌کنند. دلیلش این بوده که برنامه نویسان بتوانند یکبار زبان را یاد بگیرند و بعدش بچسبند به کار؛ نه اینکه هر چند ماه یکبار مجبور باشد کلی قابلیت جدید را یاد بگیرند!

گولنگ بر خلاف زبان‌های هم دوره‌اش، یک زبان «مدرن» **نیست**! اکثر ایده‌های موجود در زبان بیش از سی سال از عمرشان می‌گذرد. گولنگ یک زبان «باحال» **نیست**! گولنگ برای افرادی است که دیگر «زرق و برق» زبان‌های مختلف چشم‌شان را نمی‌گیرد و فقط دوست دارند یک کد ساده، پایدار، و بهینه بنویسند و بروند خانه! گولنگ آن کُنده ایست که از آن دود بلند می‌شود. گولنگ، برای برنامه‌نویسان «دل سوخته» است!

اگر زبانی مانند جاوا یک قابلیت بخصوص را نداشته باشد، ممکن است بگوییم به دلیل حساسیت بالایی که این زبان به مساله Backward compatibility دارد این حالت اتفاق افتاده است. یعنی شاید وارد کردن آن قابلیت برای کدهای پیشین مشکل زا شود. اگر زبانی مانند جاوااسکریپت قابلتی را نداشته باشد، ممکن است بگوییم که این زبان از ابتدا برای کارهای کوچک (در حد کدهای ۵۰-۶۰ خطی) ساخته شده بود و طراحی آن از پایه به گونه ای بود که اضافه کردن قابلیت‌ها به آن سخت‌تر باشد. در مورد گولنگ وضع چگونه است؟ فرضا آیا نبود قابلیت‌ها در آن به خاطر Backward compatibility است یا طراحی نامناسب آن؟

گولنگ را این افراد طراحی کرده‌اند: Robert Griesemer, Rob Pike, Ken Thompson. اگر این افراد را نمی‌شناسید، مانند این است که فوتبالیست باشید ولی مسی و رونالدو را نشناسید! اگر هم این افراد را می‌شناسید، احتمالا می‌دانید که این اسامی در چه حد «سنگین» هستند! بنابراین این گزینه که نبود قابلیت‌ها در گولنگ ممکن است به دلیل سازندگان ناشایست و کارنابلد اش باشد خط خواهد خورد…

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

اولین نسخه‌ی پایدار گولنگ در سال ۲۰۱۲ منتشر شد. یعنی گولنگ یک زبان جوان است. طراحان، این زبان را از صفر طراحی کرده اند و اصلا نیاز نداشتند که نگران قضیه Backward compatibility باشند. پس این قضیه نیز نمی‌تواند دلیل نبودن یک سری از قابلیت‌ها در گولنگ باشد…

پس براستی دلیل اینکه گولنگ یک سری قابلیت‌ها را در زبان قرار نداده چیست؟

جواب در دو کلمه خلاصه می‌شود: «فلسفه‌ی گولنگ»!

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

فلسفه‌ی گولنگ چیست؟

«سادگی تا سرحد امکان!».

این فلسفه مختص گولنگ نیست؛ کن تامپسون و همکاران قدیمی‌اش در آزمایشگاه‌های Bell ، از سال‌های دور به اصرار روی این فلسفه شهرت دارند. در این فلسفه، سادگی بالاترین هدف است. حتی اگر این سادگی باعث شود کارها کمی سخت‌تر یا کمی کندتر پیش برود. این فلسفه به «مدل توسعه‌ی نیوجرسی» شهرت دارد. (شهری که پایگاه Bell است).

درباره این نوع نگاه به توسعه‌ی نرم‌‌افزار، کتاب‌ها و مقالات و مصاحبه‌های زیادی وجود دارد. می‌توانید عبارت Worse is better را در گوگل جستجو نمایید و لینک ها را از بالا به پایین مطالعه کنید.

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

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

«… پارسال به کنفرانسی رفتم… سخنرانی‌های زیادی در آنجا دیدم… خیلی از سخنرانی ها از طرف رهبران زبان‌های دیگر بود…جاوا اسکریپت، پی‌اچ‌چی، سی شارپ… درباره نسخه‌ی جدید زبان‌هایشان حرف می‌زدند… چیزی که بیشتر از همه توجه من را جلب کرد این بود که بخش بزرگی از سخرانی‌شان درباره قابلیت‌هایی بود که از دیگر زبان‌ها برداشته بودند و در این نسخه‌ی جدید به زبان خودشان اضافه کرده بودند…به این فکر کردم که این زبان‌ها اینقدر شبیه هم شده‌اند که می توان آن‌ها را در قالب یک زبان تصور کرد…» – راب پایک

از نظر من حرف بالا واقعا درست است. زبان‌های دیگر آنقدر در اضافه کردن قابلیت‌های جدید عطش دارند که در هر نسخه‌ی جدیدشان کلی قابلیت‌های تازه ارائه می کنند. و با این حال باز هم از زبانی مانند C که ۴۵ سال است قابلیت خاصی به آن اضافه نشده عقب تر هستند! وقتی همه‌ی زبان‌ها، همه‌ی قابلیت‌ها را به خود اضافه کنند، دیگر فرق زیادی با یکدیگر نخواهند داشت. همه‌شان یک شکل شده‌اند!

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

دست یابی به جنریک در گولنگ

حالا که می دانیم جنریک‌ها چگونه در زبان‌های مختلف پیاده‌سازی می‌شوند، سوال پیش می‌آید که گولنگ از کدام یکی از این روش ها پشتیبانی می‌کند؟ بسته‌سازی یا تولید کد؟ جواب: «هر دوی آن‌ها».

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

چرا در گولنگ نیاز به جنریک کمتر حس می‌شود؟

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

  • گولنگ از تایپ سیستم مبتنی بر ساختار (Structural type system) بهره می‌برد. در زبان‌هایی مثل ++C یا Java یا #C یا غیره، تمرکز اصلی روی سلسله مراتب کلاس است؛ در صورتی که در گولنگ تمرکز اصلی روی کامپوزیشن است. کل طراحی زبان گولنگ حول محور «کامپوزیشن» بنا گشته است. از همین رو «اینترفیس» ها و نحوه‌ی پیاده‌سازی آن‌ها در گولنگ از اهمیت بالایی برخوردار است. پیاده‌سازی منحصربه‌فرد گولنگ از مکانیزم اینترفیس، باعث شده است که این زبان قادر به استفاده از داک تایپینگ (Duck typing) باشد. یعنی با اینکه گولنگ یک زبان استاتیک است، اما براحتی قادر است برای قسمتی از کدها حالت داینامیک به خود بگیرد. این قضیه بخش بزرگی از نیاز به جنریک‌ها را در گولنگ کاهش داده است.

  • مهم‌ترین کاربرد جنریک در زبان‌های استاتیک، پیاده‌سازی کالکشن‌ها است (ساختارهای داده‌ای ترکیبی). گولنگ به طور پیشفرض یک سری کالکشن پایه ارائه می‌کند که تمام آن‌ها به صورت داخلی حالت جنریک دارند! تایپ‌هایی مانند array و slice و map و توابعی مانند ()new و ()make و ()chan و ()append و غیره نیز همگی حالت جنریک دارند. پایتون‌ کاران را در نظر بگیرید، اگر از آن‌ها بپرسید پر استفاده ترین ساختاری که با کار می‌کنید چیست؟ خواهند گفت List و Dict… اگر از یک پی‌اچ‌پی کار همین سوال را بپرسید، خواهد گفت: array… در واقع، هر دوی آن‌ها اعتراف خواهند کرد که تقریبا تمام کدنویسی خود را بر مبنای این ‌ها انجام می دهند و به ندرت از ساختار ترکیبی دیگری استفاده می‌کنند! گولنگ نیز با slice و map و توابع حول و حوش آن‌ها، عملا درصد بسیار بالایی از نیازمندی به جنریک را از بین برده است.

«شاخص‌ترین و قدرتمند‌ترین قابلیت گولنگ، اینترفیس‌های آن است... مردم از امکانات همروندی گولنگ بیشتر سخن می‌گویند، ولی این امکانات به اندازه‌ی اینترفیس‌ها در کدهای روزانه شما دخیل نیستند...» -- *راب پایک*

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

کپی و پست

در چند جای برنامه نیاز به جنریک دارید؟ چند تایپ را قرار است به جنریک وارد کنید؟

اگر در حد ۲-۳ مورد بود، از کپی و پست استفاده کنید! فرضا برای هر تایپ، یک گروه جدید از توابع مخصوص آن تایپ ایجاد کنید. جدی، کاملا جدی!

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

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

اینکه در ۲-۳ خط از برنامه نیازمند جنریک شویم، و فقط ۲-۳ تایپ را به آن‌ها وارد کنیم، بخش عمده‌ای از نیاز ما به جنریک در پروژه‌های مختلف را تشکیل می‌دهد!

پیاده‌سازی جنریک‌ها در گولنگ: بسته‌بندی و بسته‌گُشایی

در زبان‌های استاتیک، تایپی وجود دارد که اصطلاحا آن را Any صدا می‌زنند. تایپ Any، تایپی ایست خنثی که هر تایپ دیگری می‌تواند به جایش بنشیند. در زبان C، این تایپ در قالب *void حضور دارد. در گولنگ نیز چنین تایپی با عبارت {}interface احضار می‌شود.

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

فرضا بیاید در گولنگ یک ساختار داده‌ای ترکیبی و جنریک شبیه List ها در پایتون را ایجاد کنیم. ابتدا در مسیر GOPATH/src$ یک پروژه جدید با اسم generic1 ایجاد کنید و فایل‌ها و دایرکتوری‌های آن را با شمایل زیر بچینید:

generic1/
├── genericlist/
│   └── genericlist.go
└── main.go

پروژه بالا بسیار ساده است. فایل main.go که در ریشه‌ی پروژه قرار دارد فایلی است که تابع اصلی ()main در آن حضور دارد. یک پکیج به اسم genericlist هم ساخته‌ایم که کدهایش در فایل genericlist.go قرار خواهد گرفت (نیاز نیست نام فایل حتما با نام دایرکتوری یکی باشد)

حالا کدهای زیر را در فایل genericlist.go کپی نمایید:

package genericlist


type Element interface{}


type List struct {
  list []Element
}


func NewList() *List {
  return &List { list: []Element{} }
}


func (L *List) Insert(el interface{}) {
  L.list = append(L.list, el)
}


func (L *List) Retrieve(i int) interface{} {
  return L.list[i]
}

کد بالا ساده و کوتاه است. دقت کنید که چگونه عناصری که در لیست قرار خواهند گرفت را توسط Element بسته بندی کرده ایم. همچنین پارامتر توابع همگی از تایپ {}interface هستند. به این معنی که این تابع می‌تواند هر تایپی را به عنوان ورودی قبول کند.

تایپی که لیست را در آن نگه داری می‌کنیم یک استراکت است با نام List. درون این استراکت هم یک slice از نوع Element تعریف کرده ایم با نام list (حرف اول کوچک است) که عناصر لیست را در آن خواهیم ریخت. برای این استراکت دو متد تعریف کرده ایم:

  • متد Insert که عناصر را به لیست اضافه می‌کند
  • متد Retrieve که با قبول یک ایندکس، عنصری که در آن ایندکس هست را برمی‌گرداند.

برای استفاده از این پکیج، کدهای زیر را در فایل main.go کپی کنید:

package main

import "fmt"

import "generic1/genericlist"


func main() {

  // a list with int items.
  L := genericlist.NewList()

  L.Insert(6)
  L.Insert(8)

  eleven := L.Retrieve(0).(int) + 5
  twelve := L.Retrieve(1).(int) + 4


  // a list with string items.
  L2 := genericlist.NewList()

  L2.Insert("H")
  L2.Insert("J")

  hello := L2.Retrieve(0).(string) + "ello"
  john  := L2.Retrieve(1).(string) + "ohn"

  // a list with float items.
  L3 := genericlist.NewList()
  L3.Insert(3)
  L3.Insert(3.14)
  L3.Insert("Pi")


  fmt.Println(L)
  fmt.Println(eleven)
  fmt.Println(twelve)

  fmt.Println("------------")

  fmt.Println(L2)
  fmt.Println(hello)
  fmt.Println(john)

  fmt.Println("------------")
  
  fmt.Println(L3)
}

در کد بالا، ما از پکیج genericlist استفاده کردیم و متغیرهایی به نام‌های L و L2 ساختیم که هر کدام به ترتیب شامل لیست int و string هستند. در آخر هم متغیری به اسم L3 داریم که شامل یک لیست با عناصر ترکیبی است (مشابه چیزی که در زبان‌های داینامیک دارید). ما L3 را با سه تایپ مختلف int و string و float پر کردیم.

دقت کنید در پکیج genericlist آرگومان‌های مربوط به توابع لیست را را با کمک {}interface بست بندی کرده بودیم. از همین رو اگر بخواهیم مقداری را از لیست بیرون بکشیم، باید آن را بسته گشایی کنیم. فرضا برای بسته گشایی مقادیر int از لیست اول، به این شیوه عمل کردیم:

eleven := L.Retrieve(0).(int) + 5

حواس‌تان به آن (int). که بعد از تابع Retrieve آمده باشد. تابع Retrieve تایپِ بسته بندی شده‌ی {}interface را بر‌می‌گرداند که در داخل خود مقدار اصلی int را نگه داشته است. برای دسترسی به آن مقدار int ، ما از (int). استفاده کردیم. این عمل را باید برای هر تایپ دیگری که مقدارش درون {}interface ها بسته‌بندی شده است نیز انجام دهیم.

$ go install

دستور بالا پروژه را کامپایل می‌کند و فایل اجرایی تولید شده را در مسیر GOPATH/bin$ قرار می‌دهد. اگر بدرستی شاخه‌ی GOPATH/bin$ را به مسیر اجرایی سیستم اضافه کرده باشید (PATH)، می‌توانید با فراخوانی نام فایل اجرایی، برنامه را اجرا نمایید. در صورت اجرای موفق، خروجی زیر را خواهید داشت:

$ generic1

&{[6 8]}
11
12
------------
&{[H J]}
Hello
John
------------
&{[3 3.14 Pi]}

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

پیاده‌سازی جنریک‌ها در گولنگ: تولید کد

در جامعه‌ی کاربری گولنگ، توسعه‌ی برنامه‌ها با توسل به تکنیک‌های مختلف «تولیدِ کد» تبدیل به یک ایده‌آل شده است. از نسخه‌ی ۱.۴ گولنگ، ابزاری به نام go generate نیز به جعبه ابزار زبان اضافه شده که عملیات تولید کد را راحت‌تر می‌کند.

همانطور که گفتیم یکی از راه‌های پیاده‌سازی جنریک در زبان‌های مختلف، استفاده از تکنیکِ تولیدِ کد است؛ و گولنگ هم که در تکنیک‌های تولید کد، دستِ توانایی دارد!

شما می‌توانید تکنیک Monomorphization که در زبان‌هایی مانند ++C یا Rust برای پیاده‌سازی جنریک استفاده شده است را در گولنگ نیز پیاده‌سازی کنید. تنها فرق این است که در آن زبان‌ها Monomorphization به طور خودکار و به دور از چشم برنامه‌نویسان اتفاق می‌افتد، ولی در گولنگ به کمک ابزارهای جانبی اینکار انجام می‌شود و برنامه‌نویس باید به طور مستقیم آن ابزار را فراخانی کند. (اختلاف تنها در «یک خط» دستور اضافه‌تر است!)

خوشبختانه برای جنریک‌ها، لازم نیست خودتان ابزار خاصی بسازید. به اندازه‌ی کافی ابزارهای مختلف برای این منظور ساخته شده است:

https://github.com/clipperhouse/gen
https://github.com/cheekybits/genny
https://github.com/joeshaw/gengen
https://github.com/droundy/gotgo
https://github.com/taylorchu/generic

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

$ go get github.com/cheekybits/genny

دستور بالا ابزار genny را از اینترنت دریافت می‌کند و آن را در GOPATH$ نصب خواهد کرد. (ممکن است به وی/پی/ان نیاز پیدا کنید).

حالا می‌خواهیم همان پکیج genericlist که در قسمت قبلی تعریف کرده بودیم را به شیوه‌ی جدید بسازیم. ابتدا در مسیر GOPATH/src$ یک پروژه جدید به اسم generic2 ایجاد کنید و فایل‌ها و دایرکتوری‌های آن را با شمایل زیر بچینید:

genric2/
├── genericlist/
│   └── genericlist.go
└── main.go

حالا کدهای زیر را در فایل genericlist.go کپی نمایید:

//go:generate genny -in=$GOFILE -out=genny-$GOFILE gen "Element=int"

package genericlist


import "github.com/cheekybits/genny/generic"


type Element generic.Type


type ElementList struct {
  list []Element
}


func NewListElement() *ElementList {
  return &ElementList { list: []Element{} }
}


func (L *ElementList) Insert(el Element) {
  L.list = append(L.list, el)
}


func (L *ElementList) Retrieve(index int) Element {
  return L.list[index]
}

اگر خوب کدها رو بررسی کنید، متوجه می‌شوید که ما تغییرات زیادی به نسبت پکیج قدیمی genericlist اعمال نکرده‌ایم. الگوریتم‌ها همه یکسان است و فقط نام یک سری از عناصر تغییر کرده. اول از همه، پکیج مربوط به ابزار genny را به برنامه وارد کردیم. سپس به جای اینکه Element را از تایپ {}interface تعریف کنیم، آن را با یک تایپ مخصوص به نام generic.Type تعریف کرده ایم که از پکیج مربوط به genny در دسترس خواهد بود.

وقتی Element را از این تایپ تعریف می‌کنیم، یعنی به ابزار genny اعلام کرده‌ایم که این عنصر در واقع یک «متغیرِ تایپ» است و تایپ‌های اصلی در آینده قرار است به جای این عنصر بنشینند.

کاری که genny می کند چیست؟

کد بالا به فرم جنریک نوشته شده است. و اعلام کرده‌ایم که Element همان «متغیرِ تایپ» این کد است و در آینده باید با تایپ‌های اصلی جایگزین شود. ابزار genny این کد را آنالیز می کند، و هر جا که با Element مواجه شد، آن را با تایپ نهایی و مورد نظر ما تعویض می کند. با اینکار، کدهای جنریک ما را تبدیل به کدهای غیر جنریک خواهد کرد.

ما می‌توانیم genny را به شکل مستقل در ترمینال صدا بزنیم. اما فرمان go generate از جعبه ابزار گولنگ، این کار را برای ما راحت تر کرده است. به خط اول از کد بالا دقت کنید. آن خط یک کامنتِ معمولی نیست، یک کامنتِ مخصوص است!

//go:generate genny -in=$GOFILE -out=genny-$GOFILE gen "Element=int"

تکه‌ی اول که شامل go:generate// است، به کامپایلر گولنگ می‌گوید که در این خط قرار است فرمان مخصوصی را اجرایی کند. این فرمان در مثال بالا همان ابزار genny است. ادامه‌ی خط، مانند صدا کردن genny در ترمینال است.

  • آرگومان in- تعیین می‌کند کدام فایل آنالیز شود (در مثال بالا فایل فعلی پردازش می‌شود).
  • آرگومان out- تعیین می‌کند که حاصل عملیات genny در چه فایلی ریخته شود.
  • زیر فرمانِ gen نیز تایپ‌های مقصد را مشخص می‌کند. مثلا در بالا به genny اعلام کرده‌ایم Element ها را با int جایگزین کند. هر تعداد تایپ دیگر که دوست داریم می توانیم در این قسمت لحاظ کنیم (با , آن‌ها را از هم جدا کنید)

اگر در دایرکتوری پروژه هستید، تنها کافیست به دایرکتوری پکیج genericlist وارد شوید و فرمان زیر را در ترمینال اجرا کنید:

$ cd genericlist
$ go generate

با اینکار genny پردازش ‌اش را انجام می دهد و خروجی کارش را در فایل جدیدی به اسم genny-genericlist.go قرار خواهد داد. اگر این فایل را باز کنید، با کدهای زیر مواجه خواهید شد:

// This file was automatically generated by genny.
// Any changes will be lost if this file is regenerated.
// see https://github.com/cheekybits/genny

package genericlist

type IntList struct {
	list []int
}

func NewListInt() *IntList {
	return &IntList{list: []int{}}
}

func (L *IntList) Insert(el int) {
	L.list = append(L.list, el)
}

func (L *IntList) Retrieve(index int) int {
	return L.list[index]
}

همانطور که می‌بینید، کدهای اضافی پاک شده‌اند و تمام Element ها با int که تایپ مد نظر ما بوده تعویض شده‌اند.

برای استفاده از پکیج‌ای که ساختیم، کدهای زیر را در main.go کپی کنید:

package main

import "fmt"

import "generic2/genericlist"


func main() {
  L := genericlist.NewListInt()

  L.Insert(6)
  L.Insert(8)

  eleven := L.Retrieve(0) + 5
  twelve := L.Retrieve(1) + 4

  fmt.Println(L)
  fmt.Println(eleven)
  fmt.Println(twelve)
}

حالا به دایرکتوری ریشه‌ی پروژه برگردید و پروژه را کامپایل و اجرا نمایید:

$ go install

$ generic2

&{[6 8]}
11
12

این روش کاملا استاتیک است و در زمان کامپایل اتفاق می‌افتد؛ همچنین نیازی به هیچ گونه عملیات زمان اجرا ندارد و از نظر امنیتِ مربوط به تایپ سیستم و سرعت در بالاترین سطح ممکن است. تنهای کاری که باید می کردید این بود که یکبار کدهای‌تان را به شکل جنریک بنویسید، و تنها با یک فرمان go generate آن‌ کد را پردازش نمایید. زیاد سخت نبود، بود؟

سخن آخر

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

  • با توجه به اینکه گولنگ دارای تایپ سیستم مبتنی بر ساختار (Structural type system) است و تعدادی کالکشن استاندارد و جنریک را هم به طور پیشفرض در خودش دارد، نیاز ما به جنریک در گولنگ بسیار پایین‌تر از بقیه زبان‌های استاتیک است.

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

پ.ن: قصد این نوشته ابدا این نبوده که قابلیت جنریک را بی اهمیت جلوه دهد. جنریک یکی از مفیدترین مکانیزم‌های موجود در زبان‌های استاتیک تایپ است. در مورد گولنگ، شخصا فکر می‌کنم بسیار خوب خواهد شد اگر جنریک به زبان اضافه شود؛ در واقع هدف اصلی این نوشته این بوده که برنامه‌نویسان در هر زبانی که برنامه نویسی می‌کنند، سعی کنند بیشتر از اینکه با قابلیت‌هایش آشنا شوند، با «کم و کاستی» هایش آشنا شوند؛ روی این کم و کاستی‌ها فکر کنند؛ و راه حل‌های جایگزین آن را بررسی نمایند؛ چرا که اگر بخواهند به جای کار مفید و مثبت، مدام در حال بهانه‌جویی باشند، فقط خودشان را خسته کرده‌اند. در دنیای برنامه نویسی، هیچ زبانی بی ایراد نیست!

نظرات

comments powered by Disqus