scalabook

Форк
0
289 строк · 15.8 Кб

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

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


Ссылки:

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.