Функции высшего порядка
Функция высшего порядка (HOF - higher-order function) часто определяется как функция, которая
- принимает другие функции в качестве входных параметров или
- возвращает функцию в качестве результата.
В Scala HOF возможны, потому что функции являются объектами первого класса.
В качестве важного примечания: хотя в этом документе используется общепринятый термин "функция высшего порядка", в Scala эта фраза применима как к методам, так и к функциям. Благодаря технологии Eta Expansion их, как правило, можно использовать в одних и тех же местах.
От потребителя к разработчику
В примерах, приведенных ранее в документации, было видно, как пользоваться методами,
которые принимают другие функции в качестве входных параметров, например, map
и filter
.
В следующих разделах будет показано, как создавать HOF, в том числе:
- как писать методы, принимающие функции в качестве входных параметров
- как возвращать функции из методов
В процессе будет видно:
- синтаксис, который используется для определения входных параметров функции
- как вызвать функцию, если есть на нее ссылка
В качестве полезного побочного эффекта, как только синтаксис станет привычным, его можно начать использовать для определения параметров функций, анонимных функций и функциональных переменных, а также станет легче читать Scaladoc для функций высшего порядка.
Понимание Scaladoc метода filter
Чтобы понять, как работают функции высшего порядка, рассмотрим пример:
определим, какой тип функций принимает filter
, взглянув на
его Scaladoc.
Вот определение filter
в классе List[A]
:
def filter(p: (A) => Boolean): List[A]
Это определение указывает на то, что filter
- метод, который принимает параметр функции с именем p
.
По соглашению, p
обозначает предикат, который представляет собой просто функцию, возвращающую Boolean
.
Таким образом, filter
принимает предикат p
в качестве входного параметра и возвращает List[A]
,
где A
- тип, содержащийся в списке; если filter
вызывается для List[Int]
, то A
- это тип Int
.
На данный момент, если не учитывать назначение метода filter
, все, что известно,
так это то, что алгоритм каким-то образом использует предикат p
для создания и возврата List[A]
.
Если посмотреть конкретно на параметр функции p
: p: (A) => Boolean
, то эта часть описания filter
означает,
что любая передаваемая функция должна принимать тип A
в качестве входного параметра и возвращать Boolean
.
Итак, если список представляет собой список List[Int]
,
то можно заменить универсальный тип A
на Int
и прочитать эту подпись следующим образом: p: (Int) => Boolean
.
Поскольку isEven
имеет такой же тип — преобразует входное значение Int
в результирующее Boolean
—
его можно использовать с filter
.
Написание методов, которые принимают параметры функции
Рассмотрим пример написания методов, которые принимают функции в качестве входных параметров.
Для определенности, будем называть код, который пишется, методом, а код, принимаемый в качестве входного параметра, — функцией.
Пример
Чтобы создать метод, который принимает функцию в качестве параметра, необходимо:
- в списке параметров метода определить сигнатуру принимаемой функции
- использовать эту функцию внутри метода
Чтобы продемонстрировать это, вот метод, который принимает входной параметр с именем f
, где f
— функция:
def sayHello(f: () => Unit): Unit = f()
Эта часть кода — сигнатура типа (type signature) — утверждает, что f
является функцией,
и определяет типы функций, которые будет принимать метод sayHello
: f: () => Unit
.
Как это работает:
f
— имя входного параметра функции. Аналогично тому, как параметрString
обычно называетсяs
или параметрInt
-i
- сигнатура типа
f
определяет тип функций, которые будет принимать метод - часть
()
подписиf
(слева от символа=>
) указывает на то, чтоf
не принимает входных параметров - часть сигнатуры
Unit
(справа от символа=>
) указывает на то, что функцияf
не должна возвращать осмысленный результат - в теле метода
sayHello
(справа от символа=
) операторf()
вызывает переданную функцию
Теперь, когда sayHello
определен, создадим функцию, соответствующую сигнатуре f
, чтобы ее можно было проверить.
Следующая функция не принимает входных параметров и ничего не возвращает, поэтому она соответствует сигнатуре типа f
:
def helloJoe(): Unit = println("Hello, Joe")
Поскольку сигнатуры типов совпадают, можно передать helloJoe
в sayHello
:
sayHello(helloJoe)
// Hello, Joe
Был определен метод с именем sayHello
, который принимает функцию в качестве входного параметра,
а затем вызывает эту функцию в теле своего метода.
sayHello может принимать разные функции
Важно знать, что преимущество этого подхода заключается не в том,
что sayHello
может принимать одну функцию в качестве входного параметра;
преимущество в том, что sayHello
может принимать любую функцию, соответствующую сигнатуре f
.
Например, поскольку следующая функция не принимает входных параметров и ничего не возвращает,
она также работает с sayHello
:
def bonjourJulien(): Unit = println("Bonjour, Julien")
sayHello(bonjourJulien)
// Bonjour, Julien
Рассмотрим ещё несколько примеров того, как определять сигнатуры различных типов для параметров функции.
Общий синтаксис для определения входных параметров функции
В методе:
def sayHello(f: () => Unit): Unit
сигнатурой типа для f
является () => Unit
.
Это сигнатура означает "функцию, которая не принимает входных параметров и не возвращает ничего значимого (Unit
)".
Вот сигнатура функции, которая принимает параметр String
и возвращает Int
:
f: (String) => Int
Какие функции принимают строку и возвращают целое число? Например, такие, как "длина строки" и контрольная сумма.
Эта функция принимает два параметра Int
и возвращает Int
:
f: (Int, Int) => Int
Какие функции соответствуют данной сигнатуре?
Любая функция, которая принимает два входных параметра Int
и возвращает Int
, соответствует этой сигнатуре,
поэтому все "функции" ниже (точнее, методы) подходят:
def add(a: Int, b: Int): Int = a + b
def subtract(a: Int, b: Int): Int = a - b
def multiply(a: Int, b: Int): Int = a * b
Из примеров выше можно сделать вывод, что общий синтаксис сигнатуры функций такой:
variableName: (parameterTypes ...) => returnType
Поскольку функциональное программирование похоже на создание и объединение ряда алгебраических уравнений, обычно много думают о типах при разработке функций и приложений. Можно сказать, что «думают типами».
Параметр функции вместе с другими параметрами
Чтобы HOFs стали действительно полезными, им также нужны некоторые данные для работы.
Для класса, подобного List
, в его методе map
уже есть данные для работы: элементы в List
.
Но для автономного приложения, у которого нет собственных данных,
метод также должен принимать в качестве других входных параметров данные.
Рассмотрим пример метода с именем executeNTimes
, который имеет два входных параметра: функцию и Int
:
def executeNTimes(f: () => Unit, n: Int): Unit =
for i <- 1 to n do f()
Как видно из кода, executeNTimes
выполняет функцию f
n
раз.
Поскольку простой цикл for
, подобный этому, не имеет возвращаемого значения, executeNTimes
возвращает Unit
.
Чтобы протестировать executeNTimes
, определим метод, соответствующий сигнатуре f
:
def helloWorld(): Unit = println("Hello, world")
Затем передадим этот метод в executeNTimes
вместе с Int
:
executeNTimes(helloWorld, 3)
// Hello, world
// Hello, world
// Hello, world
Метод executeNTimes
трижды выполняет функцию helloWorld
.
Столько параметров, сколько необходимо
Методы могут усложняться по мере необходимости.
Например, этот метод принимает функцию типа (Int, Int) => Int
вместе с двумя входными параметрами:
def executeAndPrint(f: (Int, Int) => Int, i: Int, j: Int): Unit =
println(f(i, j))
Поскольку методы sum
и multiply
соответствуют сигнатуре f
,
их можно передать в executeAndPrint
вместе с двумя значениями Int
:
def sum(x: Int, y: Int) = x + y
def multiply(x: Int, y: Int) = x * y
executeAndPrint(sum, 3, 11)
// 14
executeAndPrint(multiply, 3, 9)
// 27
Согласованность подписи типа функции
Самое замечательное в изучении сигнатур типов функций Scala заключается в том, что синтаксис, используемый для определения входных параметров функции, — это тот же синтаксис, что используется для написания литералов функций.
Например, если необходимо написать функцию, вычисляющую сумму двух целых чисел, её можно было бы написать так:
val f: (Int, Int) => Int = (a, b) => a + b
Этот код состоит из сигнатуры типа:
val f: (Int, Int) => Int = (a, b) => a + b
-----------------
входных параметров:
val f: (Int, Int) => Int = (a, b) => a + b
------
и тела функции:
val f: (Int, Int) => Int = (a, b) => a + b
-----
Согласованность Scala состоит в том, что тип функции:
val f: (Int, Int) => Int = (a, b) => a + b
-----------------
совпадает с сигнатурой типа, используемого для определения входного параметра функции:
def executeAndPrint(f: (Int, Int) => Int, ...
-----------------
По мере освоения этого синтаксиса, становится привычным его использование для определения параметров функций, анонимных функций и функциональных переменных, а также становится легче читать Scaladoc для функций высшего порядка.
Ссылки: