← Все статьи

Зарисовки в функциональном стиле

PascalABC.NET не является функциональным языком, однако многие идеи функционального программирования в нем реализуются вполне естественно. В языке есть лямбда-выражения, методы расширения, операции над последовательностями и цепочки вызовов.

Рассмотрим это на примере алгоритма из задачи 74 проекта Эйлера.

Каждый следующий член цепочки получается из предыдущего как сумма факториалов его цифр:

1! + 6! + 9! = 363601

Например:

169 → 363601 → 1454 → 169

Преобразование числа в цифры

Сначала напишем метод расширения для типа integer, возвращающий массив его цифр:

function ToDigits(Self: integer): array of integer; extensionmethod;
begin
  Result := Self.ToString.Select(ch -> ch.ToDigit).ToArray;
end;

Теперь можно написать:

var a := 145.ToDigits;
a.Println;

Результат:

1 4 5

Метод ToDigits выглядит как обычный метод типа integer, хотя на самом деле это функция с параметром Self, помеченная директивой extensionmethod.

Факториал

Определим функцию факториала:

function Fact(n: integer): integer;
begin
  Result := 1;
  for var i := 1 to n do
    Result *= i;
end;

Теперь сумму факториалов цифр числа можно записать очень компактно:

var n := 145;
Println(n.ToDigits.Select(d -> Fact(d)).Sum);

Результат:

145

поскольку

145 = 1! + 4! + 5!

Разберем выражение:

n.ToDigits.Select(d -> Fact(d)).Sum

Сначала

n.ToDigits

преобразует число в массив цифр.

Затем

.Select(d -> Fact(d))

заменяет каждую цифру ее факториалом.

Наконец,

.Sum

складывает полученные значения.

Раньше для такого кода пришлось бы самостоятельно писать методы вроде Map и Sum. В современном PascalABC.NET эти операции уже есть.

Следующий элемент цепочки

Оформим вычисление следующего элемента цепочки отдельной функцией:

function Next(n: integer): integer;
begin
  Result := n.ToDigits.Select(d -> Fact(d)).Sum;
end;

Проверим:

Println(Next(169));

Получим:

363601

Построение цепочки

Теперь напишем метод расширения, который строит первые n элементов цепочки по заданной функции перехода:

function TakeBy(Self: integer; f: integer -> integer; n: integer): array of integer; extensionmethod;
begin
  Result := new integer[n];
  Result[0] := Self;
  for var i := 1 to n-1 do
    Result[i] := f(Result[i-1]);
end;

Функция перехода передается в параметре:

f: integer -> integer

То есть f получает целое число и возвращает целое число.

Теперь исходная задача решается так:

169.TakeBy(Next,4).Println;

Результат:

169 363601 1454 169

Лямбда вместо отдельной функции

Отдельная функция Next здесь необязательна. Ее можно заменить лямбда-выражением:

169.TakeBy(
  n -> n.ToDigits.Select(d -> Fact(d)).Sum,
  4
).Println;

Получим тот же результат:

169 363601 1454 169

Весь алгоритм перехода к следующему элементу теперь находится непосредственно в месте использования:

n -> n.ToDigits.Select(d -> Fact(d)).Sum

Это функция, которая получает число n и возвращает сумму факториалов его цифр.

Что здесь функционального

В этом небольшом примере используются сразу несколько характерных идей функционального стиля.

Функции можно передавать как параметры

Метод TakeBy получает функцию:

function TakeBy(Self: integer; f: integer -> integer; n: integer): array of integer; extensionmethod;

и применяет ее для получения каждого следующего элемента:

Result[i] := f(Result[i-1]);

В качестве такой функции можно передать как обычную функцию:

169.TakeBy(Next,4)

так и лямбда-выражение:

169.TakeBy(n -> n.ToDigits.Select(d -> Fact(d)).Sum,4)

Последовательности можно преобразовывать без явных циклов

Вместо цикла, вычисляющего факториал каждой цифры, используется:

n.ToDigits.Select(d -> Fact(d))

Метод Select применяет указанное преобразование к каждому элементу последовательности.

Операции можно объединять в цепочки

Выражение

n.ToDigits.Select(d -> Fact(d)).Sum

читается практически слева направо:

  1. получить цифры числа;
  2. заменить каждую цифру ее факториалом;
  3. сложить результаты.

Методы расширения позволяют дополнять существующие типы

Мы определили обычную функцию:

function ToDigits(Self: integer): array of integer; extensionmethod;

но благодаря extensionmethod можем вызывать ее в точечной форме:

145.ToDigits

Точно так же функция

function TakeBy(Self: integer; f: integer -> integer; n: integer): array of integer; extensionmethod;

вызывается как метод целого числа:

169.TakeBy(Next,4)

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

PascalABC.NET остается прежде всего императивным языком, но лямбда-выражения, методы расширения и операции над последовательностями позволяют во многих задачах использовать удобный функциональный стиль.