Spec-Zone.ru › Julia 0.5

Методы

Напомним из Функций, что функция — это объект, который сопоставляет кортеж аргументов со значением возврата или выбрасывает исключение, если соответствующее значение вернуть нельзя. Часто одна и та же концептуальная функция или операция реализуется совершенно по-разному для разных типов аргументов: сложение двух целых чисел сильно отличается от сложения двух чисел с плавающей точкой, и оба эти случая отличаются от сложения целого числа и числа с плавающей точкой. Несмотря на различия в реализации, все эти операции относятся к общему понятию «сложения». Соответственно, в Julia эти поведения принадлежат одному объекту: функции +.

Чтобы обеспечить плавную работу с множеством различных реализаций одного и того же понятия, функции не обязательно определять сразу, а можно определять по частям, предоставляя конкретное поведение для определенных комбинаций типов и количества аргументов. Определение одного из возможных поведений функции называется методом. До сих пор мы представляли только примеры функций, определенных с одним методом, применимым ко всем типам аргументов. Однако сигнатуры определений методов могут быть аннотированы, чтобы указать типы аргументов помимо их количества, и может быть предоставлено более одного определения метода. Когда функция применяется к определенному кортежу аргументов, применяется наиболее специфичный метод, применимый к этим аргументам. Таким образом, общее поведение функции является набором поведений ее различных определений методов. Если набор разработан хорошо, то даже если реализации методов могут быть совершенно разными, внешнее поведение функции будет казаться бесшовным и согласованным.

Выбор метода для выполнения при применении функции называется диспетчеризацией. Julia позволяет процессу диспетчеризации выбирать, какой из методов функции вызвать, основываясь на количестве заданных аргументов и типах всех аргументов функции. Это отличается от традиционных языков объектно-ориентированного программирования, где диспетчеризация происходит только по первому аргументу, который часто имеет специальный синтаксис аргумента, а иногда подразумевается, а не явно записан как аргумент. [1] Использование всех аргументов функции для выбора метода, который должен быть вызван, а не только первого, известно как многократная диспетчеризация. Многократная диспетчеризация особенно полезна для математического кода, где мало имеет смысла искусственно считать, что операции «принадлежат» одному аргументу больше, чем любому другому: принадлежит ли операция сложения в x + y к x больше, чем к y? Реализация математического оператора, как правило, зависит от типов всех его аргументов. Однако многократная диспетчеризация оказывается мощной и удобной парадигмой для структурирования и организации программ, и за пределами математических операций.

[1] Например, в C++ или Java при вызове метода obj.meth(arg1,arg2), объект obj «получает» вызов метода и неявно передается в метод через ключевое слово this, а не как явный аргумент метода. Когда текущий объект this является получателем вызова метода, его можно вообще опустить, написав просто meth(arg1,arg2), при этом this подразумевается как получающий объект.

Определение методов

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

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

julia> f(x::Float64, y::Float64) = 2x + y;

Это определение функции применяется только к вызовам, где 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;

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) at none:1
f(x::Number, y::Number) at none:1

что показывает, что f имеет два метода, один принимающий два аргумента типа Float64 и один принимающий аргументы типа Number. Он также указывает файл и строку, где были определены методы: поскольку эти методы были определены в REPL, мы получаем видимый номер строки none:1.

В отсутствие объявления типа с ::, тип параметра метода по умолчанию является Any, что означает, что он не ограничен, поскольку все значения в Julia являются экземплярами абстрактного типа Any. Таким образом, мы можем определить универсальный метод для f следующим образом:

julia> f(x,y) = println("Whoa there, Nelly.");

julia> f("foo", 1)
Whoa there, Nelly.

Этот универсальный метод менее специфичен, чем любое другое возможное определение метода для пары значений параметра, поэтому он вызывается только для пар аргументов, к которым не применимо другое определение метода.

Хотя это кажется простым понятием, многократная диспетчеризация по типам значений, пожалуй, является самой мощной и центральной функцией языка Julia. Основные операции, как правило, имеют десятки методов:

 julia> methods(+)
 # 166 methods for generic function "+":
 +(a::Float16, b::Float16) at float16.jl:136
 +(x::Float32, y::Float32) at float.jl:206
 +(x::Float64, y::Float64) at float.jl:207
 +(x::Bool, z::Complex{Bool}) at complex.jl:126
 +(x::Bool, y::Bool) at bool.jl:48
 +(x::Bool) at bool.jl:45
 +{T<:AbstractFloat}(x::Bool, y::T) at bool.jl:55
 +(x::Bool, z::Complex) at complex.jl:133
 +(x::Bool, A::AbstractArray{Bool,N<:Any}) at arraymath.jl:105
 +(x::Char, y::Integer) at char.jl:40
 +{T<:Union{Int128,Int16,Int32,Int64,Int8,UInt128,UInt16,UInt32,UInt64,UInt8}}(x::T, y::T) at int.jl:32
 +(z::Complex, w::Complex) at complex.jl:115
 +(z::Complex, x::Bool) at complex.jl:134
 +(x::Real, z::Complex{Bool}) at complex.jl:140
 +(x::Real, z::Complex) at complex.jl:152
 +(z::Complex, x::Real) at complex.jl:153
 +(x::Rational, y::Rational) at rational.jl:179
 ...
 +(a, b, c, xs...) at operators.jl:119

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

Неопределенности методов

Возможно определить набор методов функции таким образом, чтобы не было единственного наиболее специфичного метода, применимого к некоторым комбинациям аргументов:

julia> g(x::Float64, y) = 2x + y;

julia> g(x, y::Float64) = x + 2y;

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. Candidates:
  g(x, y::Float64) at none:1
  g(x::Float64, y) at none:1
 ...

Здесь вызов g(2.0, 3.0) может обрабатываться либо методом g(Float64, Any), либо методом g(Any, Float64), и ни один из них не является более специфичным, чем другой. В таких случаях Julia поднимает исключение MethodError, а не произвольно выбирает метод. Можно избежать неоднозначности методов, указав подходящий метод для пересекающегося случая:

julia> g(x::Float64, y::Float64) = 2x + 2y;

julia> g(x::Float64, y) = 2x + y;

julia> g(x, y::Float64) = x + 2y;

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{T}(x::T, y::T) = true;

julia> same_type(x,y) = false;

Первый метод применяется всякий раз, когда оба аргумента являются одного и того же конкретного типа, независимо от того, каким типом является этот тип, в то время как второй метод выполняет роль универсального метода, охватывая все остальные случаи. Таким образом, в целом это определяет булеву функцию, которая проверяет, являются ли ее два аргумента одного и того же типа:

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

Этот вид определения поведения функции посредством диспетчеризации довольно распространен — даже идиоматичен — в Julia. Параметры типа методов не ограничиваются использованием в качестве типов параметров: они могут использоваться в любом месте, где в сигнатуре функции или теле функции было бы значение. Вот пример, где параметр типа метода T используется как параметр типа для параметрического типа Vector{T} в сигнатуре метода:

julia> myappend{T}(v::Vector{T}, x::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{T}(::Array{T,1}, !Matched::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{T}(::Array{T,1}, !Matched::T) at none:1
...

Как видите, тип добавляемого элемента должен соответствовать типу элемента вектора, к которому он добавляется, в противном случае генерируется исключение MethodError. В следующем примере параметр типа метода T используется в качестве значения возврата:

julia> mytypeof{T}(x::T) = T
mytypeof (generic function with 1 method)

julia> mytypeof(1)
Int64

julia> mytypeof(1.0)
Float64

Так же, как вы можете добавить ограничения на подтипы для параметров типов в объявлениях типов (см. Параметрические типы), вы также можете добавить ограничения на параметры типов методов:

same_type_numeric{T<:Number}(x::T, y::T) = true
same_type_numeric(x::Number, y::Number) = false

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)
no method same_type_numeric(String,Float64)

julia> same_type_numeric("foo", "bar")
no method same_type_numeric(String,String)

julia> same_type_numeric(Int32(1), Int64(2))
false

Функция same_type_numeric ведет себя очень похоже на функцию same_type , определённую выше, но определена только для пар чисел.

Методы Varargs с параметрическими ограничениями

Параметры функции также могут использоваться для ограничения числа аргументов, которые могут быть переданы функции с «varargs» (Функции Varargs). Нотация Vararg{T,N} используется для обозначения такого ограничения. Например:

julia> bar(a,b,x::Vararg{Any,2}) = (a,b,x);

julia> bar(1,2,3)
ERROR: MethodError: no method matching bar(::Int64, ::Int64, ::Int64)
...

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)
...

Более полезно можно ограничить методы varargs параметром. Например:

function getindex{T,N}(A::AbstractArray{T,N}, indexes::Vararg{Number,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 «вызываемым», добавив методы к его типу. (Такие «вызываемые» объекты иногда называют «функторами».)

Например, можно определить тип, хранящий коэффициенты полинома, но ведёт себя как функция, вычисляющая полином:

immutable Polynomial{R}
    coeffs::Vector{R}
end

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
[Clarke61] Артур К. Кларк, Профили будущего (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.5/manual/methods/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API