Методы
Напомним из раздела Функции, что функция — это объект, сопоставляющий кортеж аргументов со значением результата или выбрасывающий исключение, если соответствующее значение вернуть нельзя. Часто одна и та же концептуальная функция или операция реализуются совершенно по-разному для различных типов аргументов: сложение двух целых чисел сильно отличается от сложения двух чисел с плавающей запятой, которые, в свою очередь, отличаются от сложения целого числа и числа с плавающей запятой. Несмотря на различия в реализации, все эти операции попадают под общее понятие «сложения». Соответственно, в Julia эти поведения принадлежат одному объекту: функции +.
Чтобы обеспечить плавное использование многих различных реализаций одного и того же концепта, функции не обязательно определять сразу, а можно определять по частям, предоставляя конкретные поведения для определенных комбинаций типов и количества аргументов. Определение одного возможного поведения для функции называется методом. До сих пор мы рассматривали только примеры функций, определенных с одним методом, применимым ко всем типам аргументов. Однако сигнатуры определений методов могут быть аннотированы для указания типов аргументов помимо их количества, и может быть предоставлено более одного определения метода. Когда функция применяется к определенному кортежу аргументов, применяется наиболее специфичный метод, применимый к этим аргументам. Таким образом, общее поведение функции представляет собой мозаику из поведений ее различных определений методов. Если мозаика спроектирована хорошо, то, хотя реализации методов могут быть совершенно разными, внешнее поведение функции будет казаться бесшовным и согласованным.
Выбор метода, который нужно выполнить при применении функции, называется диспетчеризацией. Julia позволяет процессу диспетчеризации выбирать, какой из методов функции вызвать, основываясь на количестве аргументов и типах всех аргументов функции. Это отличается от традиционных языков объектно-ориентированного программирования, где диспетчеризация происходит только на основе первого аргумента, который часто имеет специальный синтаксис аргумента и иногда подразумевается, а не записывается явно как аргумент. [1] Использование всех аргументов функции для выбора метода, который следует вызвать, а не только первого, известно как множественная диспетчеризация. Множественная диспетчеризация особенно полезна для математического кода, где мало смысла искусственно считать, что операции «принадлежат» одному аргументу больше, чем другим: принадлежит ли операция сложения в x + y объекту x больше, чем объекту y? Реализация математического оператора, как правило, зависит от типов всех его аргументов. Однако даже за пределами математических операций множественная диспетчеризация оказывается мощной и удобной парадигмой для структурирования и организации программ.
Например, в C++ или Java в вызове метода, таком как obj.meth(arg1,arg2), объект obj «получает» вызов метода и неявно передается в метод через ключевое слово this, а не в качестве явного аргумента метода. Когда текущий объект this является получателем вызова метода, его можно вообще опустить, записав просто meth(arg1,arg2), при этом this подразумевается в качестве объекта-получателя.
Определение методов
До сих пор в наших примерах мы определяли только функции с одним методом, имеющим неограниченные типы аргументов. Такие функции ведут себя так же, как и в традиционных языках с динамической типизацией. Тем не менее, мы почти постоянно использовали множественную диспетчеризацию и методы, даже не осознавая этого: все стандартные функции и операторы Julia, такие как упомянутая функция +, имеют множество методов, определяющих их поведение для различных возможных комбинаций типов и количества аргументов.
При определении функции можно необязательно ограничивать типы параметров, к которым она применима, используя оператор утверждения типа ::, представленный в разделе Составные типы:
julia> f(x::Float64, y::Float64) = 2x + y f (generic function with 1 method)
Это определение функции применяется только к вызовам, где x и y являются значениями типа Float64:
julia> f(2.0, 3.0) 7.0
При применении ее к любым другим типам аргументов возникнет ошибка MethodError:
julia> f(2.0, 3)
ERROR: MethodError: no method matching f(::Float64, ::Int64)
Closest candidates are:
f(::Float64, !Matched::Float64) at none:1
julia> f(Float32(2.0), 3.0)
ERROR: MethodError: no method matching f(::Float32, ::Float64)
Closest candidates are:
f(!Matched::Float64, ::Float64) at none:1
julia> f(2.0, "3.0")
ERROR: MethodError: no method matching f(::Float64, ::String)
Closest candidates are:
f(::Float64, !Matched::Float64) at none:1
julia> f("2.0", "3.0")
ERROR: MethodError: no method matching f(::String, ::String)
Как видите, аргументы должны быть точно типа Float64. Другие числовые типы, такие как целые числа или 32-битные числа с плавающей запятой, не преобразуются автоматически в 64-битные числа с плавающей запятой, а строки не анализируются как числа. Поскольку Float64 является конкретным типом, а конкретные типы не могут быть унаследованы в Julia, такое определение может быть применено только к аргументам, которые точно являются типа Float64. Однако часто бывает полезно писать более общие методы, где объявленные типы параметров являются абстрактными:
julia> f(x::Number, y::Number) = 2x - y f (generic function with 2 methods) julia> f(2.0, 3) 1.0
Это определение метода применяется к любой паре аргументов, являющихся экземплярами Number. Они не должны быть одного типа, до тех пор, пока каждый из них является числовым значением. Задача обработки разнородных числовых типов делегируется арифметическим операциям в выражении 2x - y.
Чтобы определить функцию с несколькими методами, просто определяют функцию несколько раз с различными количествами и типами аргументов. Первое определение метода создаёт объект функции, а последующие определения методов добавляют новые методы к существующему объекту функции. При применении функции будет выполнен наиболее специфичный метод, соответствующий количеству и типам аргументов. Таким образом, два определения метода выше вместе определяют поведение для f над всеми парами экземпляров абстрактного типа Number — но с другим поведением, специфичным для пар значений Float64. Если один из аргументов является 64-битным числом с плавающей запятой, а другой нет, то метод f(Float64,Float64) не может быть вызван, и должен использоваться более общий метод f(Number,Number):
julia> f(2.0, 3.0) 7.0 julia> f(2, 3.0) 1.0 julia> f(2.0, 3) 1.0 julia> f(2, 3) 1
Определение 2x + y используется только в первом случае, а определение 2x - y используется во всех остальных. Никакого автоматического преобразования или приведения аргументов функции никогда не выполняется: все преобразования в Julia не являются магическими и полностью явными. Преобразование и продвижение, однако, демонстрирует, как продуманное применение достаточно продвинутых технологий может быть неотличимым от магии. [Clarke61]
Для значений, не являющихся числовыми, и для меньшего или большего количества аргументов функция f остаётся неопределенной, и её применение по-прежнему приведёт к ошибке MethodError:
julia> f("foo", 3)
ERROR: MethodError: no method matching f(::String, ::Int64)
Closest candidates are:
f(!Matched::Number, ::Number) at none:1
julia> f()
ERROR: MethodError: no method matching f()
Closest candidates are:
f(!Matched::Float64, !Matched::Float64) at none:1
f(!Matched::Number, !Matched::Number) at none:1
Вы легко можете увидеть, какие методы существуют для функции, введя сам объект функции в интерактивной сессии:
julia> f f (generic function with 2 methods)
Этот вывод говорит нам, что f — это объект функции с двумя методами. Чтобы узнать, каковы подписи этих методов, используйте функцию methods():
julia> methods(f) # 2 methods for generic function "f": f(x::Float64, y::Float64) in Main at none:1 f(x::Number, y::Number) in Main at none:1
что показывает, что у f есть два метода, один принимающий два аргумента типа Float64, а другой — аргументы типа Number. Также указаны файл и строка, где были определены методы: поскольку эти методы были определены в интерактивной сессии, мы получаем видимый номер строки none:1.
В отсутствие объявления типа с ::, тип параметра метода по умолчанию Any, что означает, что он не ограничен, так как все значения в Julia являются экземплярами абстрактного типа Any. Таким образом, мы можем определить универсальный метод для f следующим образом:
julia> f(x,y) = println("Whoa there, Nelly.")
f (generic function with 3 methods)
julia> f("foo", 1)
Whoa there, Nelly.
Этот универсальный метод менее специфичен, чем любое другое возможное определение метода для пары значений параметров, поэтому он будет вызван только для пар аргументов, к которым не применяются другие определения методов.
Хотя это кажется простым понятием, множественная диспетчеризация по типам значений, пожалуй, является самой мощной и центральной особенностью языка Julia. Основные операции обычно имеют десятки методов:
julia> methods(+)
# 180 methods for generic function "+":
+(x::Bool, z::Complex{Bool}) in Base at complex.jl:224
+(x::Bool, y::Bool) in Base at bool.jl:89
+(x::Bool) in Base at bool.jl:86
+(x::Bool, y::T) where T<:AbstractFloat in Base at bool.jl:96
+(x::Bool, z::Complex) in Base at complex.jl:231
+(a::Float16, b::Float16) in Base at float.jl:372
+(x::Float32, y::Float32) in Base at float.jl:374
+(x::Float64, y::Float64) in Base at float.jl:375
+(z::Complex{Bool}, x::Bool) in Base at complex.jl:225
+(z::Complex{Bool}, x::Real) in Base at complex.jl:239
+(x::Char, y::Integer) in Base at char.jl:40
+(c::BigInt, x::BigFloat) in Base.MPFR at mpfr.jl:303
+(a::BigInt, b::BigInt, c::BigInt, d::BigInt, e::BigInt) in Base.GMP at gmp.jl:303
+(a::BigInt, b::BigInt, c::BigInt, d::BigInt) in Base.GMP at gmp.jl:296
+(a::BigInt, b::BigInt, c::BigInt) in Base.GMP at gmp.jl:290
+(x::BigInt, y::BigInt) in Base.GMP at gmp.jl:258
+(x::BigInt, c::Union{UInt16, UInt32, UInt64, UInt8}) in Base.GMP at gmp.jl:315
...
+(a, b, c, xs...) at operators.jl:119
Множественная диспетчеризация вместе с гибкой параметрической системой типов обеспечивают Julia возможность абстрактно выражать алгоритмы высокого уровня, отвязавшись от деталей реализации, но при этом генерировать эффективный специализированный код для каждого случая во время выполнения.
Неопределённости методов
Возможна ситуация, когда множество определений методов функции таково, что для некоторых комбинаций аргументов нет уникального наиболее специфичного метода:
julia> g(x::Float64, y) = 2x + y g (generic function with 1 method) julia> g(x, y::Float64) = x + 2y g (generic function with 2 methods) julia> g(2.0, 3) 7.0 julia> g(2, 3.0) 8.0 julia> g(2.0, 3.0) ERROR: MethodError: g(::Float64, ::Float64) is ambiguous. [...]
Здесь вызов g(2.0, 3.0) может быть обработан либо методом g(Float64, Any), либо методом g(Any, Float64), и ни один из них не является более специфичным, чем другой. В таких случаях Julia поднимает ошибку MethodError, а не произвольно выбирает метод. Вы можете избежать неопределённостей методов, указав соответствующий метод для пересечения случаев:
julia> g(x::Float64, y::Float64) = 2x + 2y g (generic function with 3 methods) julia> g(2.0, 3) 7.0 julia> g(2, 3.0) 8.0 julia> g(2.0, 3.0) 10.0
Рекомендуется, чтобы различающий метод был определен первым, так как в противном случае неопределенность существует, хотя бы временно, до определения более специфичного метода.
В более сложных случаях разрешение неоднозначностей методов требует определенного дизайнерского подхода; эта тема рассматривается подробнее ниже.
Параметрические методы
Определения методов могут необязательно содержать параметры типа, квалифицирующие сигнатуру:
julia> same_type(x::T, y::T) where {T} = true
same_type (generic function with 1 method)
julia> same_type(x,y) = false
same_type (generic function with 2 methods)
Первый метод применяется, когда оба аргумента имеют один и тот же конкретный тип, независимо от того, какой это тип, а второй метод действует как универсальный, охватывающий все остальные случаи. Таким образом, в целом, это определяет булеву функцию, которая проверяет, являются ли её два аргумента одного типа:
julia> same_type(1, 2)
true
julia> same_type(1, 2.0)
false
julia> same_type(1.0, 2.0)
true
julia> same_type("foo", 2.0)
false
julia> same_type("foo", "bar")
true
julia> same_type(Int32(1), Int64(2))
false
Такие определения соответствуют методам, сигнатуры типов которых являются UnionAll типами (см. UnionAll Типы).
Этот вид определения поведения функции с помощью диспетчеризации довольно распространён — даже типичен — в Julia. Параметры типа методов не ограничиваются использованием в качестве типов аргументов: они могут быть использованы в любом месте, где требуется значение в сигнатуре функции или в теле функции. Вот пример, где параметр типа метода T используется как параметр типа для параметрического типа Vector{T} в сигнатуре метода:
julia> myappend(v::Vector{T}, x::T) where {T} = [v..., x]
myappend (generic function with 1 method)
julia> myappend([1,2,3],4)
4-element Array{Int64,1}:
1
2
3
4
julia> myappend([1,2,3],2.5)
ERROR: MethodError: no method matching myappend(::Array{Int64,1}, ::Float64)
Closest candidates are:
myappend(::Array{T,1}, !Matched::T) where T at none:1
julia> myappend([1.0,2.0,3.0],4.0)
4-element Array{Float64,1}:
1.0
2.0
3.0
4.0
julia> myappend([1.0,2.0,3.0],4)
ERROR: MethodError: no method matching myappend(::Array{Float64,1}, ::Int64)
Closest candidates are:
myappend(::Array{T,1}, !Matched::T) where T at none:1
Как вы видите, тип добавляемого элемента должен совпадать с типом элемента вектора, к которому он добавляется, иначе возникает MethodError. В следующем примере параметр типа метода T используется в качестве возвращаемого значения:
julia> mytypeof(x::T) where {T} = T
mytypeof (generic function with 1 method)
julia> mytypeof(1)
Int64
julia> mytypeof(1.0)
Float64
Так же, как вы можете установить ограничения на подтипы для параметров типа в объявлениях типов (см. Параметрические типы), вы также можете ограничивать параметры типов методов:
julia> same_type_numeric(x::T, y::T) where {T<:Number} = true
same_type_numeric (generic function with 1 method)
julia> same_type_numeric(x::Number, y::Number) = false
same_type_numeric (generic function with 2 methods)
julia> same_type_numeric(1, 2)
true
julia> same_type_numeric(1, 2.0)
false
julia> same_type_numeric(1.0, 2.0)
true
julia> same_type_numeric("foo", 2.0)
ERROR: MethodError: no method matching same_type_numeric(::String, ::Float64)
Closest candidates are:
same_type_numeric(!Matched::T<:Number, ::T<:Number) where T<:Number at none:1
same_type_numeric(!Matched::Number, ::Number) at none:1
julia> same_type_numeric("foo", "bar")
ERROR: MethodError: no method matching same_type_numeric(::String, ::String)
julia> same_type_numeric(Int32(1), Int64(2))
false
Функция same_type_numeric ведет себя очень похоже на функцию same_type , определённую выше, но она определена только для пар чисел.
Параметрические методы позволяют использовать тот же синтаксис, что и выражения where , используемые для записи типов (см. Типы UnionAll). Если есть только один параметр, фигурные скобки (в where {T}) можно опустить, но их часто предпочитают для большей ясности. Несколько параметров можно разделить запятыми, например, where {T, S<:Real}, или записать с использованием вложенных where, например, where S<:Real where T.
Переопределение методов
При переопределении метода или добавлении новых методов важно понимать, что эти изменения не вступают в силу немедленно. Это ключевой момент для способности Julia статически выводить и компилировать код для быстрой работы без обычных хитростей JIT и накладных расходов. Действительно, любое новое определение метода не будет видно текущей среде выполнения, включая задачи и потоки (и любые ранее определённые @generated функции). Давайте начнём с примера, чтобы увидеть, что это означает:
julia> function tryeval()
@eval newfun() = 1
newfun()
end
tryeval (generic function with 1 method)
julia> tryeval()
ERROR: MethodError: no method matching newfun()
The applicable method may be too new: running in world age xxxx1, while current world is xxxx2.
Closest candidates are:
newfun() at none:1 (method too new to be called from this world context.)
in tryeval() at none:1
...
julia> newfun()
1
В этом примере обратите внимание, что новое определение для newfun было создано, но не может быть немедленно вызвано. Новый глобальный объект немедленно виден функции tryeval, поэтому вы можете написать return newfun (без скобок). Но ни вы, ни кто-либо из ваших вызывающих функций, ни функции, которые они вызывают, и т.д. не могут вызвать это новое определение метода!
Но есть исключение: будущие вызовы к newfun из REPL работают как ожидается, имея возможность и увидеть, и вызвать новое определение newfun.
Однако, будущие вызовы к tryeval будут продолжать видеть определение newfun таким, каким оно было на предыдущей строке в REPL, и, следовательно, до этого вызова tryeval.
Вы можете попробовать это самостоятельно, чтобы увидеть, как это работает.
Реализация этого поведения — это «счётчик возраста мира». Это монотонно возрастающее значение отслеживает каждую операцию определения метода. Это позволяет описать «набор определений методов, видимых данной среде выполнения», как одно число или «возраст мира». Это также позволяет сравнивать методы, доступные в двух мирах, просто сравнивая их порядковый номер. В примере выше мы видим, что «текущий мир» (в котором существует метод newfun()), на единицу больше, чем локальный для задачи «мир выполнения», который был фиксирован при запуске выполнения tryeval.
Иногда необходимо обойти это (например, если вы реализуете вышеупомянутый REPL). К счастью, есть простое решение: вызовите функцию с помощью Base.invokelatest:
julia> function tryeval2()
@eval newfun2() = 2
Base.invokelatest(newfun2)
end
tryeval2 (generic function with 1 method)
julia> tryeval2()
2
Наконец, давайте рассмотрим несколько более сложных примеров, где это правило играет роль. Определите функцию f(x), которая изначально имеет один метод:
julia> f(x) = "original definition" f (generic function with 1 method)
Начните другие операции, которые используют f(x):
julia> g(x) = f(x) g (generic function with 1 method) julia> t = @async f(wait()); yield();
Теперь добавим новые методы к f(x):
julia> f(x::Int) = "definition for Int"
f (generic function with 2 methods)
julia> f(x::Type{Int}) = "definition for Type{Int}"
f (generic function with 3 methods)
Сравните, как эти результаты различаются:
julia> f(1) "definition for Int" julia> g(1) "definition for Int" julia> wait(schedule(t, 1)) "original definition" julia> t = @async f(wait()); yield(); julia> wait(schedule(t, 1)) "definition for Int"
Параметрически ограниченные методы с произвольным количеством аргументов
Параметры функции также могут использоваться для ограничения количества аргументов, которые могут быть переданы в функцию с произвольным количеством аргументов («varargs») (Функции с произвольным количеством аргументов). Обозначение Vararg{T,N} используется для указания такого ограничения. Например:
julia> bar(a,b,x::Vararg{Any,2}) = (a,b,x)
bar (generic function with 1 method)
julia> bar(1,2,3)
ERROR: MethodError: no method matching bar(::Int64, ::Int64, ::Int64)
Closest candidates are:
bar(::Any, ::Any, ::Any, !Matched::Any) at none:1
julia> bar(1,2,3,4)
(1, 2, (3, 4))
julia> bar(1,2,3,4,5)
ERROR: MethodError: no method matching bar(::Int64, ::Int64, ::Int64, ::Int64, ::Int64)
Closest candidates are:
bar(::Any, ::Any, ::Any, ::Any) at none:1
Более полезно, можно ограничить методы с произвольным количеством аргументов параметром. Например:
function getindex(A::AbstractArray{T,N}, indexes::Vararg{Number,N}) where {T,N}
будет вызвана только тогда, когда количество indexes совпадает с размерностью массива.
Примечание об аргументах по умолчанию и ключевых аргументах
Как кратко упоминалось в Функции, аргументы по умолчанию реализуются как синтаксис для нескольких определений методов. Например, это определение:
f(a=1,b=2) = a+2b
переводится в следующие три метода:
f(a,b) = a+2b f(a) = f(a,2) f() = f(1,2)
Это означает, что вызов f() эквивалентен вызову f(1,2). В этом случае результат 5, потому что f(1,2) вызывает первый метод f выше. Однако это не всегда так. Если вы определите четвёртый метод, более специализированный для целых чисел:
f(a::Int,b::Int) = a-2b
то результат как f() , так и f(1,2) — это -3. Другими словами, аргументы по умолчанию связаны с функцией, а не с каким-либо конкретным методом этой функции. От типов аргументов по умолчанию зависит, какой метод вызывается. Когда аргументы по умолчанию определены в терминах глобальной переменной, тип аргумента по умолчанию может даже изменяться во время выполнения.
Ключевые аргументы ведут себя совершенно иначе, чем обычные позиционные аргументы. В частности, они не участвуют в диспетчеризации методов. Методы диспетчеризируются только на основе позиционных аргументов, а ключевые аргументы обрабатываются после определения соответствующего метода.
Объекты, подобные функциям
Методы связаны с типами, поэтому любой произвольный объект Julia можно сделать «вызываемым», добавив методы к его типу. (Такие «вызываемые» объекты иногда называют «функторами».)
Например, вы можете определить тип, хранящий коэффициенты многочлена, но ведёт себя как функция, вычисляющая многочлен:
julia> struct Polynomial{R}
coeffs::Vector{R}
end
julia> function (p::Polynomial)(x)
v = p.coeffs[end]
for i = (length(p.coeffs)-1):-1:1
v = v*x + p.coeffs[i]
end
return v
end
Обратите внимание, что функция задаётся типом, а не именем. В теле функции p будет ссылаться на объект, который был вызван. Объект Polynomial может быть использован следующим образом:
julia> p = Polynomial([1,10,100])
Polynomial{Int64}([1, 10, 100])
julia> p(3)
931
Этот механизм также является ключом к тому, как работают конструкторы типов и замыкания (внутренние функции, которые ссылаются на свою окружающую среду) в Julia, обсуждаемые позже в руководстве.
Пустые обобщённые функции
Иногда полезно ввести обобщённую функцию без добавления методов. Это может быть использовано для разделения определений интерфейсов от реализаций. Это также может быть сделано для документации или читабельности кода. Синтаксис для этого — пустой блок function без кортежа аргументов:
function emptyfunc end
Проектирование методов и избегание неоднозначностей
Полиморфизм методов Julia — одно из его самых мощных свойств, но использование этой мощности может создать проблемы с проектированием. В частности, в более сложных иерархиях методов неоднозначность возникает довольно часто.
Выше было отмечено, что можно разрешить неоднозначность, например
f(x, y::Int) = 1 f(x::Int, y) = 2
определив метод
f(x::Int, y::Int) = 3
Это часто правильная стратегия; однако существуют обстоятельства, когда слепое следование этому совету может быть неэффективным. В частности, чем больше методов у обобщённой функции, тем больше возможностей для неоднозначности. Когда ваши иерархии методов становятся сложнее, чем этот простой пример, может быть полезно тщательно продумать альтернативные стратегии.
Ниже мы обсудим конкретные проблемы и некоторые альтернативные способы решения таких проблем.
Аргументы Tuple и NTuple
Аргументы Tuple (и NTuple) представляют собой особые сложности. Например,
f(x::NTuple{N,Int}) where {N} = 1
f(x::NTuple{N,Float64}) where {N} = 2
имеют неоднозначность из-за возможности N == 0: нет элементов, чтобы определить, какой вариант Int или Float64 должен быть вызван. Для разрешения неоднозначности один подход — определить метод для пустого кортежа:
f(x::Tuple{}) = 3
В качестве альтернативы, для всех методов, кроме одного, можно потребовать, чтобы в кортеже был хотя бы один элемент:
f(x::NTuple{N,Int}) where {N} = 1 # this is the fallback
f(x::Tuple{Float64, Vararg{Float64}}) = 2 # this requires at least one Float64
Ортогонализируйте свой дизайн
Когда вы, возможно, захотите выполнить диспетчеризацию по двум или более аргументам, подумайте, не привело ли бы создание «обёртки» функции к более простому проектированию. Например, вместо написания нескольких вариантов:
f(x::A, y::A) = ... f(x::A, y::B) = ... f(x::B, y::A) = ... f(x::B, y::B) = ...
вы можете рассмотреть определение
f(x::A, y::A) = ... f(x, y) = f(g(x), g(y))
где g преобразует аргумент к типу A. Это очень специфический пример более общего принципа ортогонального проектирования, в котором отдельные концепции назначаются отдельным методам. Здесь g скорее всего потребует определения по умолчанию
g(x::A) = x
Похожая стратегия использует promote для приведения x и y к общему типу:
f(x::T, y::T) where {T} = ...
f(x, y) = f(promote(x, y)...)
Одна из опасностей этого проектирования заключается в возможности того, что если нет подходящего метода преобразования, преобразующего x и y к одному типу, второй метод будет рекурсивно вызывать себя бесконечно и вызовет переполнение стека. Неэкспортируемая функция Base.promote_noncircular может использоваться как альтернатива; при неудаче преобразования она всё равно сгенерирует ошибку, но ошибку, которая произойдёт быстрее с более конкретным сообщением об ошибке.
Диспетчеризация по одному аргументу за раз
Если вам нужно выполнять диспетчеризацию по нескольким аргументам, и существует много обратных вызовов с слишком многими комбинациями, чтобы сделать практичным определение всех возможных вариантов, то подумайте о введении «каскада имён», где (например), вы выполняете диспетчеризацию по первому аргументу, а затем вызываете внутренний метод:
f(x::A, y) = _fA(x, y) f(x::B, y) = _fB(x, y)
Тогда внутренние методы _fA и _fB могут выполнять диспетчеризацию по y без опасений по поводу неоднозначностей друг с другом относительно x.
Заметьте, что у этой стратегии есть как минимум один существенный недостаток: во многих случаях пользователи не могут дальше настроить поведение f путём определения дальнейших специализаций вашей экспортируемой функции f. Вместо этого им придётся определять специализации для ваших внутренних методов _fA и _fB, и это размывает границы между экспортируемыми и внутренними методами.
Абстрактные контейнеры и типы элементов
По возможности старайтесь избегать определения методов, которые выполняют диспетчеризацию по конкретным типам элементов абстрактных контейнеров. Например,
-(A::AbstractArray{T}, b::Date) where {T<:Date}
создаёт неоднозначность для любого, кто определяет метод
-(A::MyArrayType{T}, b::T) where {T}
Лучший подход — избегать определения любого из этих методов: вместо этого полагайтесь на универсальный метод -(A::AbstractArray, b) и убедитесь, что этот метод реализован с помощью универсальных вызовов (например, similar и -), которые выполняют правильные действия для каждого типа контейнера и типа элемента отдельно. Это просто более сложная вариация совета о ортогонализации ваших методов.
Когда этот подход невозможен, может стоить начать обсуждение с другими разработчиками о разрешении неоднозначности; просто потому, что один метод был определён первым, не обязательно означает, что его нельзя изменить или удалить. В крайнем случае, один разработчик может определить «метод-пластырь»
-(A::MyArrayType{T}, b::Date) where {T<:Date} = ...
который устраняет неоднозначность путём грубой силы.
Сложный метод «каскад» с аргументами по умолчанию
Если вы определяете метод «каскад», который предоставляет значения по умолчанию, будьте внимательны к удалению любых аргументов, соответствующих потенциальным значениям по умолчанию. Например, предположим, что вы пишете алгоритм цифровой фильтрации и у вас есть метод, который обрабатывает края сигнала, применяя заполнение:
function myfilter(A, kernel, ::Replicate)
Apadded = replicate_edges(A, size(kernel))
myfilter(Apadded, kernel) # now perform the "real" computation
end
Это приведёт к конфликту с методом, который предоставляет значения по умолчанию для заполнения:
myfilter(A, kernel) = myfilter(A, kernel, Replicate()) # replicate the edge by default
Вместе эти два метода создают бесконечную рекурсию с A постоянно увеличивающимся размером.
Лучший дизайн — определить вашу иерархию вызовов следующим образом:
struct NoPad end # indicate that no padding is desired, or that it's already applied
myfilter(A, kernel) = myfilter(A, kernel, Replicate()) # default boundary conditions
function myfilter(A, kernel, ::Replicate)
Apadded = replicate_edges(A, size(kernel))
myfilter(Apadded, kernel, NoPad()) # indicate the new boundary conditions
end
# other padding methods go here
function myfilter(A, kernel, ::NoPad)
# Here's the "real" implementation of the core computation
end
NoPad передаётся в той же позиции аргумента, что и любой другой вид заполнения, поэтому он поддерживает хорошо организованную иерархию диспетчеризации с уменьшенной вероятностью неоднозначностей. Более того, он расширяет «публичный» myfilter интерфейс: пользователь, который хочет явно контролировать заполнение, может вызвать NoPad версию напрямую.
Артур К. Кларк, Профили будущего (1961): Третий закон Кларка.
© 2009–2016 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/release-0.6/manual/methods/