Crystal-এ একটি ভ্যারিয়েবল একাধিক টাইপ নিয়ে গঠিত হতে পারে। এটাকে বলা হয় ইউনিয়ন টাইপ। Crystal-এ কম্পাইলার নিজেই প্রায়ই একটি ইউনিয়ন টাইপ ইনফার করে।
রানটাইমে একটি ইউনিয়ন টাইপ তবুও একটি একক টাইপ, এমনকি যদি তা একাধিক টাইপ দিয়ে গঠিত হয়।
এর মানে হলো, যদি একটি ইউনিয়ন টাইপ String ও Int32 দিয়ে তৈরি হয়, তাহলে সেটি একই সময়ে দুটোই হবে না।
বরং সেটি হয় একটি String, নয়তো একটি Int32 হবে।
টাইপগুলোর মাঝে একটি পাইপ (|) দিয়ে আলাদা করে ইউনিয়ন টাইপ ডিক্লেয়ার করা হয়।
এগুলো প্রায়ই বন্ধনীর ভেতরে রাখা হয়, তবে এটি বাধ্যতামূলক নয়।
সবচেয়ে কমন ইউনিয়ন টাইপ হলো (T | Nil), যেখানে T একটি টাইপ; এটি এমন মেথডে দেখা যায় যেগুলো Nil রিটার্ন করতে পারে।
একে একটি ভ্যারিয়েবল নিলেবল হওয়াও বলা হয়।
foo : String = "Hello"
foo = nil # Error: type must be String, not (String | Nil)
foo : (String | Nil) = "Hello"
foo = nil
এটি কেবল দুটি টাইপেই সীমাবদ্ধ নয়, আপনি যত চান তত টাইপ থাকতে পারে।
foo : (String | Int32 | Nil | Float64) = "Hello"
foo = 1
foo = nil
foo = 1.0
typeof বনাম Object#class
একটি ভ্যারিয়েবলের টাইপ বের করার দুটি উপায় আছে।
হয় typeof ব্যবহার করে, নয়তো Object#class ব্যবহার করে।
পার্থক্য হলো, typeof কম্পাইল টাইমে ভ্যারিয়েবলের টাইপ রিটার্ন করে, আর Object#class রানটাইমে টাইপ রিটার্ন করে।
এর মানে হলো, কোনো ভ্যারিয়েবল ইউনিয়ন টাইপ কি না তা জানতে চাইলে Object#class সেটি বলতে পারবে না, কারণ এটি কেবল রানটাইমে টাইপ রিটার্ন করে, যা একটি একক টাইপ।
foo = 0 == 0 ? "a" : 1
typeof(foo) # => (String | Int32)
foo.class # => String
রানটাইমে যেহেতু একটি ইউনিয়ন টাইপ একটি একক টাইপ, তাই এর উপর সব স্ট্যান্ডার্ড অপারেশনই কাজ করে। কিন্তু কোড কম্পাইল করার সময় কম্পাইলারকে জানতে হয় এটি কোন টাইপ। তাই টাইপ-নির্দিষ্ট অপারেশন ব্যবহার করতে চাইলে কোড এমনভাবে সাজাতে হয় যাতে সেটি কেবল একটি টাইপই হতে পারে।
foo : (String | Int32) = "Hello"
foo.downcase # Error: undefined method 'downcase' for (String | Int32)
ইউনিয়ন টাইপের জন্য Crystal-এ একটি বিশেষ মেথড আছে: is_a? মেথড, যা আর্গুমেন্ট হিসেবে একটি টাইপ নেয় এবং একটি বুলিয়ান রিটার্ন করে।
nil? মেথডটি is_a?(Nil)-এর একটি শর্টকাট।
is_a? মেথডটি একটি কন্ট্রোল এক্সপ্রেশনে বসালে কম্পাইলার জেনে যাবে এটি কোন টাইপ, আর এভাবে নিশ্চিত হবে যে এটি ওই টাইপই।
আর else ব্রাঞ্চের ক্ষেত্রে নিশ্চিত হবে যে এটি ওই টাইপ নয়।
foo : (String | Int32) = "Hello"
if foo.is_a?(String)
typeof(foo) # => String
foo.downcase # => "hello"
end
এই is_a? কেবল একটি একক টাইপ আর্গুমেন্ট নিতেই সীমাবদ্ধ নয়, এটি একটি ইউনিয়ন টাইপও নিতে পারে।
একাধিক টাইপের জন্য এটি &&-এর সাথে মিলিয়েও ব্যবহার করা যায়।
কন্ট্রোল এক্সপ্রেশনের সাথে ব্যবহার করার সময় is_a? মেথডটি একটি ইনস্ট্যান্স ভ্যারিয়েবল বা ক্লাস ভ্যারিয়েবল হতে পারে না।
বরং এগুলোকে প্রথমে একটি লোকাল ভ্যারিয়েবলে অ্যাসাইন করতে হয়।
ইউনিয়ন টাইপকে একটি একক টাইপে পরিণত করার একটি উপায় হলো কোড এমনভাবে সাজানো যে একটি ব্রাঞ্চে কেবল তখনই ঢোকা যাবে যদি টাইপটি একটি নির্দিষ্ট টাইপ হয়।
আরেকটি উপায় হলো as মেথড ব্যবহার করা।
এটি রানটাইমে একটি চেক করে ইউনিয়ন টাইপকে একটি একক টাইপে পরিণত করে।
টাইপটি প্রত্যাশিত টাইপ না হলে একটি এক্সসেপশন রেইজ করা হবে।
foo : String | Int32 = "Hello"
foo.as(String).downcase # => "hello"
foo.as(Int32) # Error: can't cast String to Int32
এই উপায়টি কেবল তখনই ব্যবহার করার কথা যখন আপনি নিশ্চিত যে টাইপটি প্রত্যাশিত টাইপ, অথবা টাইপটি প্রত্যাশিত না হলে একটি এক্সসেপশন রেইজ করতে চান।
অনুপযুক্ত সেটআপে এই উপায় ব্যবহার করলে অপ্রত্যাশিত আচরণের দিকে নিয়ে যেতে পারে।
as? প্রায় as-এর মতোই কাজ করে, তবে টাইপটি প্রত্যাশিত টাইপ না হলে এক্সসেপশন রেইজ না করে বরং nil রিটার্ন করে।
এর মানে হলো, এটি প্রত্যাশিত টাইপ ও Nil নিয়ে গঠিত একটি ইউনিয়ন টাইপ রিটার্ন করে।
foo : (String | Int32) = "Hello"
foo.as?(String).downcase # => "hello"
foo.as?(Int32) # => nil
নিলেবল মানে হলো একটি ভ্যারিয়েবল হয় কোনো একটি টাইপ, নয়তো Nil হতে পারে।
এটি (T | Nil) হিসেবে লেখা যায়।
কিন্তু নিলেবল টাইপ তুলনামূলকভাবে কমন হওয়ায় এর একটি শর্টহ্যান্ড আছে: T?।
# This:
foo : (String | Nil) = "Hello"
foo = nil
# Is the same as:
foo : String? = "Hello"
foo = nil
কোন টাইপ তা নিশ্চিত না হলে কম্পাইলার একটি ইউনিয়ন টাইপ ইনফার করে।
উদাহরণস্বরূপ, নিচের কোডে কম্পাইলার জানবে না foo কোন টাইপ, কারণ এটি String বা Int32 দুটোর যেকোনো একটি হতে পারে।
if true
foo = 1
else
foo = "Hello"
end
typeof(foo) # => (Int32 | String)
এই ইনফারেন্স স্বয়ংক্রিয়ভাবে ঘটে, এবং এমন আরও পরিস্থিতি আছে যেখানে উদাহরণস্বরূপ কম্পাইলার ইনফার করে যে একটি মেথড একটি ইউনিয়ন টাইপ রিটার্ন করে।
character = "Hello world"[10]?
typeof(character) # => (Char | Nil)