Треки
/
Julia
Julia
/
Вправи
/
Гуру оцінювання
Гуру оцінювання

Гуру оцінювання

Навчальна вправа

Вступ

Досі в силабусі майже не йшлося про типи, але очевидно, що в 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-компілятор перегляне (увесь) код, побачить, як використовується змінна, і виведе відповідний типовий тип, сумісний із цим використанням.

Присвоєння типів

Покладатися на виведення типів цілком доречно, коли ми розвʼязуємо прості навчальні вправи, але для більших програм нам, найімовірніше, знадобиться точніший контроль.

На більшості сучасних процесорів ціле число типово має тип 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.

Ієрархія типів

Int64, Int16, String, Char: звідки ж ці типи беруться?

У багатьох обʼєктно-орієнтованих (ООП) мовах кожен тип є класом, а підкласи шикують їх в ієрархію класів, і методи класів визначають поведінку.

Java і Ruby - очевидні приклади цього підходу, але навіть Python усередині влаштований схоже.

У Julia немає класів.

Задокументована причина полягає в тому, що можливості ООП заважають JIT-компілятору та шкодять швидкодії під час виконання.

І попри це, погляньмо на цей код:

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

Абстрактні та конкретні типи

Ми бачили, що ієрархія типів утворює деревоподібну структуру (у сенсі інформатики, з коренем угорі).

Кожен елемент дерева - це 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.

Вказівки

У цій вправі ми трохи займемося інженерією даних, попередньо обробивши деякі дані з оцінками. Школа, у якій навчається дуже, дуже, дуже багато (забагато?) студентів, намагається проаналізувати отримані студентами оцінки й хоче зробити це ефективно, тож ідея в тому, щоб зменшити обсяг даних і впорядкувати їх.

Шкала оцінок зазвичай від 0 до 10, тож найефективніше використовувати тип даних, який займає лише вісім бітів на одну оцінку.

  • Деякі викладачі використовують дійсні числа (бо вони педантичні щодо точності), і нам потрібно буде перетворити їх на тип даних UInt8.
  • Інші викладачі використовують цілі числа з відʼємними значеннями (щоб карати особливо неслухняних студентів), і в таких випадках нам потрібно буде перетворити їх на тип даних Int8.

Коли ми реалізуємо функцію для перетворення оцінок, потрібно буде написати ще одну, яка оброблятиме колекції, у яких зберігаються оцінки, знижуючи їх і повертаючи у спадному порядку.

  • Деякі викладачі використовують Vector, відсортовані за зростанням, щоб зберігати оцінки, бо вони прискіпливо ставляться до своїх даних.
  • Інші викладачі використовують Set, які не відсортовані, щоб зберігати оцінки, бо вони трохи ледачіші.

В обох функціях нам потрібно буде обробити некоректні вхідні дані, викинувши MethodError.

Note

Обробку винятків буде розглянуто в пізнішому концепті, тож для цієї вправи можна скористатися таким синтаксисом:

throw(MethodError(f, args))

де f - це функція, а args - кортеж аргументів, переданих у функцію. Для функції demote(n) f - це demote, а args - це (n,):

throw(MethodError(demote, (n,)))

1. Знизьте оцінки

Реалізуйте метод demote(n).

  • Якщо на вхід надходить Float64, вона має округлити число вгору до найближчого цілого й повернути тип даних UInt8.
  • Якщо на вхід надходить довільне Integer, вона має повернути те саме ціле число з типом даних Int8.
  • Для всіх інших вхідних даних вона має викинути MethodError.
julia> demote(4.2)::UInt8
5

julia> demote(4)::Int8
4

julia> demote("hi")
MethodError: no method matching demote(::String)     #output truncated

2. Попередньо обробіть дані

Реалізуйте метод preprocess(coll).

  • Якщо на вхід надходить Vector, вона має знизити всі числа й перевернути вектор.
  • Якщо на вхід надходить Set, вона має знизити всі числа й повернути відсортований вектор у спадному порядку.
  • Для всіх інших вхідних даних вона має викинути MethodError.
julia> preprocess([1, 2, 3])
3-element Vector{Int8}:
 3
 2
 1

julia> preprocess(Set([2.2, 5.8, 3.4]))
3-element Vector{UInt8}:
 6
 4
 3

julia> preprocess(42)
MethodError: no method matching preprocess(::Int64)     #output truncated
Редагувати через GitHub Посилання відкривається в новому вікні або вкладці
Julia Exercism

Час розпочати Гуру оцінювання?

Зареєструйтеся на Exercism, щоб вивчати й опановувати Julia, а також 35 концепцій128 вправ та справжнє наставництво від людей, і все це безкоштовно.