Scala 3 — Book

Функции высшего порядка

Language

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

Написание методов, которые принимают параметры функции

Рассмотрим пример написания методов, которые принимают функции в качестве входных параметров.

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

Пример

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

  1. в списке параметров метода определить сигнатуру принимаемой функции
  2. использовать эту функцию внутри метода

Чтобы продемонстрировать это, вот метод, который принимает входной параметр с именем 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: