Зарисовки в функциональном стиле
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
читается практически слева направо:
- получить цифры числа;
- заменить каждую цифру ее факториалом;
- сложить результаты.
Методы расширения позволяют дополнять существующие типы
Мы определили обычную функцию:
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 остается прежде всего императивным языком, но лямбда-выражения, методы расширения и операции над последовательностями позволяют во многих задачах использовать удобный функциональный стиль.