এখন পর্যন্ত সিলেবাসে টাইপ নিয়ে বিশেষ কিছু বলা হয়নি, তবে Julia-তে স্পষ্টভাবেই এগুলো আছে:
julia> vals = (42, 4.3, π, "hello", 'Q')
(42, 4.3, π, "hello", 'Q')
julia> typeof(vals)
Tuple{Int64, Float64, Irrational{:π}, String, Char}
আমরা কখনোই টাইপ নির্দিষ্ট করিনি, তবুও Julia নিজে থেকেই সেগুলো বরাদ্দ করে নিয়েছে।
types আছে, যা এর নকশার কেন্দ্রবিন্দু।Type Inference ব্যবহার করে টাইপটি "অনুমান" করতে পারে।JIT কম্পাইলার (সব) কোড দেখে নেবে, দেখবে একটি ভ্যারিয়েবল কীভাবে ব্যবহৃত হচ্ছে, আর সেই ব্যবহারের সাথে মানানসই একটি উপযুক্ত ডিফল্ট টাইপ ইনফার করে নেবে।
আগের ধারণাগুলোতে এমন অনেক উদাহরণ আছে যেখানে সংখ্যার টাইপগুলোকে একরকম করে তোলা হয়েছে, আর শুরুটা হয়েছিল সবচেয়ে সহজ পাটিগণিত দিয়ে।
julia> 2 + 1.3
3.3
আমরা একটি ইন্টিজার (Int64) আর একটি ফ্লোটিং-পয়েন্ট (Float64) সংখ্যা যোগ করেছি এবং ফলাফল পেয়েছি Float64। একইভাবে:
julia> nums = (3, 4.1, 1//4)
(3, 4.1, 1//4)
julia> typeof(nums)
Tuple{Int64, Float64, Rational{Int64}}
julia> [nums...]
3-element Vector{Float64}:
3.0
4.1
0.25
উপরের উদাহরণে একটি টিউপল প্রতিটি এলিমেন্টের টাইপ অক্ষুণ্ন রেখেছিল, কিন্তু ভেক্টরে রূপান্তর করলে সেগুলোর সবগুলোই একরকম Float64 হয়ে গেল।
Julia কম্পাইলার বোঝে কোন কোন টাইপ রূপান্তর সম্ভব: ইন্টিজার থেকে ফ্লোটে যাওয়ায় কোনো সমস্যা নেই, কিন্তু ফ্লোট থেকে ইন্টিজারে গেলে নির্ভুলতা কমে যায়, তাই একটি InexactError উত্থাপিত হয়।
টাইপ প্রোমোশন এক্সপ্রেশনের সব মানকে একটি কমন টাইপে রূপান্তর করে, যা সব ইনপুটের সাথে মানানসই হওয়ার জন্য যথেষ্ট নমনীয়।
একই রূপান্তর promote() ফাংশন দিয়ে স্পষ্টভাবেও করা যায়:
julia> promote(nums...)
(3.0, 4.1, 0.25)
সহজ টিউটোরিয়াল অনুশীলনী সমাধান করার জন্য টাইপ ইনফারেন্স আর টাইপ প্রোমোশনের ওপর ভরসা করা ঠিক আছে, কিন্তু বড় প্রোগ্রামের ক্ষেত্রে আপনার আরও নিখুঁত নিয়ন্ত্রণের দরকার হতে পারে।
বেশিরভাগ আধুনিক প্রসেসরে একটি ইন্টিজারের ডিফল্ট টাইপ Int64।
আমরা Numbers ধারণায় দেখেছি যে একটি মানকে ডিফল্ট নয় এমন একটি নির্দিষ্ট টাইপে রূপান্তর করা যায়।
julia> x = Int16(42)
42
julia> typeof(x)
Int16
তবে ভ্যারিয়েবল x-কে এখনও ভিন্ন একটি টাইপে আবার অ্যাসাইন করা যায়:
julia> x = "changed"
"changed"
julia> typeof(x)
String
এটাই type instability, যার বৈশিষ্ট্য:
এর বদলে :: অপারেটর ব্যবহার করে আমরা x-এর টাইপ নির্দিষ্ট করতে পারি:
julia> y::Int16 = 42
42
julia> typeof(y)
Int16
julia> y = "changed"
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Int16
The function `convert` exists, but no method is defined for this combination of argument types.
এখন y-এর টাইপ Int16, এবং সবসময় তাই থাকবে।
ফলে কম্পাইলার জানে এর জন্য কত বাইট আলাদা রাখতে হবে, আর বাকি কোডটুকু একটি স্থির টাইপের ওপর ভরসা করে অপটিমাইজ করতে পারে।
এ দিক থেকে ভ্যারিয়েবলটি অনেকটা C-এর মতো স্ট্যাটিকালি টাইপড ভাষার ভ্যারিয়েবলের মতোই।
উপরে বর্ণিত টাইপ অ্যাসাইনমেন্ট একটি ভ্যারিয়েবল অ্যাসাইনমেন্টের বাম দিকে ব্যবহৃত হয়, সেই ভ্যারিয়েবলের টাইপ সীমাবদ্ধ করতে।
একটি মান, বা মানে পরিণত হয় এমন কিছু, :: অপারেটরের সাথে ব্যবহার করা সাধারণত একটি অ্যাসারশন: মানটি অবশ্যই এই টাইপের হতে হবে, নইলে একটি এরর উত্থাপিত হওয়া উচিত।
julia> 42::Number
42
julia> "two"::Number
ERROR: TypeError: in typeassert, expected Number, got a value of type String
সাধারণত এটি একটি ফাংশনের রিটার্ন ভ্যালুর সাথে ব্যবহার করা হতে পারে, ফাংশনটি প্রত্যাশামতো আচরণ করল কি না তার একটি সহজ শেষ পরীক্ষা হিসেবে।
লক্ষ্য করুন, অন্য ধরনের অ্যাসারশনের জন্য @assert নামের একটি ম্যাক্রো আছে।
Int64, Int16, String, Char: এই টাইপগুলো আসলে "কোথা থেকে আসে"।
অনেক অবজেক্ট-ওরিয়েন্টেড (OO) ভাষায় প্রতিটি টাইপ একটি ক্লাস, সাবক্লাসিং তাদের একটি ক্লাস অনুক্রমে সাজায়, আর ক্লাস মেথডগুলো আচরণ নির্ধারণ করে।
Java আর Ruby এই প্যাটার্নের স্পষ্ট উদাহরণ, তবে Python-ও ভেতরে ভেতরে একই রকম।
Julia-র কোনো ক্লাস নেই।
ডকুমেন্টে বলা কারণটি হলো, OO-র বৈশিষ্ট্যগুলো JIT কম্পাইলারের কাজে বাধা দেয় এবং রানটাইম পারফরম্যান্সের ক্ষতি করে।
কে জানে, হয়তো তারাও এই উক্তিটি পড়েছিলেন:
"Object-oriented programming is an exceptionally bad idea which could only have originated in California."
এটি Edsger Dijkstra-কে আরোপিত, যিনি ১৯৫০-এর দশক থেকে কয়েক দশক ধরে একজন অসাধারণ কম্পিউটার বিজ্ঞানী ছিলেন (যদিও তিনি তাঁর প্রাণবন্ত আশাবাদ বা সূক্ষ্ম কূটনীতির জন্য সাধারণত পরিচিত ছিলেন না)।
তবুও, এই কোডটি দেখুন:
julia> y::Int16 = 42
42
julia> typeof(y)
Int16
julia> supertype(Int16)
Signed
julia> supertypes(Int16)
(Int16, Signed, Integer, Real, Number, Any)
কিছু বিবরণ যোগ করলে:
Int16 একটি টাইপ, আর আমরা এই টাইপের ভ্যারিয়েবল তৈরি করতে পারি।Int16 হলো Signed-এর একটি সাবটাইপ, আর supertype() ফাংশনটি আমাদের সেটি দেখায়।Integer, Real আর Number হয়ে ওপরে Any পর্যন্ত ওঠে, আর subtypes() আমাদের জন্য অনুক্রমের এই শাখাটি তালিকাবদ্ধ করবে।সব শাখাই Any-তে শেষ হয়, যার একটি অনন্য বৈশিষ্ট্য হলো এটি নিজেই নিজের সুপারটাইপ।
julia> supertypes(String)
(String, AbstractString, Any)
julia> supertype(Any)
Any
তাই Julia-র কোনো class অনুক্রম নেই, কিন্তু এর একটি type অনুক্রম আছে।
পুরো অনুক্রমটি দেখাতে গেলে একটি বিশাল বৃক্ষ পাওয়া যায়, যা সত্যিকার অর্থে দেখা অসম্ভব।
এর কিছু অংশ দেখা নিয়ে অনলাইনে আলোচনা হয়েছে।
একসময় আমরা বের করার চেষ্টা করব এটি কীভাবে কাজ করে, তবে তার আগে আরও অনেক কিছু ঘুরে দেখার আছে।
সাধারণ উপায়ে সমতা পরীক্ষা করতে আমরা typeof() ব্যবহার করতে পারি।
julia> typeof(11)
Int64
julia> typeof(11) == Int64
true
julia> typeof(11) == Number
false
টাইপের সমতা অবশ্যই নিখুঁত হতে হবে, কারণ এই ধরনের তুলনার টাইপ অনুক্রম সম্পর্কে কোনো ধারণা নেই।
আরও নমনীয়ভাবে, isa আমাদের বলে দেবে একটি মানের টাইপ তুলনার টাইপের সমান কি না, অথবা তার একটি সাবটাইপ কি না।
এটি ইনফিক্স বা ফাংশন, দুই রূপেই ব্যবহার করা যায়।
julia> 12 isa Int64
true
julia> 12 isa Number
true
julia> isa(12, Number)
true
julia> 12 isa String
false
লক্ষ্য করুন, isa বাঁ দিকে একটি value আশা করে, type নয়।
এইভাবে দুটি টাইপ তুলনা করতে গেলে অপ্রত্যাশিত ফলাফল আসবে।
সঠিক অপারেটরটি হলো <:, যা আমরা ভবিষ্যতের ধারণাগুলোতে আরও অনেকবার দেখব।
julia> Int64 isa Number ## Don't do this!
false
julia> Int64 <: Number
true
ফাংশনের ভেতরে প্রবাহ নিয়ন্ত্রণ করতে টাইপ পরীক্ষা ব্যবহার করা যায়, তবে আইডিয়োম্যাটিক Julia-তে এটি তুলনামূলকভাবে অস্বাভাবিক।
মাল্টিপল ডিসপ্যাচ ধারণায় আমরা দেখব, ফাংশনের আর্গুমেন্টে টাইপ যোগ করে Julia-র ডিসপ্যাচ ব্যবস্থাকে এসব লজিক সামলাতে দেওয়াই প্রায়ই বেশি কার্যকর। তবে মাল্টিপল ডিসপ্যাচে পৌঁছানোর আগে টাইপ-সম্পর্কিত আরও কয়েকটি ধারণা আমাদের আলোচনা করা দরকার।
আমরা দেখেছি টাইপ অনুক্রম একটি বৃক্ষ কাঠামো তৈরি করে (কম্পিউটার বিজ্ঞানের অর্থে, যার মূল থাকে সবার ওপরে)।
বৃক্ষের প্রতিটি বস্তু একটি node, আর এগুলোকে কয়েকটি শ্রেণিতে ভাগ করা যায়:
abstract।concrete।julia> subtypes(Integer) # an abstract type
3-element Vector{Any}:
Bool
Signed
Unsigned
julia> subtypes(Int64) # a concrete type
Type[]
এটি একটি গুরুত্বপূর্ণ পার্থক্য, কারণ কেবল কনক্রিট টাইপগুলোই ভ্যারিয়েবল হিসেবে instantiated হতে পারে।
julia> a::Int16 = 42
42
julia> typeof(a)
Int16
julia> b::Integer = 42
42
julia> typeof(b)
Int64
লক্ষ্য করুন, একটি অ্যাবস্ট্রাক্ট টাইপ ব্যবহার করতে গেলে (এই ক্ষেত্রে) কোনো এরর বার্তা আসে না, বরং কম্পাইলার একটি উপযুক্ত কনক্রিট টাইপ তৈরি করে: Integer-এর বদলে Int64।
অন্তত এটি ভ্যারিয়েবলকে ইন্টিজার ছাড়া অন্য কিছু অ্যাসাইন করা থেকে আটকায়:
julia> f::Integer = "hello"
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Integer
The function `convert` exists, but no method is defined for this combination of argument types.
তাই অ্যাবস্ট্রাক্ট টাইপ দিয়ে টাইপ অ্যাসাইনমেন্ট ভ্যারিয়েবলের টাইপের ওপর একটি সীমাবদ্ধতা আরোপ করে, যা (উপরের উদাহরণে) Integer-এর যেকোনো সাবটাইপ হতে পারে।
প্রোগ্রামিং ভাষার জগতে এটি বেশ অস্বাভাবিক: C-তে টাইপ অ্যাসাইনমেন্টের চেয়ে দুর্বল, আবার Python-এর সাম্প্রতিক সংস্করণগুলোর টাইপ হিন্টের চেয়ে শক্তিশালী।
isabstracttype() আর isconcretetype() ফাংশন দুটি দিয়ে পরীক্ষা করা যায়।
লক্ষ্য করুন, এরা একে অন্যের শুধু বিপরীত নয়: পরের একটি ধারণায় আমরা দেখব, কিছু টাইপ অ্যাবস্ট্রাক্টও নয় আবার কনক্রিটও নয়।
julia> isconcretetype(Integer), isabstracttype(Integer)
(false, true)
julia> isconcretetype(Int64), isabstracttype(Int64)
(true, false)
# Vector is neither
julia> isconcretetype(Vector), isabstracttype(Vector)
(false, false)
যদিও Vector কোনো কনক্রিট টাইপ নয়, এর এলিমেন্টগুলো কনক্রিট।
eltype() (এলিমেন্টের টাইপ) ফাংশনটি এই টাইপটি বের করে আনে:
julia> eltype([1, 2.3])
Float64