На цій стадії силабусу ми вже бачили чимало визначень функцій. Зазвичай їх подавали без докладних пояснень щодо виводу в 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 незвичний, хоч і не унікальний.
+ наразі має 161 метод).... може частково це обійти).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))."
Спочатку ми будемо моделювати те, що відбувається, коли зустрічаються коти і собаки.
Спершу додайте абстрактний тип Pet.
Потім визначте типи Cat і Dog як підтипи Pet.
Кожен із них має одне поле name.
Реалізуйте функцію name(), яка повертає імʼя улюбленця.
julia> fido = Dog("Fido")
Dog("Fido")
julia> name(fido)
"Fido"
Реалізуйте методи meets() для таких зустрічей:
julia> ginger = Cat("Ginger")
Cat("Ginger")
julia> meets(ginger, fido)
"hisses"
Реалізуйте функцію encounter() з одним методом, щоб повідомляти, що відбувається, коли зустрічаються дві іменовані сутності.
julia> encounter(ginger, fido)
"Ginger meets Fido and hisses."
Що станеться, якщо під час прогулянки вони зустрінуть іншого улюбленця, як-от кінь?
julia> bella = Horse("Bella")
Horse("Bella")
julia> encounter(fido, bella)
"Fido meets Bella and is cautious."
Є багато інших речей, які улюбленці можуть зустріти на своєму шляху і які не є улюбленцями: машини, люди, рослини, стихійні лиха, астероїди… Що тоді відбувається?
julia> colnago = Bicycle("Colnago")
Bicycle("Colnago")
julia> encounter(ginger, colnago)
"Ginger meets Colnago and runs away."
У нашій симуляції може статися багато інших зустрічей. Машина зустрічає собаку, два електрони зустрічаються і взаємодіють…
Неможливо заздалегідь охопити всі зустрічі, тому ми реалізуємо загальний запасний варіант.
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.
Зареєструйтеся на Exercism, щоб вивчати й опановувати Julia, а також 35 концепцій128 вправ та справжнє наставництво від людей, і все це безкоштовно.