Функции

В этой главе количество текста от моего имени будет стремиться к нулю. Это свободный перевод главы по функциям из документации (http://docs.julialang.org/en/stable/manual/functions/).

Базовый синтаксис определения функций такой:

function f(x,y)
  x + y
end

Есть более краткий синтаксис:

f(x,y) = x + y

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

z = begin
         x = 1
         y = 2
         x + y
       end

# возвращает 3

или в одну строку:

begin x = 1; y = 2; x + y end

# возвращает 3
z = (x = 1; y = 2; x + y)

# возвращает 3

или на несколько строк:

(x = 1;
 y = 2;
 x + y)
# 3

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

f(2,3)
# 5

Без круглых скобок f будет ссылаться на саму функцию:

g = f;
g(2,3)
# 5

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

∑(x,y) = x + y

Поведение при передачи аргументов

В Julia принятые внутри функции аргументы - это новые привязки. Изменение самих привязок аргументов (т.е. присвоение именам принятых аргументов новых значений) внутри функции не ведет к изменению их значения в вызывающей области видимости:

julia> a = 2;
julia> function f(x)
           x = 3
       end;
julia> f(a)
3
julia> a
2

Передача массива:

julia> aa = [3]
1-element Array{Int64,1}:
3

julia> function fff(x::Array)
           x = [3,4,5]
       end

julia> fff(aa)
3-element Array{Int64,1}:
 3
 4
 5

julia> aa
1-element Array{Int64,1}:
 3

Однако, если аргумент - это ссылка на контейнер, например массив, то изменение значения элемента массива будет видно вызывающему коду:

julia> aa = [2]
1-element Array{Int64,1}:
2

julia> function ff(x::Array)
           x[1]=3
       end
ff (generic function with 1 method)

julia> ff(aa)
3

julia> aa
1-element Array{Int64,1}:
3

return

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

julia> function has_positive(xs)
           for i in xs
               i>0 && return true
           end
           false 
        end

Нужно внимательно рассмотреть все варианты потока выполнения, чтобы не ошибиться. Если забыть вставить false в функции выше, то при отсутствии положительных чисел в последовательности функция вернет nothing, потому что последняя операция - завершение цикла (а не проверка усовия).

А здесь, если не подойдет ни один вариант, будет возвращено "Не знаю". Но если забыть написать эту строчку ("Не знаю"), то будет возвращено false, потому что последним вычисленным выражением будет неудавшаяся проверка на равенство строке "Дружок":

julia> function guess_animal(name::AbstractString)
           n = strip(name)
           isempty(n) && return "Пустая строка не подходит"
           n == "Вася" && return "Кот"
           n == "Дружок" && return "Пес"
           "Не знаю"
       end

Эта функция вернет nothing, если передать ей значение 0 (ноль), потому что конструкция if else elseif end возвращает nothing, когда ни одна ветвь не выполнена:

julia> function positive_or_negative(x::Number)
           if x>0
               return "positive"
           end
           if x<0
               return "negative"
           end
       end

Как вы поняли, использовать return для выхода из функции лучше только, когда проще задача не решается. К примеру, функция has_positive может быть переписана так:

has_positive(xs) = !isempty( filter(x->x>0, xs) )

Если вспомнить, что существует стандартная функция any, то окажется что в отдельной has_positive вообще, нет надобности:

julia> any(x->x>0, [-1,-2])
false

julia> any(x->x>0, [-1,-2, 0, 1])
true

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

julia> get( Dict("Вася"=>"Кот","Дружок"=>"Пес"), 
              "Пальма", 
              "Не знаю")
"Не знаю"

На примере третьей функции видно, что иногда код лучше переписать так (тернарный оператор не даст забыть определить какую-то ветвь):

julia> positive_or_negative(x::Number) =
           x==0 ? "zero" :
           x>0 ? "positive" :
           "negative"

Я бы, даже, обобщил: использовать return с циклами или ветвлениями лучше тогда, когда задача не решается проще без них.

Операторы - это функции

В Julia все операторы кроме инфиксной записи имеют постфиксный вариант, потому что они - функции. За исключением операторов&& (логическое "И") и || (логическое "ИЛИ").Они не могут быть функциями из-за того, что должно выполняться правило "коротких вычислений" (x||y не вычисляет y, если x==true, а x&&y не вычисляет y, если x==false.

Итак, два следующих выражения равнозначны:

julia> 1 + 2 + 3

julia> +(1,2,3)

Т.к. операторы - это функции, то можно делать так:

julia> f = +;

julia> f(1,2,3)

Но под именем f функция + потеряет способность работать в инфиксной записи.

Анонимные функции

Синтаксис:

x -> x^2 + 2x - 1

или

function (x)

x^2 + 2x - 1

end

Главное применение анонимных функций - передача в качестве аргумента для друшой функции. Возмем для примера передачу функции в map(). Когда существует готовая функция, мы делаем так:

map(round, [1.2,3.5,1.7])

А когда готовой функции нет, мы создаем анонимную функцию и передаем ее:

map(x -> x^2 + 2x - 1, [1,3,-1])

Анонимная функция, принимающая несколько аргументов, записывается так:

(x,y,z)->2x+y-z

Не принимающая аргументов:

()->3

Возврат нескольких значений

Для возрата из функции нескольких значений, их нужно перечислить через запятую (в качестве последнего выражения или с ключевым словом return ):

function foo(a,b)

a+b, a*b

end

На самом деле, функция возвращает кортеж (a+b, a*b). Но так как кортежи (или по другому "тюплы" от "tuple") можно создавать и распаковывать без использования круглых скобок, то создается иллюзия возврата нескольких значений.

Не путайте запятую с точкой с запятой!

Если вы выполните эту функцию в REPL, вы увидите, что она возвращает кортеж

julia> foo(2,3)

(5,6)

Принять эти значения из функции можно так:

x, y = foo(2,3)

что равнозначно такому:

(x, y) = foo(2,3)

Аргументы переменной длины (varargs)

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

bar(a,b,x...) = (a,b,x)

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

julia> bar(1,2)

(1,2,())

julia> bar(1,2,3)

(1,2,(3,))

julia> bar(1,2,3,4)

(1,2,(3,4))

julia> bar(1,2,3,4,5,6)

(1,2,(3,4,5,6))

Как видно, x собирает в кортеж все, что передано после a и b.

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

julia> x = (3,4)

(3,4)

julia> bar(1,2,x...)

(1,2,(3,4))

Но если поставить разложенную коллекцию на на свое место в аргументах, она потеряет свою целостность внутри функции:

julia> x = (2,3,4)

(2,3,4)

julia> bar(1,x...)

(1,2,(3,4))

julia> x = (1,2,3,4)

(1,2,3,4)

julia> bar(x...)

(1,2,(3,4))

Итерируемый объект, не обязан быть кортежем:

julia> x = [3,4]
2-element Array{Int64,1}:
3
4

julia> bar(1,2,x...)

(1,2,(3,4))

julia> x = [1,2,3,4]

4-element Array{Int64,1}:
1
2
3
4

julia> bar(x...)

(1,2,(3,4))

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

julia> baz(a,b) = a + b;

julia> args = [1,2]

2-element Array{Int64,1}:
1
2

julia> baz(args...)
3

Но, количество переданных аргументов, в таком случае, должно быть соответственным:

julia> args = [1,2,3]
3-element Array{Int64,1}:
1
2
3

julia> baz(args...)
ERROR: MethodError: no method matching baz(::Int64, ::Int64, ::Int64)
Closest candidates are:
baz(::Any, ::Any) at none:1

Опциональные аргументы

Аргументы могут быть обозначены с таким синтаксисом:

function parse(type, num, base=10)
    ###
end

Тогда использование этой функции будет выглядеть так:

julia> parse(Int,"12",10)
12

julia> parse(Int,"12",3)
5

julia> parse(Int,"12")
12

Опциональные аргументы, на самом деле - лишь удобный синтаксис чтобы определить сразу несколько методов функции с разными количествами аргументов.

Именованные аргументы (keyword arguments)

Если определить аргументы таким способом (обратите внимание, точка с запятой отделяет обязательные аргументы от именованых)

function plot(x, y; style="solid", width=1, color="black")
    ###
end

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

plot(x, y, width=2)

или

plot(x, y; width=2)

Значения поумолчанию вычисляются только по необходимости и в порядке слева направо. Так в определении следующего аргумента можно использовать значение предыдущего.

Тип именованных аргументов можно определить так:

function f(;x::Int64=1)
    ###
end

Остальные, кроме указанных, именованные аргументы могут быть собраны так:

function f(x; y=0, kwargs...)
    ###
end

Inside f, kwargs will be a collection of (key,value) tuples, where each key is a symbol. Such collections can be passed as keyword arguments using a semicolon in a call, e.g. f(x, z=1; kwargs...). Dictionaries can also be used for this purpose.

Внутри f kwargs будет коллекцией кортежей (key,value) , где каждый key - типа Symbol.

В такую функцию можно передать кортежи (key,value) или пары key=>value:

plot(x, y; (:width,2))

или

plot(x, y; :width => 2)

будет эквивалентом этому:

plot(x, y, width=2)

Область видимости значений по умолчанию

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

function f(x, a=b, b=1)

###

end

здесь a=b ссылается не на ту b, которая b=1, а на внешнюю область видимости, что, скорее всего, приведет к ошибке.

Do-синтаксис

Для вызова функций, которые принимают первый аргумент типа Function можно использовать do-синтаксис. К примеру, такой случай:

map(x->begin
           if x < 0 && iseven(x)
               return 0
           elseif x == 0
               return 1
           else
               return x
           end
       end,
    [A, B, C])

с помощью do-синтаксиса можно переписать так:

map([A, B, C]) do x
    if x < 0 && iseven(x)
        return 0
    elseif x == 0
        return 1
    else
        return x
    end
end

do - блок должен заканчиваться end.
do-синтаксис создает анонимную функцию и передает ее первым аргументом в функцию.

Соответственно do a,b будет создавать анонимную функцию, принимающую два аргумента,
а do создает функцию без аргументов вроде () -> ....

Как пример применения рассмотрим использование функции open(). В do-блоке мы пишем все, что нужно сделать пока файл открыт:

open("outfile", "w") do io
    write(io, data)
end

Где определение функции выглядит так:

function open(f::Function, args...)
    io = open(args...)
    try
        f(io)
    finally
        close(io)
    end
end

Синтаксис с точкой для векторизации функций

Любая функция f может быть применена поэлементно к массиву или другой коллекции A с помощью синтаксиса:

f.(A)

Конечно, если вы уже определили специальный метод своей функции для массивов, вроде

f(A::AbstractArray) = map(f, A)

то точка не понадобится.

Более обще, можно сказать

f.(args...)

это эквивалент вызову:

broadcast(f, args...)

который разрешает манипулировать несколькими массивами или смеью массивов и скаларов. Например, если у вас есть

f(x,y) = 3x + 4y

то

f.(pi,A)

будет возвращать вам новый массив как результат применения

f(pi,a)

для каждого a в A.

В тоже время

f.(vector1,vector2)

вернет новый вектор из

f(vector1[i],vector2[i])

для каждого индекса i.

Более того, вложенные f.(args...) (dot-циклы) сливаются в один broadcast цикл. Например, sin.(cos.(X)) равносильно broadcast(x -> sin(cos(x)), X) так же как раскрывается списковое включение [sin(cos(x)) for x in X] - происходит только один цикл через X и память выделяется только для одного результирующего массива. Это не оптимизация компилятора, которая может случиться, а может нет, это - синтаксическая гарантия. Технически слияние dot-циклов прерывается как только встретится функция без точки:

sin.(sort(cos.(X)))

здесь sin. и cos. не могут быть объединены в один цикл.

Максимальной эффективности код достигает, когда для выходного массива заранее выделена память. Для этого существует синтаксис X .= ..., который равносилен вызову broadcast!(identity, X, ...) за тем исключением, что, как и выше broadcast! будет объединять dot-циклы. X .= sin.(Y) - эквивалент broadcast!(sin, X, Y) переписывающий X на месте результатом выполнения sin.(Y). Если с левой стороны стоит выражение индексации массива X[2:end] .= sin.(Y), то происходит broadcast!(sin, view(X, 2:endof(X)), Y). Таким образом, левая часть обновляется на месте.

results matching ""

    No results matching ""