learn haskell
25 subscribers
2 photos
2 links
изучение языка haskell
Download Telegram
Переменные

Математические аналогии в Haskell не заканчиваются на функциях. В Haskell не переменные, а объявления, или константные переменные, если брать аналогию из других языков.
Например, записать где-то
x = 3
...
x = 5

не получится, компилятор откажется такое компилировать.
Если рассматривать переменные, как естественный способ упрощения записи вычислений, то Haskell предлагает 2 варианта, которые можно комбинировать - блоки let и where:
calcChange :: Int -> Int -> Int
calcChange owed given =
let change = given - owed
in
if change > 0
then change
else 0

или
calcChange :: Int -> Int -> Int
calcChange owed given = if change > 0
then change
else 0
where change = given - owed

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

GHCi — это интерактивная среда (REPL — Read-Eval-Print Loop) для языка программирования Haskell, входящая в состав компилятора GHC (Glasgow Haskell Compiler).

Основные функции GHCi
- Выполнение Haskell-выражений в реальном времени;
- Можно вводить выражения, функции, определения и сразу видеть результат;
- Загрузка и тестирование модулей;
- Можно загрузить .hs-файлы (например, :load MyModule.hs) и работать с их содержимым;
- Отладка и инспекция типов. GHCi позволяет проверять типы выражений с помощью команды :type (сокращённо :t).

Когда GHCi особенно полезен
- изучение Haskell;
- быстрое прототипирование кода;
- тестирование функций без компиляции всего проекта.

Полезные команды
:load / :l - загрузить файл
:reload / :r - перезагрузить текущий файл
:type / :t - показать тип выражения
:info / :i - информация о функции, типе или классе типов
:help / :? - справка по командам
:quit / :q - выйти из GHCi
:set +t - После выполнения команды интерпретатор будет выводить на экран не только результат вычисления выражения, но и его тип.
:set +s - После выполнения команды интерпретатор будет выводить на экран не только результат вычисления выражения, но и статистику вычислений.

Пример использование GHCI
Запускаем интерпретатор командой
ghci

и далее пишем что хотим
ghci> let factorial 0 = 1; factorial n = n * factorial (n - 1)
ghci> factorial 5
120
ghci> :t factorial
factorial :: (Eq p, Num p) => p -> p
Такое разное ветвление

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

Условный оператор if
отличается от аналогов в императивных языках тем, что он является выражением, а не управляющей конструкцией.
Это означает, что:
- if всегда должен иметь и then, и else;
- if ... then ... else ... возвращает значение, которое можно использовать, например, присвоить переменной или передать в функцию;
- обе ветки (then и else) должны иметь один и тот же тип.

Пример:
signum' :: Int -> Int
signum' x = if x > 0
then 1
else if x < 0
then -1
else 0


Охраняющие (или охранные) выражения (guards)
это удобный и читаемый способ определения функций с несколькими условиями. Они особенно полезны, когда нужно проверить несколько логических условий (например, в зависимости от значений аргументов), и выглядят гораздо элегантнее вложенных if-then-else.
При этом:
- Все ветки (результаты) должны иметь один и тот же тип.
- Если ни одно условие не выполнено и нет otherwise - функция завершится исключением (ошибка времени выполнения). Поэтому почти всегда следует добавлять otherwise в конец.

Пример:
signum'' :: Int -> Int
signum'' x
| x > 0 = 1
| x < 0 = -1
| otherwise = 0


Сопоставление с образцом (pattern matching)
одна из самых мощных и характерных черт языка Haskell. Оно позволяет разбирать структуры данных (списки, кортежи, пользовательские типы и т.д.) прямо в определении функций, присваиваниях или ветвлениях, проверяя их форму и извлекая значения.
При этом:
- Порядок важен: сопоставление проверяется сверху вниз.
- Образцы должны быть исчерпывающими: если возможен случай, не охваченный ни одним паттерном, компилятор выдаст предупреждение (или ошибка, если включён -Werror).
- Нельзя использовать произвольные функции в образцах - только конструкторы данных, константы и переменные.

Пример:
isEmpty :: [a] -> Bool
isEmpty [] = True
isEmpty _ = False
👍2
Лямбда-исчисление
Это формальная система в математической логике и теоретической информатике, предназначенная для описания вычислений с помощью функций. Оно было разработано в 1930-х годах Алонзо Чёрчем как способ формализовать понятие вычислимой функции и лежит в основе функционального программирования.

Основные идеи
1. Всё - это функция. В лямбда-исчислении нет чисел, строк или других типов данных в привычном смысле. Всё представляется как функции. Даже числа, логические значения и операции над ними кодируются с помощью функций (например, через кодировку Чёрча).
2. Лямбда-выражения - это основные "строительные блоки" системы. Есть три типа выражений:
- Переменные: например, x, y.
- Абстракции (определения функции): записываются как λx.M, что означает "функция от аргумента x, возвращающая выражение M".
- Аппликации (применение функции): записывается как (M N), что означает "применить функцию M к аргументу N".
3. β-редукция - основное правило вычисления. Это процесс подстановки аргумента в тело функции:
(λx.M) N → M[x := N]

То есть, функция λx.M применяется к N, и все вхождения x в M заменяются на N.
4. α-конверсия - переименование связанных переменных. Например, λx.x и λy.y считаются эквивалентными, потому что имена связанных переменных не важны.
5. η-эквивалентность - принцип расширяемости. Говорит, что если две функции ведут себя одинаково на всех аргументах, то они эквивалентны:
λx.(f x) ≡ f, если x не свободен в f


Значение лямбда-исчисления
Теоретическое:
- Эквивалентно машине Тьюринга по вычислительной мощности (тезис Чёрча–Тьюринга).
- Лежит в основе теории типов, формальных языков и логики.
Практическое:
- Влияние на функциональные языки программирования: Haskell, Lisp, ML, Scheme, Scala, F# и др.
- Концепция анонимных функций (лямбда-выражений) в современных языках (Python, JavaScript, Java и др.) идёт именно отсюда.

Типизированное и нетипизированное λ-исчисление
- Нетипизированное - самый простой и выразительный вариант, но в нём возможны "странные" выражения, не имеющие нормальной формы (например, (λx.x x)(λx.x x) - зацикливается).
- Типизированное - каждое выражение имеет тип, что исключает некоторые виды ошибок и делает систему безопасной (например, простое типизированное λ-исчисление, система F, исчисление конструкций и т.д.).
👍1
Лямбда-функции в Haskell
это функция без имени, которая создаётся на месте и может быть передана в качестве аргумента или использована в выражении. В Haskell синтаксис лямбда-функций основан на лямбда-исчислении, где λ обозначается как \.
Haskell — один из самых чистых функциональных языков, и его дизайн во многом основан на лямбда-исчислении, что делает лямбда-функции естественной частью языка.
Лямбда-функции так же являются значениями и при именовании они становятся эквивалентны обычным функциям:
-- Лямбда-функция
addOne = \x -> x + 1
-- То же самое как именованная функция
addOne x = x + 1

Внутренне Haskell воспринимает все функции как лямбда-выражения.

Синтаксис
\аргументы -> выражение

- \ — начало лямбда-выражения (напоминает греческую букву λ).
- аргументы — список параметров функции (через пробел).
- -> — стрелка, обозначающая тело функции.
- выражение — то, что возвращает функция.

Когда использовать лямбда-функции?
Рекомендуется использовать, когда:
- Функция короткая и используется однажды.
- Она передаётся в функцию высшего порядка (например, map, filter, foldr).
- Нужно захватить переменные из окружения (замыкание).
Не рекомендуется, если:
- Лямбда-функция слишком сложная.
- Она используется несколько раз — лучше дать ей имя.
- Код становится менее читаемым.

Примеры
Prelude> (\x -> x + 1) 3
4

Prelude> (\x -> x ++ "!") "Hello"
"Hello!"

Prelude> map (\x -> x ^ 2) [1..5]
[1,4,9,16,25]

Prelude> filter (\(a, b) -> a > b) [(1,2), (3,2), (4,4), (5,3)]
[(3,2),(5,3)]
Хаскелл Брукс Карри
американский математик и логик, чьи работы оказали огромное влияние на развитие математической логики, теории вычислений и функционального программирования.

«Logic is the anatomy of thought.»


Хотя Карри не был первым, кто открыл идею, названную его именем, именно его систематическая работа привлекла к ней широкое внимание.

Результатами его работы являются:
1. Каррирование (Currying). Это преобразование функции от нескольких аргументов в последовательность функций от одного аргумента. Например, функция f(x, y) преобразуется в g(x)(y). Хотя идея восходит к Мозесу Шёнфинку и Готтлобу Фреге, именно Карри глубоко исследовал её в контексте комбинаторной логики - поэтому процесс получил его имя.

2. Комбинаторная логика. Карри развил комбинаторную логику - систему, в которой функции строятся без переменных, с использованием базовых "комбинаторов" (например, S, K, I). Эта система стала альтернативой лямбда-исчислению и важной вехой в теории вычислимости.

3. Парадокс Карри. Выявил логический парадокс (называемый парадоксом Карри), показывающий, что в некоторых системах логики можно вывести любое утверждение из самоссылающегося высказывания вида:
«Если это утверждение истинно, то 1 = 2».

Это важный вклад в понимание ограничений формальных систем.

Почему язык программирования называется Haskell?
Язык программирования Haskell (созданный в 1990 году) назван именно в честь Хаскелла Карри - как дань уважения его вкладу в основы функционального программирования, особенно:
- лямбда-исчисление,
- каррирование,
- теорию типов.

Интересный факт: сам Карри не работал с компьютерами - он был чистым математиком и логиком. Но его идеи стали фундаментом для функциональных языков, включая Haskell, ML, Lisp и другие.

Хаскелл Карри - один из отцов современной логики и теоретической информатики. Его имя носит:
- Процесс каррирования (currying),
- Язык программирования Haskell,
- Парадокс Карри,
- Единица измерения "карри" в шуточной академической культуре (для "логической выразительности").
Функции как значения первого класса

Функции в 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