itmo_conspects

Функциональное программирование. Трек Холопова Д. С.

Лекция 1. Введение

Сейчас самой распространенной парадигмой программирования в бизнес-разработке является объектно-ориентированное программирование (ООП), которое строится вокруг 4 ключевых принципов:

  1. Абстрагирование сущностей для выделения существенных характеристик
  2. Инкапсуляция, то есть отделение друг от друга элементов объекта, которые определяют его устройство и поведение
  3. Наследование, что позволяет объекту одного класса иметь общие свойства с объектом другого класса, но при этом расширять или изменять структуру или поведение
  4. Модульность системы

Парадигма ООП имеет множество преимуществ, но иногда не подходит под некоторые задачи

Так, например, в ООП у объекта есть внутреннее изменяемое состояние, от которого неявно зависит поведение функции. Например, в этом примере, функция изменяет переданный объект (а именно удаляет запись в нем):

class Validator {
    static void validate(Map<String, User> users) {
        if (users.containsKey("admin") && !users.get("admin").isActive()) {
            users.remove("admin"); // валидация мутирует входные данные
        }
    }
}

Map<String, User> users = new HashMap<>();
users.put("admin", new User("Alice", false));
Validator.validate(users);
User admin = users.get("admin"); // admin == null

В функциональной парадигме у объектов иммутабельность по умолчанию, то есть данные объекта не изменяется, а вместо этого создается новый объект с другими данными

users = [("admin", User ("Alice", False))]
users' = validate users

result1 = lookup "admin" users' -- "admin" не найдется
result2 = lookup "admin" users -- найдется объект "Alice"

Также функции в ООП зачастую имеют побочные эффекты. Это значит, что результат их выполнения зависит не только от аргументов, но и от внутреннего состояния других объектов, например, базы данных:

double withdraw(BankAccount account, double amount) {
    if (amount > account.getBalance()) {
        logger.error("Not enough money for account {}", account.getId());
        throw new IllegalStateException("Insufficient funds");
    }

    account.setBalance(account. getBalance() - amount);
    logger.info("Withdraw {} from {}", amount, account.getId());
    auditService. save(account);
    return account.getBalance();
}

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

Также чистые функции позволяют выполнять ленивые вычисления. Например, такой код на Java:

public int firstArgument(int a, int b) {
    return a;
}

firstArgument(1, 1/0); // ArithmeticException

запустится в ошибкой ArithmeticException, так как сначала нужно вычислить значения параметров 1 и 1/0, а затем вызвать функцию, несмотря на то, что второй аргумент не применяется. В функциональном языке с ленивыми вычислениями второй аргумент не вычисляется, из-за чего программа выполняется:

firstArgument a b = a
result = firstArgument (1) (1/0) -- ok

Подытожим ключевые идеи функциональной парадигмы:

  1. Иммутабельность
  2. Чистые функции
  3. Ленивые вычисления (поддерживаются не всеми языками, например, в Haskell ленивые вычисления применяются по умолчанию, а в Clojure, по умолчанию, нет)
  4. Функции высших порядков - функции, которые принимают другие функции как аргументы или возвращают их
  5. Алгебраические типы данных - способ моделирования данных (есть не во всех языках)

Сейчас самым распространенным функциональным языком программирования является Haskell, появившийся в конце 1980-ых

Язык Haskell имеет компилятор GHC (от Glasgow Haskell Compiler) ghc и интерактивный интерпретатор GHCi ghci. Интерпретатор (он же REPL, от Read-Eval-Print Loop) имеет свои команды:

В качестве системы сборки и пакетных менеджеров применяются два варианта:

Раньше Cabal имел проблему с зависимостями, из-за чего на диске сохранялась только одна версия конкретного пакета, в ответ на что был создан Stack. Но с приходом Cabal версии 2 проблема исчезла. Сейчас Cabal используется по умолчанию

Функциональные языки программирования хорошо подходят для создания компиляторов, парсинга и форматирования строк, CLI-утилит и бэкенда, но плохо подходят для машинного обучения, создания графического интерфейса и игровой разработки