یک تاپل فهرستی متناهی و مرتب از عناصر است که تغییرناپذیر است. در تاپل، همهی موقعیتها باید نوع ثابتی داشته باشند. این یعنی کامپایلر میداند در هر موقعیت چه نوعی قرار دارد. نوعهای بهکاررفته در یک تاپل میتوانند در هر موقعیت متفاوت باشند، اما باید در زمان کامپایل مشخص باشند.
بسته به اینکه نوع مقادیر تاپل در زمان کامپایل قابل تشخیص باشد یا نه، تاپل به روشهای مختلفی ساخته میشود. اگر مقادیر در زمان کامپایل مشخص باشند، میتوان تاپل را با نحوهی نگارش تحتاللفظی تاپل ساخت؛ در غیر این صورت باید بهصراحت اعلام شوند. همچنین مهم است که نوع مقادیر با نوعهای مشخصشده در تاپل همخوانی داشته باشد و تعداد مقادیر با تعداد نوعهای مشخصشده برابر باشد. در ادامه نمونهای از تعریف با نحوهی نگارش تحتاللفظی تاپل را میبینید:
tuple = {1, "foo", 'c'} # Tuple(Int32, String, Char)
همچنین میتوان تاپلی را با کلاس Tuple ساخت.
tuple = Tuple(Int32, String, Char).new(1, "foo", 'c')
روش دیگر این است که نوع متغیری را که به تاپل نسبت داده میشود، بهصراحت مشخص کنید.
tuple : Tuple(Int32, String, Char) = {1, "foo", 'c'}
مشخصکردن صریح نوع تاپل میتواند مفید باشد، چون امکان میدهد تعریف کنید که یک موقعیت باید نوع اتحادی داشته باشد. این یعنی یک موقعیت میتواند چند نوع را در خود نگه دارد.
tuple : Tuple(Int32 | String, String, Char) = {1, "foo", 'c'}
میتوانید با استفاده از متد from کلاس Tuple، از یک آرایه تاپل بسازید.
این کار مستلزم آن است که نوع تاپل مشخص شده باشد.
array = [1, "foo", 'c']
tuple = Tuple(Int32, String, Char).from(array)
میتوانید با استفاده از متد to_a یک تاپل را به آرایه تبدیل کنید.
نوع عناصر آرایهی حاصل، اجتماع نوع هر فیلد در تاپل است.
tuple = {1, "foo", 'c'}
array = tuple.to_a
array # => [1, "foo", 'c']
مانند آرایهها، اندیسگذاری تاپل از صفر شروع میشود؛ یعنی اولین عنصر در اندیس ۰ قرار دارد.
اما برخلاف آرایهها، نوع هر عنصر ثابت و در زمان کامپایل مشخص است؛ بنابراین هنگام اندیسگذاری روی تاپل، نوع عنصر مخصوص همان موقعیت است.
برای دسترسی به یک عنصر در تاپل، میتوانید از عملگر [] استفاده کنید.
array = [1, "foo", 'c']
array[0] # => 1
typeof(array[0]) # => Int32 | String | Char
tuple = {1, "foo", 'c'}
tuple[0] # => 1
typeof(tuple[0]) # => Int32
تفاوت دیگر در دسترسی به عناصر نسبت به آرایهها این است که اگر اندیس مشخص شده باشد، کامپایلر بررسی میکند که اندیس در محدودهی تاپل باشد. یعنی بهجای خطای زمان اجرا، خطای زمان کامپایل دریافت میکنید.
tuple = {1, "foo", 'c'}
tuple[3]
# => Error: index out of bounds for Tuple(Int32, String, Char) (3 not in -3..2)
اما اگر اندیس در یک متغیر ذخیره شده باشد، کامپایلر نمیتواند در زمان کامپایل بررسی کند که اندیس در محدودهی تاپل قرار دارد یا نه و در عوض خطای زمان اجرا میدهد.
میتوانید با استفاده از عملگر [] همراه با یک بازه، زیرتاپلی از یک تاپل به دست آورید.
آنچه برگردانده میشود، تاپل جدیدی است که شامل عناصر بازهی مشخصشده است.
بازه باید در زمان کامپایل داده شود، در غیر این صورت کامپایلر نمیتواند نوع عناصر زیرتاپل را بداند.
یعنی بازه باید یک بازهی تحتاللفظی باشد و به متغیری نسبت داده نشود.
tuple = {1, "foo", 'c'}
subtuple = tuple[0..1] # Tuple(Int32, String)
i = 0..1
tuple[i]
# Error: Tuple#[](Range) can only be called with range literals known at compile-time
تاپلها زمانی مفیدند که بخواهید تعدادی مشخص از مقادیر را کنار هم گروهبندی کنید و نوع مقادیر در زمان کامپایل مشخص باشد. دلیلش این است که تاپلها حافظهی کمتری مصرف میکنند و به دلیل تغییرناپذیری، از آرایهها سریعترند. کاربرد دیگر، برگرداندن چند مقدار از یک متد است. این کار بهویژه زمانی کمککننده است که مقادیر نوعهای متفاوتی داشته باشند، چون هر موقعیت در تاپل میتواند نوع متفاوتی داشته باشد.
هر جا به ساختار دادهای نیاز باشد که بتواند بزرگ یا کوچک شود یا مرتباً نیاز به تغییر داشته باشد، نباید از تاپل استفاده کرد.
نمادها شناسههای نامداری هستند که میتوان از آنها برای اشاره به یک مقدار استفاده کرد.
نمادها از طریق یک نماد تحتاللفظی ساخته میشوند؛ یعنی با قرار دادن کاراکتر : پیش از یک اسم، مثلاً :foo.
همچنین میتوان آنها را داخل گیومه نوشت، مثلاً :"foo"، که برای نمونه امکان استفاده از فاصله در اسم را میدهد.
:foo # => :foo
:"foo boo" # => :"foo boo"
:foo == :"foo" # => true
نمادها در جاهای زیادی از زبان استفاده میشوند، از جمله به عنوان کلید در namedtuple و برای نمایش اسم متدها و متغیرها.
نمادها در Crystal با Ruby تفاوت زیادی دارند. در Crystal، نماد نوعی ثابت است و بنابراین در زمان کامپایل مقداردهی میشود. یعنی نمادها را نمیتوان بهصورت پویا ساخت، کاری که در Ruby ممکن است.
نمادها در Crystal بهصورت Int32s نمایش داده میشوند که آنها را بسیار کارآمد میکند.
نمادها را میتوان به رشته تبدیل کرد، اما عکس آن ممکن نیست. دلیلش این است که نمادها در زمان کامپایل و رشتهها در زمان اجرا ساخته میشوند.
:foo.to_s # => "foo"
هنگام آمادهشدن برای پختن کلوچه برای دوستانتان، متوجه میشوید که باید برخی از اندازههای بهکاررفته در دستور پخت را تبدیل کنید. چون فقط با دستگاه متریک آشنایید، باید راهی پیدا کنید تا اندازههای رایج آشپزی آمریکایی را برای راحتی خودتان به میلیلیتر (mL) تبدیل کنید.
برای حل مسئله از این جدول تبدیل استفاده کنید:
| واحد برای تبدیل | حجم | بر حسب میلیلیتر (mL) |
|---|---|---|
| mL | 1 | 1 |
| US cup | 1 | 240 |
| US fluid ounce | 1 | 30 |
| US teaspoon | 1 | 5 |
| US tablespoon | 1 | 15 |
چون برنامهنویسی مستعد و در حال یادگیری هستید، تصمیم میگیرید میلیلیتر را بهعنوان «واحد واسط» به کار ببرید تا تبدیل از هر واحد فهرستشده به واحد دیگر (حتی به خودش) آسان شود.
متد KitchenCalculator.get_volume را پیادهسازی کنید.
این متد با گرفتن یک جفت حجم، فقط مؤلفهی عددی را برمیگرداند.
KitchenCalculator.get_volume({:cup, 2.0})
# => 2.0
متد KitchenCalculator.to_milliliter را پیادهسازی کنید.
این متد با گرفتن یک جفت حجم، حجم را با استفاده از جدول تبدیل به میلیلیتر تبدیل میکند.
KitchenCalculator.to_milliliter({:cup, 2.5})
# => {:milliliter, 600.0}
متد KitchenCalculator.from_milliliter را پیادهسازی کنید.
این متد با گرفتن یک جفت حجم و واحد موردنظر، حجم را با استفاده از جدول تبدیل به واحد موردنظر تبدیل میکند.
KitchenCalculator.from_milliliter({:milliliter, 1320.0}, :cup)
# => {:cup, 5.5}
متد KitchenCalculator.convert را پیادهسازی کنید.
این متد با گرفتن یک جفت حجم و واحد موردنظر، حجم دادهشده را به واحد موردنظر تبدیل میکند.
KitchenCalculator.convert({:teaspoon, 9.0}, :tablespoon)
# => {:tablespoon, 3.0}