learn haskell
25 subscribers
2 photos
2 links
изучение языка haskell
Download 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
Реализация классов типов

Иногда тривиальные реализации через deriving не подходят или не могут быть автоматически собраны. В этом случае писать реализацию придётся самому. А если есть собственные классы типов, то тут уже никуда не деться. Реализация классов типов выполняется с помощью ключевого слова instance и называется экземпляром. То есть в отличии от языков, реализующих ООП, экземпляром именуется не конкретный объект, а, говоря языком ООП, класс реализующий интерфейс.
data TrafficLight = Red | Yellow | Green

instance Eq TrafficLight where
Red == Red = True
Green == Green = True
Yellow == Yellow = True
_ == _ = False

instance Show TrafficLight where
show Red = "Красный свет"
show Yellow = "Жёлтый свет"
show Green = "Зелёный свет"


В примере выше TrafficLight реализует классы типов Eq и Show.
Хотя в классе Eq описаны функции == и /= следует переопределить только одну функцию в объявлении экземпляра класса. Это называется минимальным полным определением класса типов – имеется в виду минимум функций, которые надо реализовать, чтобы наш тип мог вести себя так, как предписано классом. Для класса Ord таким минимумом является метод compare, который возвращает значение типа Ordering.

Именно через использование различных экземпляров классов типов реализуются ограничения безудержного полиморфизм в haskell.
Можно записать такую функцию:
sum x y = x + y

без описания сигнатуры. Если у вас отключены варнинги как ошибки (никогда так не делайте!!!), то такое запустится и даже будет работать, пока не случится страшное, и кто-то не захочет сложить число и строку. Тогда случится ошибка компиляции в месте вызова. Однако разумнее ограничить возможные типы для функции через указание классов типов, которые должен реализовывать аргумент функции:
someFunc :: (Bounded a, Enum a) => a -> a -> a


Использование классов типов позволяет чётко выразить мысли разработчика, ограничив использование функций лишь экземплярами, которые обладают определённым поведением. В примере выше, функция someFunc работает с любыми экземплярами, которые реализуют одновременно классы типов Bounded и Enum.
👍2
Тип-сумма и тип-произведение

Тип-сумма (sum type) и тип-произведение (product type) — это фундаментальные концепции из теории типов, которые отражают структуру составных типов данных в haskell.

Типы-произведения определяются посредством комбинирования двух или более существующих типов с помощью «И»:
haskell 
data AuthorName = AuthorName String String


это аналог простых структур в императивных языках, например в C:
struct author_name {
char *first_name;
char *last_name;
};


В большинстве языков это единственный способ группировки типов в структуры.

Типы-суммы позволяют комбинировать типы с помощью «ИЛИ». Самым понятным примером тут является тип Bool который может принимать одно из двух значений - True или False (вот оно ИЛИ тут и проявляется).
data Bool = False | True


Типы-суммы позволяют создавать типы набор полей которых может быть различен, но это не мешает им оставаться одним и тем же типом. Например тип имени можно представить как "имя + фамилия" или "имя+отчество+фамилия" или "инициалы+фамилия":
type FirstName = String
type LastName = String
type MiddleName = String

data Name = Name FirstName LastName
| NameWithMiddle FirstName MiddleName LastName
| TwoInitialsWithLast Char Char LastName


Чтобы получить доступ к полю каждого возможного варианта типа нужно указать имя конструктора при определении функции:
lastName :: Name -> String
lastName (Name _ n) = n
lastName (NameWithMiddle _ _ n) = n
lastName (TwoInitialsWithLast _ _ n) = n
👍2
Полугруппы

Полугруппы - это одна из концепций языка haskell позволяющая комбинировать элементы одного типа. Можно взять две сущности одного типа и комбинировать их для получения новой сущности того же типа. Аналогией может служить операция слияния различных коллекций для объединения их в новую коллекцию того же типа.

Класс Semigroup содержит всего один метод - операцию <>:
class Semigroup a where
(<>) :: a -> a -> a

Для типа Integer операция <> идентична сложению:
instance Semigroup Integer where
(<>) x y = x + y

Для каждого типа может быть довольно большое количество возможных инстансов Semigroup, но определить можно только один. Для Integer это может быть сложение, умножение, максимум и др.

Операция <> должна удовлетворять закону ассоциативности - (x <> y) <> z = x <> (y <> z). Операция сложения удовлетворяет этому закону - 1+(2+3) = (1+2)+3, а вот вычитание или деление - нет. Ассоциативность является одним из законов классов типов (laws).
1
Monoid

Monoid - это Semigroup с нейтральным элементом. Нейтральность элемента означает, что x <> id = x и id <> x = x (id от identity).
Нейтральный элемент даёт пустое (нулевое) состояние для обработки коллекций - свёрток, композиций, и позволяет работать с пустыми списками. Semigroup же подходит для случаев, когда у типа нет пустого состояния или оно бессмысленно.
class Semigroup a => Monoid a where
mempty :: a -- нейтральный элемент
mappend :: a -> a -> a -- композиция (<>)
mconcat :: [a] -> a


Для сложения целых чисел нейтральным элементом будет 0, для умножения - 1.
instance Monoid Integer where
mappend = (+)
mempty = 0

-- или (но не одновременно)
instance Monoid Integer where
mappend = (*)
mempty = 1


Для определения экземпляра Monoid минимальным полным определением являются mempty и mappend, а mconcat определяется автоматически:
mconcat = foldr mappend mempty


У класса Monoid есть свои законы:
1. mappend mempty x = x и mappend x mempty = x - если что-то прибавить к пустому элементу, то это что-то и получим
2. mappend x (mappend y z) = mappend (mappend x y) z - ассоциативность
3. mconcat = foldr mappend mempty - определение mconcat из минимального полного определения класса.

Многие встроенные функции haskell требуют, чтобы типом аргумента был экземпляр Monoid. Monoid используется для свёрток через функции fold и foldMap (например суммирование чисел, склеивание строк), накопления результатов в рекурсивных алгоритмах, параллельных обработок (а-ля MapReduce).
totalSalary :: [Int] -> Int
totalSalary = getSum . foldMap Sum

totalSalary [3000, 4000, 5000]
totalSalary []


Semigroup часто в этих случаях не подходит потому что может быть пустой список для свёртки, пустой результат на определённом этапе, пустое дерево и т.д., а обрабатывать вручную случай для проверки на пустоту не хочется.

Моноиды так же могут обладать дополнительными свойствами:
- коммутативность - все элементы можно поменять местами (1+2==2+1). Не для всех это работает, для списков например нет ([1,2]++[3,4] != [3,4]++[1,2])
- идемпотентность - если x <> x = x. Списки и числа под сложение/умножение - не идемпотентные, а множество и объединения по логическому ИЛИ или И - идемпотентные.
- дуальность - наличие моноида с обратным порядком применения операций.
👍2
Hackage и Hoogle

Hackage ( https://hackage.haskell.org ) - это официальный репозиторий пакетов haskell с открытым исходным кодом. Название репозитория происходит от слияния слов Haskell и package. Он содержит тысячи библиотек и приложений, организованных в пакеты. Hackage является основным источником пакетов для менеджеров сборки cabal и stack.
Стандартная библиотека включает в себя более сотни модулей, но есть среди них самый известный, носящий имя Prelude, его содержимое автоматически импортируется во все модули наших проектов.
Каждый пакет включает:
- исходный код
- метаданные (.cabal-файл с зависимостями, версией, лицензией)
- документацию (Haddock)

Hoogle ( https://hoogle.haskell.org ) - это специализированная поисковая система для hackage. Hoogle позволяет искать функции не только по имени, но еще и по типу (например: (a -> b) -> [a] -> [b]).
Hoogle можно установить локально
stack install hoogle

или интегрировать в редактор через плагины. Eсли вы используете HLS (haskell language server), то он будет использовать hoogle установленный в системе.
👍3
Модули

При помощи модулей можно группировать программные сущности в отдельные файлы. Это декларации самого верхнего уровня которые включают в себя остальные - функции, типы данных, классы типов и экземпляры типов.
В одном файле можно определить только один модуль. Делается это так:
module ModuleName where


Имя модуля должно совпадать с именем файла (не обязательно, просто так транслятор сам найдёт нужный модуль) и начинаться с заглавной буквы. Строка определения модуля должна быть первой в файле (комментарии не в счёт).
При объявлении модуля можно инкапсулировать его содержание, сделав так, чтобы наружу торчали только нужные идентификаторы. Для этого нужно их перечислить в скобках после имени файла:
modele ModuleName (
MyType0,
MyType1 (...),
MyType2 (WithOne, WithTwo)
myFunction
) where


При этом у типов можно ограничить экспорт конструкторов. В примере выше у типа MyType0 не экспортируются конструкторы и, следовательно, мне этого модуля будет доступен только идентификатор типа. Для MyType1 экспортируются все конструкторы, а для MyType2 - только перечисленные.

Импортировать модуль можно с помощью ключевого слова import. При этом можно указать имена сущностей, которые мы хотим импортировать (как в Module2) или импортировать все доступные (как в Module1). В стандартной библиотеке принято именование модулей, которое включает относительный путь к файлу модуля от точки входа в корневой каталог модуля.
import Module1
import Module2 (
myFunction1,
myFunction2
)
import Data.List -- лежит в data/list.hs


Естесвенно, что импортировать можно только те сущности, которые экспортируются указанным модулем, иначе будет ошибка.
При конфликте имён сущностей в импортируемых модулях можно:
- скрыть идентификатор с помощью hiding
- использовать квалифицированный импорт (qualified). Тогда все сущности их модуля придётся предварять именем модуля. При это можно задать псевдоним модуля через as.
import MyModule hiding (
hidingFunction
)
import qualified OtherModule as OM
...
otherFunction data = map OM.otherFunction data
...


Так как экземпляры классов не имеют имён, то они экспортируются/импортируются всегда вместе с классами и типами для которых определёны.
👍21
картинки к посту ниже
🔥1
EMACS как IDE

Emacs - хорошая операционная система, вот только редактор в ней подкачал.

Уже пару лет я использую doom emacs для ... практически всего что связано с разработкой и, естественно, для освоения haskell. С шуткой выше давно можно не согласиться (со второй частью), если использовать evil mode для emacs, который активирует vim-комбинации клавиш для редактирования текста и манипуляции с буферами.
Использовать emacs для написания кода - это сплошное удовольствие. Это не только продвинутый редактор, но и почти всё, что потребуется для разработки в одном флаконе - терминалы, справка, git-клиент, в нем можно открыть pdf или сайт как в браузере (без js и картинок, конечно).
Благодаря встроенным функциям и lsp сразу становится доступно множество функций по запуску тестов, переименованию в проекте, просмотру определений, навигации по проекту и т.д.

Чтобы настроить emacs для haskell у вас должен быть установлен hls (haskell language server).
Далее в файле init.el нужно раскомментировать строчку
(haskell +lsp +tree-sitter)    ; a language that's lazier than I am

она позволит использовать haskell-mode для редактирования файлов .hs.

Чтобы работать с комфортом нам понадобятся еще некоторые настройки - хуки для отображения сигнатуры типов и автоиндентации, а так же автоформатирование файла при сохранении:
(add-hook 'haskell-mode-hook 'turn-on-haskell-doc-mode)
(add-hook 'haskell-mode-hook 'turn-on-haskell-indentation)
(add-hook 'haskell-mode-hook #'lsp)
(add-hook 'haskell-literate-mode-hook #'lsp)
(setq haskell-stylish-on-save t)
👍2