learn haskell
25 subscribers
2 photos
2 links
изучение языка haskell
Download Telegram
Функции как значения первого класса

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

Функции как аргументы
Передача функций как аргументов позволяет обобщить процесс повторяющихся вычислений.
Допустим у нас есть функция ifEvenInc, которая увеличивает число n на единицу, если оно чётное, оставляя его неизменным в противном случае:
module Main (main) where

ifEvenInc :: Int -> Int
ifEvenInc n = if even n
then n+1
else n

main :: IO ()
main = do
putStrLn $ show $ ifEvenInc 20


Если позже нам понадобятся функции ifEvenDouble и ifEvenSquare, то мы будем вынуждены дублировать условие, а когда оно измениться, то переписывать синхронно все эти функции.
НО можно параметризовать функцию ifEven..., передав ей в качестве аргумента функцию-модификатор для x.
module Main (main) where

ifEven :: (Int -> Int) -> Int -> Int
ifEven func x = if even x
then func x
else x

ifEvenInc :: Int -> Int
ifEvenInc n = ifEven (\x -> x+1) n

main :: IO ()
main = do
putStrLn $ show $ ifEvenInc 20


Не стоит забывать, что лямбда-функции - это полноценные функции и их так же можно передавать как значение.
Наиболее частым примером такого использования функций, не только в haskell, но и в других языках, является сортировка пользовательских типов сортировкой из стандартной библиотеки. Например sortBy или std::sort (в C++).

Функции как результат
В haskell все функции принимают ровно один аргумент и возвращают одно значение - которое может быть другой функцией. Это называется каррированием, и это фундаментальный принцип функционального программирования - если функция принимает несколько аргументов, она на самом деле возвращает функцию на каждом шаге.
Даже в простом примере:
add :: Int -> Int -> Int
add x y = x + y

Тип Int -> Int -> Int на самом деле означает Int -> (Int -> Int) — то есть add принимает Int и возвращает функцию Int -> Int.

Функции можно возвращать и явно. Например, можно изменить нашу функцию ifEven. Если она принимает четный первый аргумент, то будет возвращена функция, которая увеличивает свой аргумент на 1, иначе - функция, которая увеличивает свой аргумент в 2 раза:
module Main (main) where

ifEven :: Int -> (Int -> Int)
ifEven x = if even x
then (\a -> a+1)
else (\b -> b*2)

main :: IO ()
main = do
let func = ifEven 21
putStrLn $ show $ func 3
1
Замыкание

Концепция замыкания довольно простая - когда лямбда-функция захватывает некоторое значение из внешней области видимости, то это называется замыкание.
Например:
genIfEven f = (\x -> ifEven f x)

тут f - захватывается лямбда-функцией.

Замыкания в Haskell захватывают значения, а не ссылки на изменяемую память, и поскольку значения неизменны, разницы между «по значению» и «по ссылке» в привычном смысле не существует. Но стоит отметить, что haskell может делить (share) подвыражения в памяти - то есть, если замыкание захватывает большое значение, оно не обязательно копируется полностью, а может ссылаться на уже существующую структуру в памяти. Но это оптимизация реализации, а не семантика: с точки зрения программы, это всё равно «значение», и оно неизменно.
Частичное применение

Это мощный механизм, который позволяет зафиксировать значения некоторых аргументов функции и на её основе получить функцию с меньшим количеством аргументов.
Например у нас есть функция add которая складывает два числа. На её основе можно создать функцию increment, которая выполняет увеличение аргумента на 1.
add :: Int -> Int -> Int
add x y = x + y

increment :: Int -> Int
increment = add 1


В языке haskell существует полезная практика
"Всякий раз, когда вы хотите использовать замыкание, нужно располагать аргументы функции в порядке от наиболее общего к наименее общему".

Это связано именно с использованием частичного применения и позволяет делать его более выразительным. Если же, вдруг, нужно частично применить второй аргумент, а не первый, то можно воспользоваться стандартной функцией flip - она меняет местами аргументы у функции с двумя аргументами:
flip :: (a -> b -> c) -> b -> a -> c

Не трудно написать свой аналог flip для замены 3 и 4, 4 и 5 или еще полного порядка аргументов. Однако, такое легаси довольно поддерживать.

Не стоит путать частичное применение с каррированием — это связанная, но другая концепция:
- каррирование — это преобразование функции от нескольких аргументов в цепочку функций от одного аргумента.
- частичное применение — это процесс применения функции к части её аргументов, чтобы получить новую функцию. Но в Haskell все функции изначально каррированы, поэтому частичное применение работает «из коробки».
Списки

Списки это очень важная структура данных в функциональном программировании. Определение списка является рекурсивным - это или пустой список, или элемент, за которым следует список. Список разделяется на два элемента - голова (head) и хвост (tail). По умолчанию список всегда заканчивается пустым списком.
Все элементы списка должны быть одного типа.

Примеры списков:
[1, 2, 3]
[1.3, 45.7899]
["TCP", "UDP", "DCCP", "SCTP"]


Строки в haskell это тоже списки символов.
GHCi>[’п’,’р’,’и’,’в’,’е’,’т’]
"привет"
GHCi> ’п’:’р’:’и’:’в’:’е’:’т’:[]
"привет"


Перечисления
Списки можно создавать с помощь перечисления
[1..10] = [1,2,3,4,5,6,7,8,9,10]
[2,4..10] = [2,4,6,8,10]
[9,8..1] = [9,8,7,6,5,4,3,2,1]
['a'..'z'] = "abcdefghijklmnopqrstuvwxyz"

Списки в Haskell ленивы, т.е. элементы вычисляются только тогда, когда они нужны. Это позволяет работать с бесконечными списками:
naturals = [1..]       -- бесконечный список натуральных чисел


Функции для работы со списками
head - возвращает голову списка
tail - формирует другой список, представляющий собою всё от первоначального списка, кроме головы
last - получить последний элемент
init - вернёт всё кроме последнего элемента
length - возвращает длину списка
null - проверка на пустоту
reverse - обращает список
take N [lst] - отбирает N элементов из списка
drop N [lst] - вырезает N элементов из списка
X 'elem' [lst] - проверяет, входит ли элемент в список
!! - взять элемент по индексу
take N - возвращает N первых элементов списка
[2,4 ..] - бесконечный список чётных чисел
cycle [] - зацикливает список в бесконечный
repeat X - делает бесконечный список из X
zip - скомбинировать два списка в пары
++ - конкантенация списков
: - добавление в начало списка

Примеры:
GHCi> [1,2,3] !! 0
1
GHCi> length [(10,20),(1,2),(15,16)]
3
GHCi> zip "пёс" "кролик"
[(’п’,’к’),(’ё’,’р’),(’с’,’о’)]
Генераторы списков

Математические аналогии в haskell не заканчиваются на функциях. Замечательной возможностью этого языка является генерация списков.
Вот пример простого описания множества. Множество, состоящее из первых десяти чётных чисел, это
S = {x · 2 | x ∈ N, x<=10}

, где выражение перед символом | называется производящей функцией,
x - переменная,
N - входной набор,
а x ∈ 10 - условие выборки.
Это означает, что множество содержит удвоенные натуральные числа, которые удовлетворяют условию выборки.

Чтобы выразить это в языке haskell потребуется написать такое:
ghci> [x*2 | x <– [1..10]]
[2,4,6,8,10,12,14,16,18,20]

Тут мы извлекаем элементы из списка [1..10], т. е. x последовательно принимает все значения элементов списка. Иногда говорят, что x связывается с каждым элементом списка. Часть генератора, находящаяся левее вертикальной черты |, определяет значения элементов результирующего списка.

В генераторы можно добавить условия выборки, они перечисляются после задания источника данных и отделяются запятыми.
-- только те элементы, которые, будучи удвоенными, больше либо равны 12.
ghci> [x*2 | x <– [1..10], x*2 >= 12]
[12,14,16,18,20]

-- все числа от 50 до 100, остаток от деления на 7 которых равен 3
ghci> [ x | x <– [50..100], x `mod` 7 == 3]
[52,59,66,73,80,87,94]


Для удобства, выражение генератора можно поместить в функцию и вызывать её для разных интервалов.
boomBangs xs = [if x < 10 then "БУМ!" else "БАХ!" | x <– xs, odd x]
...
boomBangs [7..13] -- ["БУМ!","БУМ!","БАХ!","БАХ!"]
Рекурсия

В функциональном программировании нет циклов. Это значит, что все итерационные задачи должны решаться через рекурсию. Для людей, которые уже имеют опыт работы с императивными языками, понятие рекурсии кажется сложным. Хотя почти все повторяющиеся действия в нашей жизни имеют рекурсивное описание.

Функция называется рекурсивной, если она вызывает саму себя в своём определении.
Главное правило при работе с рекурсивными алгоритмами - точно знать, когда нужно выходить из рекурсии (базовый случай).

Самыми распространёнными и заезженными примерами работы с рекурсией являются вычисление факториала и наибольшего общего делителя. Но в haskell рекурсия применяется еще, например, для работы со списками:
-- вычисление длины списка
length' :: [a] -> Int
length' [] = 0
length' (_:xs) = 1 + length' xs

-- сумма элементов списка
sum' :: Num a => [a] -> a
sum' [] = 0
sum' (x:xs) = x + sum' xs


Оптимизация хвостовой рекурсии

Программисты не любят рекурсию еще за то, что она сильно расходует оперативку. При шаге рекурсивного алгоритма нужно запомнить стэк текущего вызова, чтобы раскрутить алгоритм обратно. Однако, если рекурсивный вызов является последней операцией в функции (хвостовая рекурсия) и в неё передаются все нужные данные как аргументы, то компилятор оптимизирует этот вызов и избежать роста стэка.
Кортежи

Это упорядоченная коллекция фиксированного размера, содержащая элементы разных типов (в отличие от списков, которые однородны).
Кортежи обозначаются круглыми скобками, а их компоненты отделяются запятыми:
ghci> (1, 3)
(1,3)
ghci> (50, 50.4, "привет", 'b')
(50,50.4,"привет",'b')
ghci> :t (50, 50.4, "привет", 'b')
(50, 50.4, "привет", 'b')
:: (Fractional b, Num a) => (a, b, String, Char)

Кортежи хорошо подходят для возврата нескольких значений из функции и для временного объединения двух-трёх значений (аналог анонимной структуры).
minMax :: [Int] -> (Int, Int)
minMax xs = (minimum xs, maximum xs)

Но стоит помнить, что кортежи - не замена алгебраическим типам (a.k.a. структуры), их использование может снижать читаемость и выразительность кода.

Важное
- Кортежи с разным количеством элементов — это разные типы. Например, (Int, String) и (Int, String, Bool) несовместимы.
- Несмотря на то что есть списки с одним элементом, не бывает кортежей с одним компонентом.

Полезными функциями для работы с кортежами из двух элементов являются fst и snd, которые возвращают первый и второй элементы кортежа:
ghci> fst ("Вау", False)
"Вау"
ghci> snd ("Вау", False)
False


Распаковка кортежей

Получить значение из кортежа в переменную можно с помощью сопоставления с образцом. В примере аргументом функции является кортеж (Int, Int), эти два числа складываются в теле функции:
addPair :: (Int, Int) -> Int
addPair (x, y) = x + y
👍1🤓1
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
Чистые функции

Чистые функции — это фундаментальное понятие в функциональном программировании и хорошей практике написания кода в целом. Они обладают двумя ключевыми свойствами:
1. Нет побочных эффектов - функция не изменяет состояние вне своего тела (не мутирует глобальные переменные, не изменяет входные аргументы, не пишет в файл, не отправляет HTTP-запросы и т.п.).
2. Детерминированность - при одинаковых входных аргументах функция всегда возвращает один и тот же результат, независимо от контекста или времени вызова.

Преимущества чистых функций

1. Предсказуемость. Поведение функции легко понять и протестировать: она зависит только от входных данных.
2. Лёгкость тестирования. Нет необходимости в моках, заглушках или подготовке состояния — просто передаёшь аргументы и проверяешь результат.
3. Повторное использование. Чистые функции легко переиспользовать в разных частях программы.
4. Параллелизм и многопоточность. Поскольку чистые функции не изменяют состояние, их можно безопасно выполнять параллельно.
5. Мемоизация (кеширование). Результат вызова чистой функции можно кэшировать по входным аргументам.
6. Референциальная прозрачность. Вызов функции можно заменить её результатом без изменения поведения программы.

Где НЕЛЬЗЯ использовать чистые функции?

Чистые функции не могут выполнять задачи, которые по своей природе изменяют состояние:
- Ввод/вывод (чтение файла, логирование, вывод в консоль)
- Работа с сетью (HTTP-запросы)
- Генерация случайных чисел
- Работа с текущим временем
- Изменение DOM в браузере
Однако такие "грязные" операции можно инкапсулировать на периферии программы, оставляя ядро приложения чистым.

Почему Haskell — «чисто функциональный»?

В Haskell все функции по умолчанию чистые. Это означает:
- Нельзя случайно произвести побочный эффект (например, изменить переменную или напечатать в консоль) из обычной функции.
- Побочные эффекты изолированы в специальных типах (например, IO).
- Это даёт ссылочную прозрачность: любой вызов функции можно заменить её результатом без изменения поведения программы.

В Haskell вы не можете написать "нечистую" функцию в обычном смысле — побочные эффекты обязательно упаковываются в монады, чаще всего в IO.
Haskell гарантирует чистоту на уровне системы типов:
- Если функция имеет тип a -> b (без IO, State, и т.д.), она обязана быть чистой.
- Компилятор не позволит вам вызвать putStrLn внутри такой функции.
👍6
Функции высшего порядка

Функция высшего порядка (ФВП, higher-order functions) - это функция, которая принимает другую функцию в качестве аргумента и/или возвращает функцию как результат.
Они нужны для обобщения рекурсивных функций на коллекциях, что позволяет не думать о рекурсии работе с коллекциями. ФВП вводят абстракции, которые позволяют не использовать понятие рекурсии при написании программ. Типовые ФВП входят в стандартную библиотеку.

map

Функция map принимает другую функцию и список в качестве аргументов и применяет эту функцию к каждому элементу в списке.
GHCi> map reverse ["собака", "кот", "лось"]
["акабос","ток","ьсол"]
GHCi> map (+1) [1, 2, 3]
[2, 3, 4]
GHCi> map even [1..5]
[False, True, False, True, False]


filter

Оставляет в списке только те элементы, для которых предикат (функция вида a -> Bool) возвращает True.
GHCi> filter (> 0) [-2, -1, 0, 1, 2]
[1, 2]
GHCi> filter even [1..10]
[2, 4, 6, 8, 10]


foldl и foldr

Сворачивают список в одно значение, используя бинарную функцию и начальное значение. Их две, потому что свёртка может быть левоассоциативной и правоассоциативной.
foldl (+) 0 [1,2,3] -- разворачивается как: ((0 + 1) + 2) + 3  -- → 6
foldr (+) 0 [1,2,3] -- разворачивается как: 1 + (2 + (3 + 0)) -- → 6

Для простых примеров разницы между ними нет, но они есть и весьма существенные.
Проблема foldl:
- Не работает с бесконечными списками (должен пройти весь список, прежде чем вернуть результат).
- Может создавать большие невычисленные выражения (thunks), если не используется строгая версия.
Когда использовать foldr:
- Когда функция не строгая по второму аргументу (например, (:) или &&),
- Для работы с бесконечными списками (если результат можно вычислить частично),
- При построении списков: foldr (:) [] xs == xs.

Есть еще одна версия свёртки находится в модуле Data.List - foldl' - это неленивая версия foldl, которая зачастую более эффективна. Она вычисляет промежуточный результат на каждом шаге, что делает её эффективной по памяти.

💡 Best practise:
- Используй foldr, если можешь (особенно для ленивых или бесконечных структур).
- Используй foldl' вместо foldl, если делаешь строгую свёртку слева.
👍2
Система типов

Система типов в языке haskell - одна из самых мощных и строгих среди современных языков программирования. Она обеспечивает безопасность типов на этапе компиляции, помогает избегать многих классов ошибок и делает код более выразительным и понятным.

Основные черты
- Статическая типизация
Все типы определяются во время компиляции. Это означает, что программа не будет скомпилирована, если типы не согласованы.
- Сильная типизация
Haskell не позволяет неявных преобразований между типами. Например, нельзя сложить Int и String.
- Автоматический вывод типов
Хотя типизация статическая, аннотации типов часто не обязательны - компилятор сам выводит типы на основе использования.
- Полиморфизм
Функции могут работать с любыми типами, не завися от их конкретной реализации (параметрический полиморфизм) или с типами, удовлетворяющими определённым ограничениям (ad hoc полиморфизм через классы типов).

Всё это возможно благодаря использованию модели типов Хиндли-Милнера (HM).
Это классическая система типов, лежащая в основе многих функциональных языков, не только Haskell, но и ML, OCaml и F#. Она обеспечивает автоматический вывод типов для программ без необходимости явно указывать типы в большинстве случаев, при этом сохраняя статическую типизацию и безопасность.

Основные идеи системы Хиндли–Милнера
- Параметрический полиморфизм
HM поддерживает универсальный полиморфизм через схемы типов (type schemes), позволяя функциям работать с любыми типами, если это логически корректно.
- Вывод типов
Сердце HM - алгоритм Хиндли–Милнера, обычно реализуемый как алгоритм W (Algorithm W), разработанный Робином Милнером. Он позволяет автоматически определить наиболее общий тип выражения, используя унификацию. Например для выражения
f x = x + 1

компилятор выводит тип:
f :: Num a => a -> a


Потому что + требует, чтобы x принадлежал классу Num (в чистом HM нет классов типов — они добавлены в Haskell как расширение. Чистый HM работает с простыми типами без ограничений.).
- Лет-полиморфизм (Let-polymorphism)
Ключевая особенность HM: полиморфизм разрешён только для связываний let (или where), но не для аргументов функций.
- Типовые переменные и схемы
Тип — выражение вроде Int, a -> b, [Bool]. Схема типа — тип с кванторами: ∀a.a→a.
- Унификация (Unification)
Процесс нахождения подстановки, делающей два типа эквивалентными.
Пример:
Типы: a -> b и Int -> c
Унификация даёт: a = Int, b = c
Результат: Int -> cc как новая переменная)
Если унификация невозможна (например, Int и Bool), возникает ошибка типов.

Ограничения HM
- Нет полиморфизма высшего ранга (ранг-2 и выше) без расширений.
- Нет подтипов.
- Нет зависимых типов.
- Нет классов типов (это расширение Haskell).
- Мономорфные ограничения (в Haskell: Monomorphism Restriction).
Однако эти ограничения делают вывод типов разрешимым и эффективным (алгоритм W работает за почти линейное время в большинстве случаев).

Haskell изначально основывался на HM. Но позже были добавлены:
- Классы типов (ad hoc полиморфизм),
- Расширения: RankNTypes, GADTs, TypeFamilies и др.
Чистый HM не включает классы типов, но идея вывода наиболее общего типа сохраняется.
👍2
Тесты

В haskell нет стандартных средств для написания тестов, но есть специализированные библиотеки, например hspec.
Далее приведена пошаговая установка для linux, если вы используете windows - сочувствую.

Установка hspec
cabal update && cabal install --package-env=. --lib hspec hspec-contrib QuickCheck HUnit

Далее добавляем его в path (в файле .bashrc)
export PATH="$HOME/.cabal/packages/hackage.haskell.org/:$PATH"

Примечание: hspec можно не устанавливать отдельно, а просто прописать его в зависимости проекта. В этом случае он установиться перед сборкой проекта.

Использование
Создаём новый проект example. Переходим в каталог проекта и находим файл example.cabal.
Находим секцию test-suite example-test и в build-depends: и дописываем зависимости
  hspec >= 2.7
, hspec-discover >= 2.7


Добавляем файл src/MySum.hs
module MySum where

sum' :: Int -> Int -> Int
sum' x y = x + y


Добавляем файл test/Spec.hs
{-# OPTIONS_GHC -F -pgmF hspec-discover #-}


Добавляем тестовый файл test/MySumSpec.hs
module MySumSpec (spec) where

import Test.Hspec
import MySum

spec :: Spec
spec = do
describe "testing sum' function" $ do
it "sum some two integers" $ do
sum' 3 4 `shouldBe` 7
sum' 2 1 `shouldBe` 3
👍1
Стандарты языка

В отличии от C++ который обновляет стандарт каждые 3 года, haskell сразу появился красивый (ну, почти).

На сегодня есть два стандарта - Haskell 98 и Haskell 2010.
Haskell 2010 является обновлённой и уточнённой версией стандарта 98 года. Основная цель стандарта 2010 года - не радикальное изменение языка, а устранение неоднозначностей, добавление небольших, но полезных улучшений и поддержка современных практик программирования.

Haskell 2010 — это надмножество Haskell 98, но с минимальными и совместимыми расширениями. Большинство современных реализаций (например, GHC) поддерживают оба стандарта, но по умолчанию используют Haskell 2010. На практике почти весь код на Haskell 98 корректно компилируется в режиме Haskell 2010.

Ключевые различия
- Поддержка иерархии модулей
Haskell 98: не имел поддержки иерархических имён модулей (например, Data.List, Control.Monad).
Haskell 2010: официально ввёл иерархию имён модулей, соответствующую практике, уже давно используемой в библиотеках (например, в GHC и Hugs).
- Поддержка внешних вызовов (FFI - Foreign Function Interface)
Haskell 98: FFI не входил в стандарт.
Haskell 2010: включил стандартизированный FFI, что позволяет вызывать функции, написанные на других языках (например, C), и наоборот.
- Уточнения и исправления
Haskell 2010 устранил неоднозначности и ошибки в спецификации Haskell 98. Улучшены и уточнены правила разбора (parsing), семантика и поведение некоторых конструкций.
- Незначительные синтаксические улучшения.
- Безопасность и чистота
Haskell 2010 уточнил разделение между чистыми функциями и вводом-выводом, чтобы избежать потенциальных проблем с побочными эффектами.

Примечание из комментариев

Хотя официальным (который признан комитетом) последним стандартом является Haskell 2010, фактически стандартом языка является версия компилятора GHC. Это произошли из-за того, что комитет по стандарту давно не собирался, и потому что сейчас остался только один компилятор для haskell, и в стандарте языка, как методе синхронизации разных компиляторов, нет смысла. Версии компилятора GHC2021 и GHC2024 определяют набор расширений языка, который включен компилятором по умолчанию.
Создание собственных типов

Создать собственный тип в haskell можно тремя способами - синоним типа, новый тип - обёртка и алгебраический тип данных.

Синонимы

Создаются именно как синонимы существующего типа, то есть использование их равнозначно, но повышает читаемость. Объявляется с помощью ключевого слова type:
type Name = String
type Surname = String
type Age = Int


Использование type не накладывает дополнительных гарантий безопасности типов. Это значит, что в функцию, которая принимает Name можно передать Surname или String и компилятор не заругает.

Тип-обёртка

Является абстракцией с нулевой стоимостью за счёт оптимизации компилятора. И в отличии от type добавляет действительно новый тип, который не приводится неявно к базовому типу.
newtype Name = Name String
newtype Surname = Surname String

someFunc :: Name -> Bool
...

В таком случае в someFunc мы не сможем просто передать Surname или String.

Алгебраические типы данных

Позволяет создавать безопасные перечисления и комбинировать данные в структуры.
data Color = Red | Green | Blue

data Person = Person Surname Name


Вытащить значения Surname и Name из Person можно через сопоставление с образцом:
getName :: Person -> Name
getName (Person n _) = n


Однако проще использовать синтаксис записей, который вводит именования для полей, которые так же являются функциями доступа:
data Person = Person {
name :: Name,
surname :: Surname
}
...
somePerson = Person { name = "SomeName", surname = "SomeSurname"}
...
getName :: Person -> Name
getName = name
👍2
Операторы композиции и применения

Композиция (.) и применение ($) - это два оператора которые позволяют писать более выразительный код, убирая лишние скобочки.

Оператор композиции объединяет две функции воедино:
sum . take 2 . sort

при этом последующая функция должна принимать параметр того же типа, что и результат предыдущей. А для того, чтобы использовать композицию с функцией, которая принимает несколько аргументов придётся использовать частичное применение для уменьшения количества аргументов.
Оператор композиции правоассоциативен, то есть сначала применяется функция справа, а затем — слева. В приведённом выше примере сначала выполняется sort затем take 2 (частично применена), затем sum. При этом все три функции объединяются в одну новую.

Оператор применения позволяет избавится от множества лишних скобок при вызове нескольких функций подряд: f $ g $ h x эквивалентно выражению f (g (h x)). Он тоже правоассоциативен (в отличии от простого применения функции, которое левоассоциативно) и имеет самый низкий приоритет (в отличии от простого применения функции, которое имеет высший приоритет).

result1 = sum . take 2 . sort $ [1,3,5,2]
-- эквивалентно
result2 = (sum (take 2 (sort [1,3,5,2])))
-- эквивалентно
result3 = (sum . take 2 . sort) $ [1,3,5,2]
-- эквивалентно
result4 = (sum . take 2 . sort) [1,3,5,2]
👍3
Бесточечная нотация

Еще одним замечательным применением оператора композиции (.) является реализация т.н. бесточечной нотации. Точкой тут именуется не символ ., он то как раз есть, а аргумент функции. Этот приём позволяет убрать лишние переменные из определения функции.
Так как композиция функций создаёт новую функцию, то можно составить её так, чтобы аргументы можно было не указывать:
sumTwoSmallestNumbers :: [Int] -> Int
sumTwoSmallestNumbers xs = sum . take 2 . sort $ xs
-- эквивалентно
sumTwoSmallestNumbers = sum . take 2 . sort


Такое определение функции более "функционально" и описывает её как набор необходимых действий, а не набор действий для конкретного аргумента (xs).
Хотя в простых, как выше, случаях бесточечная нотация упрощает понимание написанного, её применение иногда переусложняет код. Для больших и длинных функций лучше использовать выражения let или where для присвоения имён промежуточным вычислениям.
👍2
Классы типов

Классы типов - это интерфейсы в мире haskell. Они определяют, какими функциями должен обладать тип, и выступают как элемент системы ограничений для определения полиморфного поведения.
Классы типов могут содержать не только описание функций, которыми должен обладать тип, но и переменные:
class MyStrangeType a where
someFunc :: a -> a
someValue :: a


Классы типов нужны, чтобы ограничить множество типов, которое может работать с полиморфными функциями.
addThenDouble :: Num a => a -> a -> a
addThenDouble x y = (x + y) * 2

Функция addThenDouble может работать со всеми типами, реализующими класс типов Num.

Основные классы типов
- Ord - для типов, которые поддерживают отношение порядка, требует реализации функций сравнения.
- Eq - требует проверки на равенство/неравенство.
- Show - требует представление значения типа строкой.
- Read - принимает строку и возвращает экземпляр типа Read.
- Bounded - требует определения верхней и нижней границы значений.
- Enum - требует реализации функций для последовательно упорядоченных типов.
- Num - класс типов для чисел, требует реализации операций над числами.
- Integral - класс типов для целых чисел.
- Floating - класс типов для чисел с плавающей точкой.

Чтобы узнать определения класса типов в ghci нужно воспользоваться командой :info
ghci> :info Num
type Num :: * -> Constraint
class Num a where
(+) :: a -> a -> a
(-) :: a -> a -> a
(*) :: a -> a -> a
negate :: a -> a
abs :: a -> a
signum :: a -> a
fromInteger :: Integer -> a
-- и многое другое
Undefined

Значение undefined в haskell представляет собой ошибочное вычисление. Если мы попытаемся его вычислить (например использовать в каком-то выражении), то получим ошибку времени выполнения.
Зачем?
Значение undefined используется как заглушка для еще не реализованного функционала, которая позволит скомпилировать и запустить приложение, чтобы проверить другие его части.
someFunction :: Int -> String
someFunction x = undefined -- пока не реализовано


Благодаря тому, что haskell реализует ленивые вычисления undefined можно использовать как значение в программе, пока кто-то не захочет его вычислить. Понятное дело, что ему не место в "продакшене", но в процессе разработки undefined позволяет лучше сосредоточится на некоторых отдельных функциях или кейсах с которыми сейчас идёт работа.
👍2
deriving

Для нового типа можно автоматически сгенерировать необходимые для классов типов функции с помощью ключевого слова deriving. Это сработает если класс имеет тривиальную реализацию.
data Person = Person { 
name :: String
, age :: Int }
deriving (Show, Eq)

Новые функции будут сгенерированы в процессе компиляции. Это может не получится, если класс сложный, тогда компилятор выдаст ошибку.

Классы типов могут зависеть от других классов типов. Например Ord зависит от Eq, значит перед реализацией Ord должен быть реализован Eq. Если же вы напишете только реализацию Ord, то получите ошибку при компиляции - No instance for 'Eq имя_вашего_класса' ....

Отдельно стоит отметить автоматическую реализацию Ord для перечислений:
data MyEnum = A | B | C | D deriving (Eq, Ord)

Компилятор будет использовать порядок следования конструкторов данных (A | B | C | D) для реализации упорядочивания экземпляров MyEnum, то есть стандартно B будет меньше C, но больше A.
👍2
Ограничение синтаксиса записей

В haskell есть удобный способ задать имена полей типа, который автоматически создаст функции доступа к значениям - это синтаксис записей. Однако этот удобный способ обладает довольно странной особенностью.
Если вы создаёте 2 типа T1 и T2 в одном пространстве имён и у них есть одинаковые имена полей, то вы получите ошибку компиляции.
data T1 = T1 { t1 :: Int, t2 :: Int }
data T2 = T2 { t1 :: String, t3 :: Int }


Компиляция примера выше выдаст ошибку Multiple declarations of ‘t1’. Это происходит из-за того, что имена функций в пространстве имён должны иметь уникальные имена.
🤔1