تا اینجا، سرفصل چیز زیادی دربارهی انواع نگفته است، اما آشکارا در 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 کلاس ندارد.
دلیل مکتوب آن این است که ویژگیهای شیءگرایی با کامپایلر JIT تداخل میکنند و به کارایی زمان اجرا آسیب میزنند.
چه میدانیم، شاید آنها هم این نقلقول را خوانده بودند:
«برنامهنویسی شیءگرا ایدهی فوقالعاده بدی است که فقط میتوانست در کالیفرنیا پدید بیاید.»
این جمله به 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
توجه کنید که تلاش برای استفاده از یک نوع انتزاعی (در این مورد) هیچ پیام خطایی نمیدهد، اما کامپایلر یک نوع عینی مناسب میسازد: Int64 بهجای Integer.
حداقل جلوی انتساب متغیر به چیزی غیر از عدد صحیح را میگیرد:
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