Досі в силабусі майже не було сказано про типи, але очевидно, що в 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: звідки ж ці типи «беруться».
У багатьох обʼєктно-орієнтованих (ОО) мовах кожен тип є класом, створення підкласів вишиковує їх в ієрархію класів, а методи класів визначають поведінку.
Java і Ruby - очевидні приклади цього підходу, але навіть Python усередині влаштований подібно.
У Julia немає класів.
Задокументована причина полягає в тому, що можливості ОО заважають JIT-компілятору та шкодять продуктивності під час виконання.
Хтозна, можливо, вони також читали цю цитату:
«Обʼєктно-орієнтоване програмування - винятково погана ідея, яка могла зародитися лише в Каліфорнії».
Її приписують Едсгеру Дейкстрі, блискучому науковцю в галузі інформатики, який працював кілька десятиліть починаючи з 1950-х (хоча загалом він не був відомий ані сонячним оптимізмом, ані тонкою дипломатичністю).
І все ж погляньмо на цей код:
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, і їх можна поділити на категорії:
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