Треки
/
Julia
Julia
/
Вправи
/
Зустрічі
Зустрічі

Зустрічі

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

Вступ

На цій стадії силабусу ми вже бачили чимало визначень функцій. Зазвичай їх подавали без докладних пояснень щодо виводу в REPL:

julia> f(x) = x^2
f (generic function with 1 method)

Чому «generic function with 1 method»?

Припустімо, ми хочемо точніше вказати типи аргументів.

julia> f(x::Integer) = x^2
f (generic function with 2 methods)

julia> f(x::Real) = x^2
f (generic function with 3 methods)

Або можемо змінити кількість аргументів:

julia> f(x, y) = x * y
f (generic function with 4 methods)

Виходить, що нові визначення тієї самої функції називають «методами». Методи додаються до списку, а не перезаписують старе визначення.

Ми легко можемо перелічити методи:

julia> methods(f)  # output edited for brevity
# 4 methods for generic function "f" from Main:
 [1] f(x, y)
 [2] f(x::Integer)
 [3] f(x::Real)
 [4] f(x)

Коли ми викликаємо функцію f, Julia якось знає, який метод використати:

julia> f(2.5)  # Float64
6.25

julia> f(2)  # Int64
4

julia> f(2, 3)  # Int64, Int64
6

Що таке функція?

Згідно з посібником, функція - це обʼєкт, який відображає кортеж аргументів у повернене значення.

Ми зазвичай уявляємо аргументи функції в f(2, 3) як два окремі елементи, але компілятор бачить їх як один кортеж. Це правильно навіть для одного аргументу (щоправда, щоб це було очевидніше, треба додати кому):

julia> typeof( (2, 3) )
Tuple{Int64, Int64}

julia> typeof( (2,) )
Tuple{Int64}

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

Як функції працюють із різною кількістю та типами аргументів?

Зазвичай це називають «поліморфізмом».

Це велика тема в інформатиці вже кілька десятиліть, і тут застосовували різні підходи.

Підхід Julia незвичний, хоч і не унікальний.

  • Функції «generic», тобто до них можна приєднати кілька методів.
  • Функції - це лише назви, доки до них не приєднано методів.
  • Методи можна додавати за потреби, і їх може бути дуже багато (оператор + наразі має 161 метод).
  • Коли функцію викликають, Julia досліджує сигнатуру виклику: кількість аргументів і поточний тип кожного з них.
  • Обирається метод, який найточніше відповідає сигнатурі виклику.
    • Загалом кількість аргументів має збігатися точно (щоправда, оператор splat/slurp ... може частково це обійти).
    • Перевагу віддають методам, типи яких точно збігаються із сигнатурою виклику.
    • За потреби Julia переглядає супертипи кожного аргументу в сигнатурі виклику, щоб знайти метод, який найточніше й найконкретніше підходить.
  • Обраний метод диспетчеризується (dispatched) із заданими аргументами й повертає результат.
  • Якщо відповідного методу не знайдено, виникає MethodError.

Зауважмо, що Julia має динамічну типізацію, тож диспетчеризація методів відбувається під час виконання.

Множинна диспетчеризація: приклад

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

Почнімо з визначення кількох власних типів, які представляють геометричні фігури.

julia> abstract type Shape end

julia> struct Circle <: Shape
           radius::Float64
       end

julia> struct Square <: Shape
           side::Float64
       end

julia> struct Rectangle <: Shape
           length::Float64
           width::Float64
       end

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

julia> area(c::Circle) = π * c.radius^2
area (generic function with 1 method)

julia> area(s::Square) = s.side^2
area (generic function with 2 methods)

julia> area(r::Rectangle) = r.length * r.width
area (generic function with 3 methods)

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

julia> circle = Circle(2.3)
Circle(2.3)

julia> area(circle)  # call signature Tuple{Circle}
16.619025137490002

julia> square = Square(1.6)
Square(1.6)

julia> area(square)  # call signature Tuple{Square}
2.5600000000000005

julia> rectangle = Rectangle(1.4, 2.1)
Rectangle(1.4, 2.1)

julia> area(rectangle)  # call signature Tuple{Rectangle}
2.94

Параметричні методи

Сигнатури методів можуть приймати фіктивні значення типів, приблизно так само, як параметричні типи.

Синтаксис тут особливий: потрібна конструкція where:

julia> same_type(v::Vector{T}, x::T) where {T} = true
same_type (generic function with 1 method)

julia> same_type([1, 2], 3)
true

julia> same_type([1, 2], "three")
ERROR: MethodError: no method matching same_type(::Vector{Int64}, ::String)

У наведеному прикладі обидва аргументи мають бути одного типу T, яким би він не був.

Можна бути конкретнішими, наклавши обмеження на T:

julia> same_type(v::Vector{T}, x::T) where {T<:Number} = true
same_type (generic function with 2 methods)

З цим методом true повертають лише однакові числові типи.

Можна вказати кілька фіктивних змінних, розділених комами в конструкції where.

julia> myfunc(a::S, b::T) where {S, T} = true
myfunc (generic function with 1 method)

Висновок

Множинна диспетчеризація дуже гнучка й надзвичайно продуктивна в Julia, тому її широко застосовують.

Як показує приклад зі 161 методом для +, програмісти Julia прагнуть визначати методи з вузько визначеними типами. Додавання двох цілих чисел на рівні байт-коду дуже відрізняється від додавання двох чисел з плаваючою комою, а додавання цілого числа до числа з плаваючою комою додає ще одне ускладнення: підвищення типів.

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

Якщо ми знаємо, які вхідні дані отримає метод, скажімо про це компілятору!

Вказівки

У цій вправі ми будемо реалізовувати симуляцію зустрічей. Це ознайомить нас з основами множинної диспетчеризації, головної парадигми Julia.

For this exercise, all function definitions should be a single-line declaration with no if/else logic.

Загалом зустріч передбачає, що одна сутність a зустрічає іншу сутність b і реагує на неї.

encounter(a, b) = "$(name(a)) meets $(name(b)) and $(meets(a, b))."

Спочатку ми будемо моделювати те, що відбувається, коли зустрічаються коти і собаки.

1. Визначте власні типи

Спершу додайте абстрактний тип Pet.

Потім визначте типи Cat і Dog як підтипи Pet. Кожен із них має одне поле name.

2. Отримайте імʼя улюбленця.

Реалізуйте функцію name(), яка повертає імʼя улюбленця.

julia> fido = Dog("Fido")
Dog("Fido")

julia> name(fido)
"Fido"

3. Визначте, що відбувається, коли зустрічаються коти і собаки

Реалізуйте методи meets() для таких зустрічей:

  • Коли зустрічаються два собаки, вони sniff одне одного.
  • Коли кіт зустрічає собаку, він hisses на собаку.
  • Коли собака зустрічає кота, вона chases кота.
  • Коли зустрічаються два коти, вони slink.
julia> ginger = Cat("Ginger")
Cat("Ginger")

julia> meets(ginger, fido)
"hisses"

4. Визначте зустріч між двома сутностями.

Реалізуйте функцію encounter() з одним методом, щоб повідомляти, що відбувається, коли зустрічаються дві іменовані сутності.

julia> encounter(ginger, fido)
"Ginger meets Fido and hisses."

5. Визначте запасну реакцію для зустрічей між улюбленцями

Що станеться, якщо під час прогулянки вони зустрінуть іншого улюбленця, як-от кінь?

  • Якщо улюбленець зустрічає іншого улюбленця, якого не впізнає, він is cautious.
julia> bella = Horse("Bella")
Horse("Bella")

julia> encounter(fido, bella)
"Fido meets Bella and is cautious."

6. Визначте запасний варіант, якщо улюбленець зустрічає щось незнайоме

Є багато інших речей, які улюбленці можуть зустріти на своєму шляху і які не є улюбленцями: машини, люди, рослини, стихійні лиха, астероїди… Що тоді відбувається?

  • Якщо улюбленець зустрічає щось, чого ніколи раніше не бачив, він runs away.
julia> colnago = Bicycle("Colnago")
Bicycle("Colnago")

julia> encounter(ginger, colnago)
"Ginger meets Colnago and runs away."

7. Визначте загальний запасний варіант

У нашій симуляції може статися багато інших зустрічей. Машина зустрічає собаку, два електрони зустрічаються і взаємодіють…

Неможливо заздалегідь охопити всі зустрічі, тому ми реалізуємо загальний запасний варіант.

  • Якщо дві невідомі речі або істоти зустрічаються, nothing happens.
julia> γ1 = Photon("γ1")
Photon("γ1")

julia> γ2 = Photon("γ2")
Photon("γ2")

julia> encounter(γ1, γ2)
"γ1 meets γ2 and nothing happens." 
# This is true, photons just pass through each other.
Редагувати через GitHub Посилання відкривається в новому вікні або вкладці
Julia Exercism

Час розпочати Зустрічі?

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