Функция высшего порядка (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