Ти

Типи у Julia

1 вправа

Про концепцію Типи

Досі в силабусі майже не було сказано про типи, але очевидно, що в Julia вони є:

julia> vals = (42, 4.3, π, "hello", 'Q')
(42, 4.3, π, "hello", 'Q')

julia> typeof(vals)
Tuple{Int64, Float64, Irrational{:π}, String, Char}

Ми жодного разу не вказували типи, але Julia їх усе одно визначила.

  1. У Julia є types, і вони відіграють центральну роль у її дизайні.
  2. Зазвичай Julia може «вгадати» тип, скориставшись 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, і їх можна поділити на категорії:

  1. Вузли, що мають підтипи, називають abstract.
  2. Листові вузли, без підтипів, називають 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
Редагувати через GitHub Посилання відкривається в новому вікні або вкладці

Вивчити концепцію Типи