Функция высшего порядка (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")
Вот что выводится в REPL:
scala> 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) f()
def executeNTimes(f: () => Unit, n: Int): Unit =
for i <- 1 to n do f()
Как видно из кода, executeNTimes
выполняет функцию f
n
раз.
Поскольку простой цикл for
, подобный этому, не имеет возвращаемого значения, executeNTimes
возвращает Unit
.
Чтобы протестировать executeNTimes
, определим метод, соответствующий сигнатуре f
:
// тип метода - `() => Unit`
def helloWorld(): Unit = println("Hello, world")
Затем передадим этот метод в executeNTimes
вместе с Int
:
scala> 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 для функций высшего порядка.
Contributors to this page:
Contents
- Введение
- Возможности Scala
- Почему Scala 3?
- Почувствуй Scala
- Пример 'Hello, World!'
- REPL
- Переменные и типы данных
- Структуры управления
- Моделирование данных
- Методы
- Функции первого класса
- Одноэлементные объекты
- Коллекции
- Контекстные абстракции
- Верхнеуровневые определения
- Обзор
- Первый взгляд на типы
- Интерполяция строк
- Структуры управления
- Моделирование предметной области
- Инструменты
- Моделирование ООП
- Моделирование ФП
- Методы
- Особенности методов
- Main методы в Scala 3
- Обзор
- Функции
- Анонимные функции
- Параметры функции
- Eta расширение
- Функции высшего порядка
- Собственный map
- Создание метода, возвращающего функцию
- Обзор
- Пакеты и импорт
- Коллекции в Scala
- Типы коллекций
- Методы в коллекциях
- Обзор
- Функциональное программирование
- Что такое функциональное программирование?
- Неизменяемые значения
- Чистые функции
- Функции — это значения
- Функциональная обработка ошибок
- Обзор
- Типы и система типов
- Определение типов
- Параметризованные типы
- Пересечение типов
- Объединение типов
- Алгебраические типы данных
- Вариантность
- Непрозрачные типы
- Структурные типы
- Зависимые типы функций
- Другие типы
- Контекстные абстракции
- Методы расширения
- Параметры контекста
- Контекстные границы
- Given импорты
- Классы типов
- Многостороннее равенство
- Неявное преобразование типов
- Обзор
- Параллелизм
- Scala утилиты
- Сборка и тестирование проектов Scala с помощью Sbt
- Рабочие листы
- Взаимодействие с Java