Сейчас самой распространенной парадигмой программирования в бизнес-разработке является объектно-ориентированное программирование (ООП), которое строится вокруг 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
Подытожим ключевые идеи функциональной парадигмы:
Сейчас самым распространенным функциональным языком программирования является Haskell, появившийся в конце 1980-ых
Язык Haskell имеет компилятор GHC (от Glasgow Haskell Compiler) ghc и интерактивный интерпретатор GHCi ghci. Интерпретатор (он же REPL, от Read-Eval-Print Loop) имеет свои команды:
:quit - выход из интерпретатора:load File.hs или :l File.hs - загрузка модуля в сессию:reload или :r - рекомпиляция модулей с измененным кодомВ качестве системы сборки и пакетных менеджеров применяются два варианта:
Cabal, появившийся в 2005. Основные команды:
cabal initcabal replcabal buildcabal runStack, который является оберткой над Cabal
stack new <название-проекта>stack ghcistack buildstack runРаньше Cabal имел проблему с зависимостями, из-за чего на диске сохранялась только одна версия конкретного пакета, в ответ на что был создан Stack. Но с приходом Cabal версии 2 проблема исчезла. Сейчас Cabal используется по умолчанию
Функциональные языки программирования хорошо подходят для создания компиляторов, парсинга и форматирования строк, CLI-утилит и бэкенда, но плохо подходят для машинного обучения, создания графического интерфейса и игровой разработки
Haskell обладает статической строгой типизацией, то есть компилятор проверяет операции над типами переменных, а также не позволяет не явных преобразований типов
Одними из базовых типов в Haskell являются:
Int (диапазон Int определяется реализацией, но строго включает [-2^29, 2^29-1])IntegerBool (истина представляется как True, а ложь как False)Float и 64-битные Double)Char в кодировке UnicodeHaskell для работы с числами и булевыми значениями имеет такой же набор операторов, как и большинство других языков:
| Операция | Обозначение в языке | Пример | Результат |
|---|---|---|---|
| Сложение | + |
2 + 3 |
5 |
| Вычитание | - |
5 - 2 |
3 |
| Умножение | * |
4 * 3 |
12 |
| Деление | / |
7 / 2 |
3.5 |
| Целочисленное деление (применимо только к целым числам) | `div` |
7 `div` 2 |
3 |
| Деление по модулю (применимо только к целым числам) | `mod` |
7 `mod` 2 |
1 |
| Возведение в степень (показатель степени должен быть неотрицательным) | ^ |
2 ^ 3 |
8 |
| Меньше | < |
2 < 3 |
True |
| Больше | > |
2 > 3 |
False |
| Меньше или равно | <= |
2 <= 2 |
True |
| Больше или равно | >= |
3 >= 2 |
True |
| Равно | == |
2 == 2 |
True |
| Не равно | /= |
2 /= 3 |
True |
| Логическое И | && |
True && False |
False |
| Логическое ИЛИ | \|\| |
True \|\| False |
True |
| Логическое НЕ | not |
not True |
False |
| Отрицание числа | negate |
negate 5 |
-5 |
Единственным отличием является использование /= в качестве оператора “не равно” вместо привычного !=
Комментарии в коде обозначаются так:
-- однострочный комментарий
{-
многострочный
комментарий
-}
В дальнейших примерах будет представлен код и вывод интерпретатора
ghci
Строка сghci>обозначает вводимый код, а код безghci>обозначает результирующее значение
Вызов функции осуществляется так:
ghci> min 5 8
5
Здесь min - это название функции, а 5 и 8 - аргументы, перечисленные через пробел
Если аргумент содержит в себе пробелы, то аргумент нужно обернуть в скобки. Например:
ghci> min (3) (8)
3
ghci> min 3 (max 8 1)
3
ghci> max 3 max 8 1
-- ошибка GHC-39999, нельзя вызвать `max` как функцию 4 аргументов
ghci> (min 3 max) 8 1
-- ошибка GHC-39999, второй аргумент в функции `min` не может иметь тип функции
Такой способ вызова функции называется префиксный. Другим способом является инфиксный вызов:
ghci> (+) 2 3
5
ghci> 2 + 3
5
ghci> div 2 3
0
ghci> 2 `div` 3
0
ghci> mod 2 3
2
ghci> 2 `mod` 3
2
Здесь (+) 2 3 является префиксной формой, а 2 + 3 - инфиксной. Как можно заметить, операторы в Haskell - это тоже функции. Если функция состоит из специальных символом, то в префиксной форме он должен быть обернутым в скобки ((+) 2 3), а если из букв, то в инфиксной форме в обратные кавычки (2 `mod` 3)
Помимо этого у операторов есть две характеристики: тип ассоциативности и приоритет выполнения
Тип ассоциативности определяет порядок выполнения выражения, содержащего одинаковый оператор, примененного в инфиксной форме. Если в префиксной форме порядок определен слева направо, то в инфиксной форме есть 3 типа операторов:
infixl), такие как +, -, /. Выполнение такой цепочки будет исполняться слева направо, то есть записи 3 / 4 / 5 и (3 / 4) / 5 будут гарантированно иметь один результатinfixr), такие как ^. Выполнение такой цепочки будет исполняться справа налево, записи 3 ^ (4 ^ 5) и 3 ^ (4 ^ 5) имеют один результатinfix), такие как ==, >=. Их нельзя применять в цепочке (например, запись 3 == 4 == 5 вызовет ошибку)Приоритет определяет последовательность выполнения операции в цепочке. Приоритет является число от 0 до 9, чем больше приоритет, тем раньше выполниться операция. Приоритет 10 зарезервирован для применений функций
Оператор ^ имеет приоритет 8, операторы *, /, `div`, `mod` имеют приоритет 7, операторы +, - имеют приоритет 6, операторы ==, /=, <=, >=, <, > имеют приоритет 4
Такую информацию об операторе можно узнать в интерпретаторе с помощью инструкции :info или :i:
ghci> :i +
type Num :: * -> Constraint
class Num a where
(+) :: a -> a -> a
...
-- Defined in `GHC.Internal.Num'
infixl 6 +
В Haskell можно сделать свой оператор, например:
(|-|) :: Int -> Int -> Int
(|-|) a b = abs (a - b)
В первой строчке определен тип оператора, а именно Int -> Int -> Int. Последний тип в этой цепочке определяет тип возврата, а предыдущие - типы аргументов. Вторая строка определяет реализацию оператора
Важно заметить, что объявление оператор нельзя указать в интерпретаторе
Далее оператор можно будет применить:
ghci> (|-|) 4 5
1
ghci> 4 |-| 5
1
По умолчанию, такой оператор будет левоассоциативным и иметь приоритет 9
Чтобы изменить тип ассоциативности и приоритет, нужно применить соответствующее ключевое слово (infix, infixl или infixr):
(|-|) :: Int -> Int -> Int
(|-|) a b = abs (a - b)
infixl 6 |-|
Обозначение оператора может состоять из таких символов: !, #, $, %, &, *, +, ., /, <, =, >, ?, @, \, ^, |, -, ~, :. Если оператор начинается с :, то он является конструктором
Функции объявляются таким же образом:
mod :: Int -> Int -> Int
mod a b = a - b * a `div` b
Компилятор может попытаться вывести тип из реализации, поэтому сигнатуру необязательно указывать, но крайне рекомендуется
Такое определение функции называется явным или точечным (pointful). Однако есть еще бесточечный (pointfree) стиль:
-- Явный
plus5 :: Int -> Int
plus5 a = a + 5
-- Бесточечный
plus5 = (+) 5
-- или так
plus5 = (+5)
Можно также применять в функциях с несколькими аргументами (но такое ухудшает читаемость):
-- Pointful
pentaSum a b c d e = a + b + c + d + e
-- Pointfree
pentaSum = ((((((+) .). (+)) .). (+)) .). (+)
Сервис
Рассмотрим конструкции языка:
Условная конструкция if expression применяется для возврата выражения при нужном условии. Синтаксис:
if <предикат>
then <выражение-для-верного-предиката>
else <выражение-для-ложного-предиката>
Например:
factorial :: Integer -> Integer
factorial n = if n <= 1 then 1 else n * factorial (n - 1)
Охранная конструкция (guard) возвращает выражение, если соблюдено условие:
grade :: Integer -> String
grade n
| n >= 90 = "A"
| n >= 75 = "B"
| n >= 60 = "C"
otherwise = "F"
В отличие от if охранная конструкция допускает несколько условных веток. Здесь otherwise - это не ключевое слово, а синоним значения True
Выражение let <binding> in <expression> позволяет сократить выражение, определяя промежуточные переменные:
dist :: Double -> Double -> Double -> Double -> Double
dist x1 y1 x2 y2 = let
dx = x2 - x1
dy = y2 - y1
in sqrt (dx * dx + dy * dy)
Такие переменные существуют только в пределах подвыражения
Аналогичное выражение <expression> where <binding> позволяет сделать то же самое:
dist :: Double -> Double -> Double -> Double -> Double
dist x1 y1 x2 y2 = sqrt (dx * dx + dy * dy)
where
dx = x2 - x1
dy = y2 - y1
Ключевое отличие - это то, что let ... in ... - это выражение, а ... where ... - синтаксическая конструкция, поэтому она возможна только в конце определения функции или ветки
В Haskell можно определить функцию используя соответствие шаблону:
isZero :: Integer -> Bool
isZero 0 = True
isZero _ = False
Такая функция возвращает истину, если аргумент соответствует нулю. Однако порядок важен, такая функция возвращает всегда ложь:
isZero :: Integer -> Bool
isZero _ = False
isZero 0 = True
Другой пример - вычисление значений последовательности a_1 = 1, a_2 = 2, a_k = a_{k - 1} + 3 * a_{k - 2}:
someSequence :: Integer -> Integer
someSequence n = calcHelper n 1 2 where
calcHelper 1 a1 _ = a1
calcHelper 2 _ a2 = a2
calcHelper k x y = calcHelper (k - 1) y (y + 3 * x)
Здесь в блоке where объявляется вспомогательная функция calcHelper
Тип функции можно узнать с помощью инструкции :t или :type:
ghci> :type not
not :: Bool -> Bool
При этом есть особые функции:
id :: a -> a возвращает аргументfst :: (a, b) -> a возвращает первый элемент кортежаsnd :: (a, b) -> b возвращает второй элемент кортежаТакие функции не определяют явно типы, поэтому они соблюдают параметрический полиморфизм
Помимо этого в Haskell есть перегрузка функций (или Ad-hoc полиморфизм):
ghci> :t (+)
(+) :: Num a => a -> a -> a
ghci> :t (==)
(==) :: Eq a => a -> a -> Bool
Здесь для операторов на тип a наложены ограничения: для оператора + это то, что a должен быть числом, а для == - что a должен быть сравниваемым
Haskell поддерживает композицию функций. В математике композиция функций g ○ f определена так, что (g ○ f)(x) = g(f(x))
В Haskell композиция функций осуществляется через оператор .:
f :: Integer -> Integer
f x = x ^ 2
g :: Integer -> Integer
g x = x + 1
-- (f ○ g)(x) = f(g(x))
example -: Integer -> Integer
example x = (f . g) x
-- или так
example = (f . g)
Так как . - это оператор, значит это функция, и у нее такой тип:
ghci> :t (.)
(.) :: (b -> c) -> (a -> b) -> a -> c
Как можно заметить . - это функция от 3 аргументов: функции b -> c, функции a -> b и значения a
Реализовать . можно так:
(.) f g x = f (g (x))
Другой оператор $ выглядит таким образом:
($) :: (a -> b) -> a -> b
f $ x = f x
Как можно заметить, оператор $ применяет функцию f с аргументом x. В отличие от обычного применения оператор $ имеет приоритет 0 и правоассоциативен
Например, так можно представить оператор .:
(.) f g x = f $ g x
Или так можно возвести в квадрат несколько раз:
f :: Integer -> Integer
f = (^2)
ghci> f . f $ 2
16
ghci> f . f . f $ 2
256
Или упростить еще сильнее:
ghci> (^2) . (^2) $ 2
16
ghci> (^2) . (^2) . (^2) $ 2
256
Такая форма не засоряет пространство имен, но ухудшает читаемость