دیتا تایپ‌های جبری (‌‌ADT)

فهرست مطالب

مقدمه

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

«دیتا تایپ‌های جبری» یا Algebraic Data Types (که با نام اختصاری ADT) شناخته می‌شوند، از مهم‌ترین و پایه‌ای ترین مفاهیم تایپ سیستم‌ها هستند که متاسفانه خیلی از برنامه‌نویسان آشنایی مناسبی با آن‌ها ندارند؛ این قضیه هم مختص برنامه‌نویسان ایرانی نیست. نکته‌ی عجیب ماجرا اینجاست که تقریبا تمام برنامه‌نویسان کم و بیش با این تایپ‌ها سر و کار داشته اند.

دیتا تایپ‌های جبری با زبان ML معروف شدند؛ و هر زبانی که به نوعی ایده‌هایی از ML را در خود دارد نیز کم و بیش دارای دیتا تایپ‌های جبری می‌باشد. زبان‌هایی مانند Haskell ، Scala، Rust، Swift، Clojure، Erlang/Elixir، Ocaml، TypeScript و… برای همین تصمیم گرفتم مطابق مطالب پیشین این وبلاگ، در این پست به زبانی ساده مفهموم دیتا تایپ‌های جبری را شرح دهم.

هر تایپ، یک مجموعه است

در مقاله‌ی مربوط به «مفاهیم بنیادین تایپ سیستم‌ها»، مفهوم «تایپ» را به این صورت تعریف کردیم:

«خصوصیتی است که تعیین می‌کند یک «داده»، می‌تواند شامل چه «محتوا» ای باشد و چه کارهایی می‌توان با آن انجام داد».

فرضا وقتی می‌گوییم متغیر A از تایپ int است منظورمان چیست؟ یعنی متغیر A می‌تواند شامل «یکی» از حالاتی باشد که از طرف تایپ int قابل ارائه است.

سوال: تایپ int چه حالاتی را ارائه میکند؟

با فرض بر اینکه int را از نوع ۳۲بیتی در نظر گرفته باشیم، مقادیر قابل ارائه در تایپ int یکی از اعدادی خواهد بود که بین منفی 2147483648 تا مثبت 2147483647 هستند. میتوان اینطور گفت که int در واقع بیان کننده‌ی «مجموعه» مقادیری است که بین این دو عدد قرار دارد. اگر بخواهیم به زبان ریاضی توضیح دهیم یعنی این:

int = {-2147483648, -2147483647, -2147483646, . . . , 2147483647}

چنین تعریفی برای بقیه تایپ‌ها نیز صدق می‌کند. مثلا تایپ string شامل «مجموعه»ای است که از تمام کاراکترهای یونیکد تشکیل شده است. یا مثلا تایپ bool یا boolean شامل «مجموعه» ای دو عضوی است: true یا false

پس هر تایپ، در قالب یک «مجموعه» قابل تعریف است. (Set)

دیتا تایپ جبری

از آنجایی که هر تایپ یک مجموعه است، پس قادر هستیم عملیات جبری مربوط به مجموعه‌ها مثل «اشتراک» و «اجتماع» و … را روی آن‌ها اعمال کنیم! بنابراین دیتا تایپ‌های جبری را می‌توان اینچنین تعریف کرد:

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

در تعریف بالا:

  • منظورمان از «ضرب»، همان «ضرب دکارتی» است که به انگلیسی با عنوان Product یا Cartesian product شناخته می‌شود.
  • و منظورمان از «جمع»، همان «اجتماع» یا Union است.

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

ضرب یا Product

همانطور که گفتیم در این مقاله منظورمان از «ضرب»، همان ضرب دکارتی است.

تعریف ضرب دکارتی:

اگر دو مجموعه داشته باشیم به نام‌های A و B ، حاصلضرب دکارتی این دو مجموعه را با نماد A × B نشان خواهیم داد؛ و نتیجه‌ی آن برابر است با مجموعه‌ای که عضوهایش شامل تمام ترکیباتی باشد که عنصر اول آن از اعضای A انتخاب شده باشد و عنصر دیگر آن از اعضای B باشد.

مثال:

A = { x, y }

B = { 3, 4, 5 }

A × B = { (x , 3), (x , 4), (x , 5),
          (y , 3), (y , 4), (y , 5) }

دقت کنید اعضای مجموعه‌ی نهایی که از حاصلضرب دکارتی دو مجموعه‌ی اول بدست آمده‌اند، هر کدام در قالب یک «ترکیب» در مجموعه حاضر شده‌اند. مثلا مانند (x , 3).

اگر همین تعاریف را بخواهیم در برنامه‌نویسی وارد کنیم، میگوییم که:

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

چگونه یک Product Type تعریف کنیم؟

پروداکت تایپ ها در اکثر زبان‌های برنامه نویسی حضور دارند و همه‌ی شما با آن‌ها کار کرده اید! برای تعریف یک پروداکت تایپ جدید، می توانید از ساختار‌های ترکیبی مانند struct یا class یا tuple استفاده کنید. مثلا میخواهیم یک تایپ تعریف کنیم که وضعیت حضور و غیاب و شماره صندلی دانش آموزان را با آن بیان نماییم. میتوانیم این تایپ را به شکل یک پرداکت تایپ تعریف کنیم. کد آن در زبان C چیزی شبیه این خواهد بود:

typedef struct {
   bool   present;
   int    seatNumber;
} ClassRoomStudent;

(حواس‌تان باشد که تایپ bool در ویرایش C99 به زبان C اضافه شده است)

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

ClassRoomStudent sara  = {true  , 12};
ClassRoomStudent john  = {true  ,  5};
ClassRoomStudent steve = {false , 18};

if (sara.present) {
  printf("Sara is here and her seat number is: %d", sara.seatNumber);
}

struct در C

یک struct در زبان C می‌تواند چندین تایپ گوناگون را در قالب یک تایپ جدید در کنار هم گردآوری کند. برای هر کدام از تایپ هایی که در struct لیست شده‌اند نیز اسمی در نظر گرفته می‌شود تا توسط آن‌ها بتوان تایپ ها را مقدار دهی کرد. (مثل present یا seatNumber در مثال بالا).

سایز یک struct در حافظه، برابر است با مجموع سایز تمام تایپ هایی که در آن لیست شده اند. فرضا در مثال بالا یک تایپ bool و یک تایپ int را در struct لیست کرده ایم پس سایز این struct برابر است با : 1+4 = 5 بایت. (فرض کرده ایم که هر int را معادل ۴ بایت است. همچنین ۳ بایت هم برای padding اضافه خواهد شد)

دقت کنید که وقتی خواستیم یک نمونه از روی struct خود ایجاد کنیم، چگونه متغیرهای sara یا john را به صورت ترکیبی مقدار دهی کردیم… این ترکیب برابر است با همان ترکیبی که در تایپ ClassRoomStudent مشخص کرده بودیم.

سوال: مجموعه مقادیری که پروداکت تایپ ClassRoomStudent میتواند ارائه کند چیست؟

  • عنصر اول از تایپ ClassRoomStudent برابر با bool است و bool شامل دو عضو true یا false می‌باشد.
  • عنصر دوم از تایپ ClassRoomStudent برابر با int است و int شامل 4294967296 رقم مختلف است که بین اعداد منفی 2147483648 تا مثبت 2147483647 قرار دارند.

بنابراین مجموعه حالات ClassRoomStudent برابر مجموعه‌ی زیر است:

ClassRoomStudent = { (true , -2147483648), (true , -2147483647), . . ., (false , 2147483647) }

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

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

جمع یا Sum

بالاتر اشاره کردیم که منظورمان از «جمع»، همان «اجتماع» یا Union است. (در برنامه‌نویسی با گونه‌ای از Union ها به اسم Tagged union بیشتر طرف خواهید شد.)

تعریف اجتماع:

اگر دو مجموعه داشته باشیم به نام‌های A و B ، اجتماع این دو مجموعه را با نماد A ∪ B نشان خواهیم داد؛ و نتیجه‌ی آن برابر است با مجموعه‌ای که اعضایش «یا» در A هستند، «یا» در B هستند، و «یا» در هردوی آن‌ها. (حواس‌تان باشد که روی «یا» حساسیت به خرج داده ام!)

مثال:

A = { x, y }

B = { 3, 4, 5 }

A ∪ B = { x, y, 3, 4, 5 }

بر خلاف پرداکت، اینجا میبینید که اعضای مجموعه‌ی نهایی به صورت فردی و تنها ظاهر شده‌اند.

اگر همین تعاریف را بخواهیم در برنامه‌نویسی وارد کنیم، میگوییم که:

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

چگونه یک Sum Type تعریف کنیم؟

برعکس پروداکت تایپ ها که در اکثر زبان های برنامه نویسی براحتی تعریف می‌شوند، متاسفانه سام تایپ ها در هر زبانی وجود ندارند! در زمان نگارش این مقاله، بیشتر زبان‌های رده اول دنیا پشتیبانی مناسبی از سام تایپ ها ارائه نمی‌کنند. زبان‌های داینامیک مانند Python، PHP، Ruby یا JS که کلا استاتیک تایپ نیستند؛ زبان‌هایی مانند C، ++C، Java و #C نیز پشتیبانی مناسبی از سام تایپ ها ندارد.

اما زبان‌های مدرن تر مثل Scala، Rust، Swift، TypeScript، OCaml، #F یا Haskell همگی دارای پشتیبانی خوبی از سام تایپ ها می باشند.

برای اینکه بدانید داستان از چه قرار است، باید کمی برگردیم به عقب و یک سری از ساختارهای موجود در زبان C را با هم مررو کنیم.

Union ها در زبان C

union یکی از قدرتمند‌ترین و در عین حال خطرناک ترین قابلیت‌‌ها در زبان C است! تعریف یک union در زبان C دقیقا مانند تعریف کردن یک struct است:

typedef union {
    char* name;
    int   age;
    int   weight;
    int   height;
} PersonInfo;

اما union چند فرق اساسی با struct دارد:

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

یعنی سایز union در مثال بالا برابر است با سایز char* که بزرگ‌ترین فیلد union است: 8 بایت! در حالی که اگر به صورت struct تعریف‌اش می‌کردیم، سایز برابر بود با: 8+4+4+4 = 20 به اضافه‌ی 4 بایت برای padding.

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

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

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

کاری که یک Union می‌تواند برای تایپ ها انجام دهد (و در کل سام تایپ ها)، مانند کاری است که Docker برای برنامه‌ها انجام می‌دهد! اگر از بیرون نگاه کنید تایپ ما یک هویت واحد دارد، ولی از داخل می‌تواند به شیوه‌ی های مختلفی بیان شود. با سام تایپ ها می‌توان خلا موجود در بین مفاهیمی مثل Inheritance و Interface و Generic را پر کرد! (در آینده و در یک مطلب مجزا این موضوع را بیشتر برای‌تان شرح خواهم داد). فعلا همین قدر بدانید که Sum Type ها از جمله‌ی مهم ترین مفاهیم در مبحث تایپ سیستم ها می‌باشند!

تا اینجا با دلیل قدرت‌مند بودن union ها آشنا شدیم؛ اما چرا گفتیم که union ها بسیار خطرناک هستند؟ در بخش قبل گفتیم که فضای موجود در یک union به طور اشتراکی توسط فیلدهایش استفاده می‌شود. یعنی در union بالا اگر به فیلد age مقدار بدهیم، مقدار موجود در فیلدهای دیگر مثل height یا weight را بازنویسی یا overwrite خواهد کرد! اگر در آن لحظه بخواهیم به فیلدهای height یا weight دسترسی داشته باشیم، با داده‌ای غلط و درهم و برهم مواجه خواهیم شد!

هیچ راهی هم نداریم که بتوانیم از یک union سوال کنیم در حال حاضر کدام یک از فیلدهایش فعال است. یعنی تنها راه این است که خودمان تمام دسترسی هایی که به فیلدهایش انجام می‌دهیم را به ذهن بسپاریم و حواس‌مان باشد که به اشتباه فیلدی را صدا نزنیم که غیر فعال است. این موضوع باعث به وجود آمدن خطاهای بسیار زیاد خواهد شد. خوشبختانه برنامه‌نویسان C راه حلی برای این مورد پیدا کرده‌اند… فعلا این را در ذهن نگه دارید تا پایین‌تر به آن برگردیم…

Enum ها در زبان C

با enum می‌توانید تعدادی «ثابتِ عددی» که با یکدیگر دارای ارتباط منطقی هستند را در کنار هم گرد آورید! همچنین می توانید برای هر کدام از این ثابت‌های عددی یک اسم تعیین کنید تا کدهای‌تان با معنی‌تر شوند.

مثلا به جای اینکه «روز هفته» را با اعداد ۱ یا ۲ یا ۳ یا … تعریف کنید، می توانید به شکل زیر از enum استفاده کنید تا کدهای‌تان تمیزتر و بامعنی‌تر شود:

typedef enum { 
    Sunday,
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday
} WeekDay;


WeekDay day = Monday;

مخصوصا وقتی می‌خواهید در کدهای‌تان مقادیری برای Flag ها یا Status Code ها تعریف کنید، استفاده از enum ها توصیه می‌شود.

ویژگی‌های enum :

  • هر تعداد ثابت که دوست داشته باشید می‌توانید در یک enum تعریف کنید، ولی در هر لحظه فقط یکی از آن‌ها قابل انتخاب شدن خواهد بود.
  • ثابت‌ها به ترتیب تعریف شدن‌شان، به طور اتوماتیک مترادف با عدد 0 تا n خواهند شد.
  • تایپ تمام این ثابت‌ها برابر با int است.
  • سایز کلی یک enum برابر با سایز تایپ int است (معمولا 4 بایت). فرقی هم ندارد که چند ثابت در آن تعریف کرده باشید.

enum ها در زبان‌های دیگری مانند C++ یا Java یا C# هم کم و بیش همین ویژگی‌ها را دارند (هر کدام مقداری قابلیت‌های مختلف به enum ها اضافه کرده‌اند، ولی اساس کارشان یک‌ایست)

Tagged Union در زبان C

در بخشی که union ها را توضیح دادیم، مشکل اساسی آن‌ها را نیز بیان کردیم؛ همچنین گفتیم که برنامه‌نویسان C راه حلی برای دور زدن این مشکل پیدا کرده‌اند. این راه حل به شرح زیر است:

  • union مد نظرمان را درون یک struct بسته‌بندی می‌کنیم.
  • درون آن struct یک فیلد اضافه با نام tag در کنار union قرار می‌دهیم. با این فیلد تعیین می‌کنیم که کدام یک از عناصر union در آن لحظه فعال است.
  • هر بار که بخواهیم عنصری از union را مقداردهی کنیم، باید مقدار tag را هم به تناسب آن تغییر دهیم.
  • و هر بار که بخواهیم عنصری از union را بخوانیم، ابتدا باید فیلد tag را چک کنیم تا بفهمیم کدام یک از عناصر union در آن لحظه فعال است.

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

#include <stdio.h>

int main()
{
    typedef struct {
        
        short tag;
        
        union {
            int   statusCode;
            char* message;
        } data;
        
    } TaggedInfo;
    
    
    TaggedInfo info;
    info.tag = 1;
    info.data.statusCode = 404;
    
    if (info.tag == 1) {
        printf("This is your status code: %d \n", info.data.statusCode);
    }
    if (info.tag == 2) {
        printf("This is your message: %s \n", info.data.message);
    }

    return 0;
}

در مثال بالا tag با تایپ short تعریف شده که یک تایپ عددی است. یعنی باید مقادیری مثل ۱ یا ۲ یا ۳ … را به آن نسبت دهیم که بسیار شبهه برانگیز خواهد بود. برای این منظور، خیلی از برنامه‌نویسان C فیلد tag را به شکل یک enum تعریف می کنند تا کدهای‌شان معنی دار شود:

#include <stdio.h>

int main()
{
    typedef struct {
        
        enum {
            Data_StatusCode,
            Data_Message
        } tag;
    
        union {
            int   statusCode;
            char* message;
        } data;
        
    } TaggedInfo;
    
    
    TaggedInfo info;
    info.tag = Data_StatusCode;
    info.data.statusCode = 404;
    
    if (info.tag == Data_StatusCode) {
        printf("This is your status code: %d \n", info.data.statusCode);
    }
    if (info.tag == Data_Message) {
        printf("This is your message: %s \n", info.data.message);
    }

    return 0;
}

این راه حل در بین برنامه نویسان C تبدیل به یک «عُرف» شده است. به union هایی که با این روش تعریف می‌شود Tagged Union می‌گویند. اما با تمام این اوصاف، هنوز هم هیچ امنیتی در کار نیست؛ یعنی تایپ سیستم هیچ چیزی از این راه حل نمی‌داند و اشتباه برنامه‌نویس در سِت کردن درست و به موقع tag، می تواند کل برنامه را با خطا مواجه کند.

تعریف Sum Type در زبان های مدرن

همه‌ی این توضیحات را دادم تا برسم به این:

سام تایپ ها با Tagged Union قابل تعریف شدن هستند. حتی خیلی وقت‌ها اسم Sum Type مترادف با Tagged Union بیان می‌شود! همانطور که می‌بینید Tagged Union ها در زبان C نزدیک‌ترین چیزی هستند که شما می‌توانید در زبان‌های متداول‌تر برنامه‌نویسی داشته باشید.

وقتی گفتیم زبان‌هایی مانند Haskell یا Rust یا Swift از سام تایپ ها پشتیبانی می کنند، در واقع داستان این است که همین Tagged Union هایی که اینجا دیدید را به طور سازمان یافته و با سینتکس ای مناسب در بطن تایپ سیستم خود پیاده سازی کرده اند.

مثلا تایپ Info که در بالاتر تعریف کردیم، در Haskell به این شکل خواهد بود:

data Info = Status Int | Message String

یا در Swift به این شکل خواهد بود:

enum Info {
  case Status(Int)
  case Message(String)
}

یا در Rust به این شکل خواهد بود:

enum Info {
    Status(i32),
    Message(String),
}

دقت کنید طراحان Rust و Swift برای تعریف سام تایپ ها از کلید واژه‌ی enum استفاده کرده اند؛ ولی این enum برابر با آن چیزی نیست که در C یا C++ یا Java می‌شناسید. enum در Rust و Swift در واقع مترادف با ورژنِ مدرن‌ترِ Tagged Union است!

غالبا پیش از استفاده از سام تایپ‌ها، ابتدا نیاز پیدا خواهید کرد مقداری که در حال حاضر فعال است را شناسایی کنید. در زبان‌هایی مانند C این کار توسط if یا select یا switch انجام می‌پذیرد. و شما با توجه به اینکه کدام یکی از آن مقادیر فعال هستند، عکس العمل مناسب را نشان خواهید داد. (از روی فیلد tag)

در زبان‌های برنامه نویسی مدرن‌تر و به خصوص زبان‌های فانکشنال، قابلیت «تطبیق الگو» یا Pattern Matching در زبان حضور دارد که شما را از هرچه if و switch و امثال‌شان است خلاص می‌کند و کدهایی تمیزتر و قابل فهم تر تولید می‌نماید. سام تایپ ها و مکانیزم تطبیق الگو، به نوعی لازم و ملزوم یکدیگر هستند.

تا اینجای کار چون استفاده‌ی سام تایپ ها در دنیای واقعی را مشاهده نکرده‌اید، شاید هنوز قدرت سام تایپ ها را بدرستی درک نکرده باشید؛ در بخش بعد یک نمونه‌ی واقعی از سام تایپ ها را باهم مرور می‌کنیم…

تایپ Option

این تایپ از شناخته شده ترین و پرکاربرد ترین سام تایپ‌هایی است که در اکثر زبان‌های برنامه‌نویسی حضور دارد. در بعضی زبان‌ها اسمش Option است، در بعضی دیگر با اسم Optional شناخته می‌شود، و گاهی هم آن را Maybe صدا می‌زنند.

شمایل این تایپ در زبان Swift اینگونه است:

enum Optional<T> {
  case some(T)
  case none
}

یا فرضا در زبان Rust اینگونه تعریف شده است:

pub enum Option<T> {
    None,
    Some(T),
}

تعریف بالا یعنی Option دو حالت را ارائه خواهد که در هر لحظه فقط یکی از حالات می‌تواند وجود داشته باشد:

  • حالت None که یعنی هیچ چیزی وجود ندارد.
  • حالت Some که یعنی یک مقدار که دارای تایپِ T است در دسترس است و توسط Some کپسوله شده است.

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

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

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

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

اینجاست که شما می‌توانید از تایپ Option استفاده کنید! یعنی تایپِ خروجی این تابع را از نوع Option انتخاب می‌کنید و با اینکار به کامپایلر می‌گویید که این تابع هم ممکن است خطا بدهد و هم ممکن است بدون ایراد کار کند. تعریف چنین تابعی در زبانی مثل Rust اینگونه است:

fn divide(a: i32, b: i32) -> Option<i32> {
    if b == 0 {
        None           // Fail, without panic!
    } else {
        Some(a / b)    // Wrap division result in Some(), and return it.
    }
}

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

match divide(12, 0) {
  None => {
    println!("error: could not do this division!")
  },
  Some(result) => {
      println!("result is: {}", result)
  },
}

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

سخن آخر

دیتا تایپ‌های جبری، اساسی و جذاب هستند! وجود پشتیبانی مناسب از آن‌ها در یک زبان برنامه‌نویسی، می‌تواند امنیت کدهایتان را بسیار بالا ببرد. فرضا همین تایپ Option که بالاتر درباره‌اش توضیح دادیم، می‌تواند شما را از خطاهای مربوط به مقادیر null خلاص کند! در بین زبان‌های مختلف اشتیاق مناسبی برای پشتیبانی بهتر از این تایپ‌ها وجود دارد. فرضا جاوا ۸ همین تایپ Optional را به زبان اضافه کرد. یا مثلا TypeScript هم تا حدی دیتا تایپ‌های جبری را در ورژن ۲ به زبان اضافه کرده تا شما در جاوا اسکریپت هم بتوانید به مزایای این تایپ‌ها دسترسی داشته باشد. از همین رو مطالعه‌ی دیتا تایپ‌های جبری برای تمام برنامه‌نویسان سودمند خواهد بود.

نظرات

comments powered by Disqus