Kitabı oxu: «Курс Хаскель от джуна до мидла»

Şrift:

ГЛАВА 1. ВВЕДЕНИЕ В HASKELL И ФУНКЦИОНАЛЬНОЕ ПРОГРАММИРОВАНИЕ

1.1. Что такое Haskell и зачем он нужен

Haskell — это чисто функциональный язык программирования с сильной статической типизацией, ленивыми вычислениями и мощной системой типов. Он был создан в конце 1980-х — начале 1990-х годов группой исследователей как стандартный язык для исследований в области функционального программирования. Назван в честь математика Хаскелла Брукса Карри.

Основные особенности Haskell:

• Чистота (purity). Функции не имеют побочных эффектов. Одна и та же функция с одними и теми же аргументами всегда возвращает один и тот же результат.

• Ленивые вычисления (lazy evaluation). Выражения вычисляются только тогда, когда их значение действительно нужно.

• Сильная статическая типизация с выводом типов (type inference). Компилятор сам выводит большинство типов.

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

• Мощная система типов, включая type classes, higher-kinded types, GADTs и т.д.

• Отсутствие изменяемого состояния по умолчанию (immutable by default).

Зачем изучать Haskell в 2020–2020-х годах?

1. Понимание функционального программирования на глубоком уровне.

2. Улучшение качества кода даже в других языках (Java, C#, Python, JavaScript, Rust, Scala).

3. Работа с современными системами, где Haskell используется: финансы (Standard Chartered, Barclays), блокчейн (Cardano), компиляторы, анализ данных, DevOps-инструменты.

4. Развитие мышления: Haskell заставляет думать о данных и трансформациях, а не о последовательности команд.

5. Высокая надёжность программ благодаря системе типов.

1.2. Императивное vs Функциональное мышление

Императивный подход (C, Java, Python в классическом стиле):

"Сделай шаг 1, потом шаг 2, измени переменную x, потом проверь условие..."

Функциональный подход:

"Опиши, что есть результат, через композицию чистых функций".

Пример. Подсчёт суммы квадратов чётных чисел в списке.

Императивно (псевдокод):

sum = 0

for x in list:

if x % 2 == 0:

sum = sum + x * x

Функционально на Haskell:

sumOfSquaresOfEvens = sum . map (^2) . filter even

Или более явно:

sumOfSquaresOfEvens xs = sum (map (^2) (filter even xs))

Здесь каждая функция делает одну вещь, и они комбинируются.

1.3. Основные концепции, которые нужно усвоить с самого начала

• Значение (value) и выражение (expression).

• Функция как значение первого класса.

• Рекурсия вместо циклов.

• Неизменяемость данных.

• Типы как контракты.

• Композиция функций.

1.4. Краткая история и экосистема

• 1987–1990: разработка языка.

• 1990: первый отчёт (Haskell 1.0).

• 1998: Haskell 98 (стандарт на долгие годы).

• 2010: Haskell 2010.

• Сегодня: GHC (Glasgow Haskell Compiler) — основной компилятор, постоянно развивается (GHC 9.x).

• Cabal и Stack — системы сборки.

• Hackage — центральный репозиторий пакетов.

• Stackage — стабильные наборы пакетов.

1.5. Кому подходит этот курс

Курс рассчитан на тех, кто:

• уже умеет программировать хотя бы на одном языке (Python, JavaScript, Java, C++ и т.д.);

• хочет перейти от junior-уровня понимания FP к уверенному middle-уровню;

• готов писать много кода и разбирать ошибки компилятора.

Если вы совсем новичок в программировании — сначала освойте основы любого императивного языка, потом возвращайтесь.

1.6. Как работать с этой книгой

1. Читайте главу.

2. Набирайте все примеры в GHCi или в файле.

3. Модифицируйте примеры.

4. Выполняйте упражнения.

5. Не бойтесь ошибок компилятора — это ваш лучший учитель.

1.7. Первые полезные ресурсы

• Официальный сайт: haskell.org

• Книга "Learn You a Haskell for Great Good!" (есть русский перевод).

• "Haskell Programming from First Principles" (платная, очень глубокая).

• Документация GHC и Hoogle (поиск по типам и функциям).

• Reddit r/haskell, Haskell Discourse, Telegram-чаты.

В следующей главе мы настроим окружение и напишем первую программу.

Упражнения к главе 1

1. Объясните своими словами, чем чистая функция отличается от процедуры с побочными эффектами.

2. Приведите 3 примера задач, которые удобнее решать функционально, чем императивно.

3. Найдите в интернете 5 компаний, которые используют Haskell в продакшене, и кратко опишите, для чего.

ГЛАВА 2. УСТАНОВКА ОКРУЖЕНИЯ, GHCI И ПЕРВЫЙ КОД

2.1. Установка GHC и инструментов

Рекомендуемый способ в 2024–2026 годах — использовать GHCup.

На Linux / macOS:

curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh

После установки доступны команды:

• ghcup — менеджер версий

• ghc — компилятор

• ghci — интерактивная оболочка (REPL)

• cabal — система сборки

• stack — альтернативная система сборки (многие до сих пор предпочитают)

Проверка:

ghc --version

ghci

На Windows лучше использовать официальный установщик или WSL2 + GHCup.

2.2. Первый запуск GHCi

Открываем терминал и пишем:

$ ghci

GHCi, version 9.6.x: https://www.haskell.org/ghc/ :? for help

Prelude>

Prelude — это стандартный модуль, который загружается по умолчанию.

Простые вычисления:

Prelude> 2 + 2

4

Prelude> 7 * 8

56

Prelude> 2 ^ 10

1024

Prelude> sqrt 16

4.0

Prelude> pi

3.141592653589793

Строки:

Prelude> "Hello, " ++ "Haskell!"

"Hello, Haskell!"

Логика:

Prelude> True && False

False

Prelude> True || False

True

Prelude> not True

False

2.3. Основные команды GHCi

:t выражение — показать тип

:i имя — информация о типе/классе/функции

:l файл.hs — загрузить файл

:r — перезагрузить последний файл

:q — выход

:? — справка

:set +t — показывать типы результатов

:set prompt "λ> " — красивый промпт

Пример:

Prelude> :t 5

5 :: Num a => a

Prelude> :t "hello"

"hello" :: String

Prelude> :t True

True :: Bool

Prelude> :t not

not :: Bool -> Bool

2.4. Первый файл .hs

Создаём файл Hello.hs:

-- Hello.hs

module Main where

main :: IO ()

main = putStrLn "Привет, Haskell!"

Компиляция и запуск:

$ ghc Hello.hs

$ ./Hello

Привет, Haskell!

Или через runhaskell (без компиляции в бинарник):

$ runhaskell Hello.hs

2.5. Простейшие определения в файле

-- Simple.hs

module Simple where

double :: Int -> Int

double x = x * 2

square :: Int -> Int

square x = x * x

sumOfSquares :: Int -> Int -> Int

sumOfSquares x y = square x + square y

Загружаем в GHCi:

$ ghci Simple.hs

*Simple> double 21

42

*Simple> sumOfSquares 3 4

25

2.6. Полезные настройки

В файле ~/.ghci можно добавить:

:set prompt "λ> "

:set +t

:set -Wall

Флаг -Wall включает почти все предупреждения — очень полезно с самого начала.

2.7. Cabal и Stack — краткий обзор

Для небольших экспериментов достаточно GHCi и ghc.

Для реальных проектов:

Создание проекта Cabal:

$ cabal init

$ cabal build

$ cabal run

Stack:

$ stack new my-project

$ cd my-project

$ stack build

$ stack exec my-project-exe

В этом курсе до главы 14–15 мы будем в основном работать с отдельными .hs файлами и GHCi. Позже перейдём к проектам.

2.8. Типичные проблемы при установке

• На macOS иногда нужно установить Command Line Tools.

• На Linux — libgmp, zlib и другие зависимости.

• Конфликт версий — используйте ghcup set.

• В Windows — лучше WSL2.

2.9. Редакторы и IDE

• VS Code + Haskell extension (на базе HLS — Haskell Language Server)

• Emacs + haskell-mode

• Vim/Neovim + coc-haskell или haskell-tools.nvim

• IntelliJ + Haskell plugin (менее популярен)

HLS даёт подсветку, автодополнение, переход к определению, type holes и многое другое.

Упражнения к главе 2

1. Установите GHCup и проверьте версии ghc, cabal, stack.

2. Напишите программу, которая выводит ваше имя и текущий год.

3. Создайте файл с тремя функциями: сложение, умножение и возведение в степень. Загрузите в GHCi и протестируйте.

4. Настройте красивый промпт в GHCi.

ГЛАВА 3. ТИПЫ, ВЫРАЖЕНИЯ И БАЗОВЫЙ СИНТАКСИС

3.1. Всё есть выражение

В Haskell почти всё является выражением, которое имеет значение и тип.

5 -- Int (или Num a => a)

True -- Bool

'a' -- Char

"hello" -- String (это [Char])

[1,2,3] -- [Int]

(1, "hello", True) -- (Int, String, Bool)

3.2. Базовые типы

• Int — целые числа фиксированного размера (обычно 64 бита)

• Integer — целые числа произвольной точности

• Float — числа с плавающей точкой одинарной точности

• Double — двойной точности

• Bool — True | False

• Char — символ

• String — синоним для [Char]

Проверка:

Prelude> :t 42

42 :: Num a => a

Prelude> :t (42 :: Int)

42 :: Int

Prelude> :t (42 :: Integer)

42 :: Integer

3.3. Списки и кортежи

Списки — однородные:

[1,2,3,4] :: [Int]

["a","b"] :: [String]

[] :: [a] -- пустой список любого типа

Кортежи — разнородные, фиксированной длины:

(1, "hello") :: (Int, String)

(True, 3.14, 'x') :: (Bool, Double, Char)

() :: () -- unit type

3.4. Функции как значения

Функция тоже имеет тип:

not :: Bool -> Bool

length :: [a] -> Int

take :: Int -> [a] -> [a]

(++) :: [a] -> [a] -> [a]

Оператор тоже функция:

(+) :: Num a => a -> a -> a

3.5. Аннotation типов

Хотя Haskell умеет выводить типы, явные аннотации очень полезны:

add :: Int -> Int -> Int

add x y = x + y

Или в выражениях:

(5 :: Int) + (7 :: Int)

3.6. Условные выражения

if условие then выражение1 else выражение2

Важно: и then, и else обязательны, и оба выражения должны иметь один тип.

absolute :: Int -> Int

absolute n = if n >= 0 then n else -n

Можно писать в несколько строк:

absolute n =

if n >= 0

then n

else -n

3.7. Охраняющие условия (guards)

Часто удобнее guards:

absolute :: Int -> Int

absolute n

| n >= 0 = n

| otherwise = -n

otherwise — это просто True.

Можно несколько:

bmiTell :: Double -> Double -> String

bmiTell weight height

| bmi <= 18.5 = "Недостаточный вес"

| bmi <= 25.0 = "Норма"

| bmi <= 30.0 = "Избыточный вес"

| otherwise = "Ожирение"

where bmi = weight / height ^ 2

3.8. where и let

where — локальные определения после функции:

cylinder :: Double -> Double -> Double

cylinder r h =

sideArea + 2 * topArea

where

sideArea = 2 * pi * r * h

topArea = pi * r ^ 2

let — выражение:

cylinder r h =

let sideArea = 2 * pi * r * h

topArea = pi * r ^ 2

in sideArea + 2 * topArea

В GHCi let используется для определений:

Prelude> let x = 5

Prelude> let y = 7

Prelude> x + y

12

3.9. Комментарии

-- однострочный комментарий

{-

многострочный

комментарий

-}

3.10. Отступы — это синтаксис

Haskell чувствителен к отступам (layout rule).

Правило: код, который относится к одному блоку, должен иметь одинаковый или больший отступ.

Плохо:

let x = 5

y = 7 -- ошибка

Хорошо:

let x = 5

y = 7

Или с фигурными скобками (редко используют):

let { x = 5; y = 7 }

3.11. Операторы и их приоритет

Инфиксные операторы:

2 + 3 * 4 -- 14, потому что * имеет больший приоритет

Можно делать функцию инфиксной с помощью обратных кавычек:

div 10 2

10 `div` 2

И наоборот — оператор в префиксной форме:

(+) 2 3

3.12. Полезные встроенные функции

fst, snd — для пар

head, tail, last, init

null, length

take, drop

reverse

elem

maximum, minimum

sum, product

and, or

zip, zipWith

words, unwords, lines, unlines

Примеры:

Prelude> head [1,2,3]

1

Prelude> tail [1,2,3]

[2,3]

Prelude> take 3 [1..10]

[1,2,3]

Prelude> [1..5]

[1,2,3,4,5]

Prelude> [1,3..10]

[1,3,5,7,9]

Prelude> elem 3 [1,2,3,4]

True

Упражнения к главе 3

1. Напишите функцию, которая возвращает большее из двух чисел (без использования max).

2. Напишите функцию signum (знак числа): -1, 0, 1.

3. Создайте функцию, которая по двум катетам считает гипотенузу.

4. Напишите функцию, которая проверяет, является ли год високосным.

5. Используя where, напишите функцию площади треугольника по формуле Герона.

ГЛАВА 4. ФУНКЦИИ, ПАТТЕРН-МАТЧИНГ И РЕКУРСИЯ

4.1. Определение функций

Самый простой способ:

add :: Int -> Int -> Int

add x y = x + y

Можно частично применять:

add5 = add 5

-- add5 :: Int -> Int

4.2. Паттерн-матчинг

Одна из самых мощных возможностей языка.

Факториал через паттерны:

factorial :: Integer -> Integer

factorial 0 = 1

factorial n = n * factorial (n - 1)

Важно: порядок уравнений имеет значение. Более специфичные паттерны должны идти раньше.

Паттерны для списков:

head' :: [a] -> a

head' [] = error "пустой список"

head' (x:_) = x

tail' :: [a] -> [a]

tail' [] = error "пустой список"

tail' (_:xs) = xs

Длина списка:

length' :: [a] -> Int

length' [] = 0

length' (_:xs) = 1 + length' xs

Сумма:

sum' :: Num a => [a] -> a

sum' [] = 0

sum' (x:xs) = x + sum' xs

4.3. as-паттерны

Иногда нужно и разобрать, и сохранить целое:

capital :: String -> String

capital "" = "Пустая строка"

capital all@(x:xs) = "Первая буква " ++ [x] ++ " остальное: " ++ xs

4.4. Паттерны в лямбдах и case

case выражение of

паттерн1 -> результат1

паттерн2 -> результат2

Пример:

describeList :: [a] -> String

describeList xs = "Список " ++ case xs of

[] -> "пустой"

[x] -> "из одного элемента"

_ -> "из нескольких элементов"

4.5. Рекурсия — основной способ итерации

В Haskell нет циклов for/while в классическом виде. Всё делается рекурсией или функциями высшего порядка.

Хвостовая рекурсия (хвостовой вызов) важна для производительности, но GHC умеет оптимизировать многие случаи.

Пример аккумулятора:

sumTail :: Num a => [a] -> a

sumTail xs = go 0 xs

where

go acc [] = acc

go acc (x:xs) = go (acc + x) xs

4.6. Рекурсия по нескольким аргументам

zip' :: [a] -> [b] -> [(a,b)]

zip' _ [] = []

zip' [] _ = []

zip' (x:xs) (y:ys) = (x,y) : zip' xs ys

4.7. Взаимная рекурсия

even' :: Int -> Bool

even' 0 = True

even' n = odd' (n - 1)

odd' :: Int -> Bool

odd' 0 = False

odd' n = even' (n - 1)

4.8. Локальные функции с where/let

Часто вспомогательные функции делают локальными:

quicksort :: Ord a => [a] -> [a]

quicksort [] = []

quicksort (x:xs) =

let smaller = filter (<= x) xs

bigger = filter (> x) xs

in quicksort smaller ++ [x] ++ quicksort bigger

Или с where:

quicksort (x:xs) = quicksort smaller ++ [x] ++ quicksort bigger

where

smaller = [a | a <- xs, a <= x]

bigger = [a | a <- xs, a > x]

4.9. Ошибки и частичные функции

error :: String -> a

undefined :: a

Лучше избегать частичных функций (head, tail, !! и т.д.) в продакшен-коде. Позже мы научимся использовать Maybe и Either.

4.10. Примеры полезных рекурсивных функций

-- Разворот списка

reverse' :: [a] -> [a]

reverse' [] = []

reverse' (x:xs) = reverse' xs ++ [x]

-- Более эффективно с аккумулятором

reverse'' :: [a] -> [a]

reverse'' xs = go [] xs

where

go acc [] = acc

go acc (x:xs) = go (x:acc) xs

-- Элемент по индексу

(!!) :: [a] -> Int -> a

[] !! _ = error "индекс слишком большой"

(x:_) !! 0 = x

(_:xs) !! n = xs !! (n - 1)

-- take

take' :: Int -> [a] -> [a]

take' n _

| n <= 0 = []

take' _ [] = []

take' n (x:xs) = x : take' (n - 1) xs

Упражнения к главе 4

1. Напишите функцию, вычисляющую n-е число Фибоначчи (сначала наивно, потом с аккумуляторами).

2. Реализуйте функцию elem самостоятельно через рекурсию.

3. Напишите функцию, которая вставляет элемент в отсортированный список, сохраняя порядок.

4. Реализуйте merge (слияние двух отсортированных списков).

5. Напишите функцию, которая проверяет, является ли список палиндромом.

ГЛАВА 5. СПИСКИ, СТРОКИ И ОСНОВНЫЕ ОПЕРАЦИИ

5.1. Списки — центральная структура данных

Синтаксис:

[1,2,3]

1:2:3:[] -- то же самое

[1..10]

[1,3..20]

['a'..'z']

5.2. Основные операции

(++) :: [a] -> [a] -> [a] -- конкатенация

(:) :: a -> [a] -> [a] -- cons

head, tail, last, init

null :: [a] -> Bool

length :: [a] -> Int

reverse :: [a] -> [a]

take, drop, splitAt

elem, notElem

maximum, minimum (требуют Ord)

sum, product (требуют Num)

and, or (для [Bool])

any, all

concat :: [[a]] -> [a]

concatMap

zip, zipWith, unzip

words, unwords, lines, unlines

5.3. Генераторы списков (list comprehensions)

Очень мощный и читаемый синтаксис:

[x * 2 | x <- [1..10]]

[x | x <- [1..20], even x]

[(x,y) | x <- [1..3], y <- [1..3]]

[(x,y) | x <- [1..3], y <- [1..3], x + y == 4]

С несколькими фильтрами:

[x | x <- [1..100], x `mod` 3 == 0, x `mod` 5 == 0]

С let внутри:

[x * x | x <- [1..10], let y = x * x, y > 50]

Для строк:

[c | c <- "Hello World", c `elem` ['A'..'Z']]

5.4. Строки

String = [Char]

Поэтому все списковые функции работают со строками.

"hello" ++ " world"

reverse "haskell"

length "привет" -- осторожно с Unicode!

Для серьёзной работы с текстом позже будем использовать Text (пакет text).

5.5. Бесконечные списки

Благодаря ленивости:

ones = 1 : ones

[1..]

fibs = 0 : 1 : zipWith (+) fibs (tail fibs)

take 20 fibs

5.6. Полезные приёмы

-- Удаление дубликатов (требует Eq)

nub :: Eq a => [a] -> [a]

-- Сортировка

import Data.List (sort)

sort [3,1,4,1,5,9]

-- Группировка

group [1,1,1,2,2,3,3,3,3]

-- Подинтервалы

inits, tails

-- Транспонирование

transpose [[1,2,3],[4,5,6]]

5.7. Примеры реальных задач

-- Частотный анализ

import Data.List (sort, group)

frequency xs = map (\g -> (head g, length g)) . group . sort $ xs

-- Проверка на анаграмму

isAnagram s1 s2 = sort s1 == sort s2

-- Разбиение на слова и подсчёт

wordCount = length . words

Упражнения к главе 5

1. Напишите функцию, которая возвращает все чётные квадраты чисел от 1 до n.

2. С помощью list comprehension создайте таблицу умножения 10×10.

3. Реализуйте функцию, которая удаляет все гласные из строки.

4. Напишите функцию, которая находит все простые числа до n (решето Эратосфена).

5. Создайте бесконечный список факториалов и возьмите первые 10.

ГЛАВА 6. ВЫСШИЕ ФУНКЦИИ: MAP, FILTER, FOLD И КОМПАНИЯ

6.1. Функции высшего порядка

Функция высшего порядка — это функция, которая принимает функции как аргументы или возвращает функцию.

map :: (a -> b) -> [a] -> [b]

filter :: (a -> Bool) -> [a] -> [a]

foldr :: (a -> b -> b) -> b -> [a] -> b

foldl :: (b -> a -> b) -> b -> [a] -> b

6.2. map

map (*2) [1..5] -- [2,4,6,8,10]

map toUpper "haskell" -- "HASKELL"

map length ["hello", "world"] -- [5,5]

Реализация:

map' :: (a -> b) -> [a] -> [b]

map' _ [] = []

map' f (x:xs) = f x : map' f xs

6.3. filter

filter even [1..10]

filter (>5) [1..10]

filter (/= ' ') "hello world"

Реализация:

filter' _ [] = []

filter' p (x:xs)

| p x = x : filter' p xs

| otherwise = filter' p xs

6.4. foldr и foldl

foldr — свёртка справа:

foldr (+) 0 [1,2,3,4] = 1 + (2 + (3 + (4 + 0)))

foldl — свёртка слева:

foldl (+) 0 [1,2,3,4] = (((0 + 1) + 2) + 3) + 4

Для ассоциативных операций и конечных списков результат одинаковый, но производительность и поведение на бесконечных списках различаются.

sum = foldr (+) 0

product = foldr (*) 1

and = foldr (&&) True

or = foldr (||) False

length = foldr (\_ acc -> acc + 1) 0

reverse = foldl (\acc x -> x : acc) []

6.5. foldl' — строгая версия

В Data.List есть foldl' — строгий слева fold. Рекомендуется использовать его вместо foldl почти всегда, чтобы избежать накопления thunk'ов.

6.6. Другие полезные функции

zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]

zipWith (+) [1,2,3] [4,5,6] -- [5,7,9]

takeWhile, dropWhile

span, break

any, all

find

partition

nubBy, groupBy, sortBy, on

6.7. Композиция функций

(.) :: (b -> c) -> (a -> b) -> a -> c

f . g = \x -> f (g x)

Пример:

sumOfSquaresOfEvens = sum . map (^2) . filter even

Очень важный стиль — point-free (бесточечный):

countEven = length . filter even

6.8. Лямбда-выражения

\x -> x * 2

\x y -> x + y

\(x,y) -> x + y

\xs -> length xs > 5

6.9. Сечения операторов

(2*) -- умножение на 2

(*2) -- то же

(>5) -- проверка > 5

(/10) -- деление на 10

(10/) -- 10 делить на что-то

6.10. Примеры

-- Подсчёт слов длиной больше 5

longWords = length . filter ((>5) . length) . words

-- Среднее значение

average xs = sum xs / fromIntegral (length xs)

-- Нормализация списка

normalize xs = map (/ sum xs) xs

Упражнения к главе 6

1. Реализуйте map, filter, foldr самостоятельно.

2. Напишите функцию, которая возводит все элементы списка в квадрат и отфильтровывает те, что меньше 20.

3. С помощью fold реализуйте length, reverse, map, filter.

4. Напишите point-free версию функции, которая считает количество гласных в строке.

5. Используя zipWith, создайте список частичных сумм.

ГЛАВА 7. МОДУЛИ, ПРОСТРАНСТВА ИМЁН И ОРГАНИЗАЦИЯ КОДА

7.1. Зачем нужны модули

• Разделение кода на логические части

• Сокрытие реализации (encapsulation)

• Избежание конфликтов имён

• Повторное использование

7.2. Объявление модуля

module Geometry

( sphereVolume

, sphereArea

, cubeVolume

, cubeArea

) where

-- реализации...

Всё, что не экспортировано, остаётся приватным.

7.3. Импорт

import Data.List

import Data.List (nub, sort)

import Data.List hiding (nub)

import qualified Data.Map as M

import Data.Map (Map)

import Data.Map as Map

Примеры:

import qualified Data.Map as Map

Map.lookup "key" myMap

7.4. Стандартные модули, которые нужно знать

Prelude — загружается автоматически

Data.List

Data.Maybe

Data.Either

Data.Tuple

Data.Char

Data.Map (контейнер)

Data.Set

Data.Text (позже)

Control.Monad

Control.Applicative

System.IO

Text.Printf

и многие другие

7.5. Структура проекта

my-project/

src/

MyProject/

Lib.hs

Types.hs

Utils.hs

Main.hs

test/

my-project.cabal

stack.yaml (если stack)

README.md

7.6. Иерархия модулей

module MyProject.Types where

module MyProject.Utils where

module MyProject.Lib where

import MyProject.Types

import MyProject.Utils

7.7. Экспорт типов и конструкторов

data Point = Point Double Double

-- экспортировать только тип, без конструкторов:

module Geometry (Point) where

-- экспортировать всё:

module Geometry (Point(..)) where

-- или выборочно:

module Geometry (Point(Point)) where

7.8. Прагмы и расширения языка

{-# LANGUAGE OverloadedStrings #-}

{-# LANGUAGE FlexibleContexts #-}

{-# LANGUAGE InstanceSigs #-}

Пока можно не углубляться, но знать, что они существуют, нужно.

7.9. Хорошие практики организации

• Один модуль — одна ответственность

• Не делать гигантских модулей

• Явно указывать экспорт

• Использовать qualified import для больших библиотек (Map, Set, Text, ByteString)

• Избегать import *

Упражнения к главе 7

1. Создайте модуль Geometry с функциями для круга, прямоугольника и треугольника. Экспортируйте только нужное.

2. Напишите программу, которая использует qualified import Data.List и Data.Char.

3. Разделите небольшой проект на 3–4 модуля.

ЧАСТЬ II. ТИПЫ И АБСТРАКЦИИ

ГЛАВА 8. АЛГЕБРАИЧЕСКИЕ ТИПЫ ДАННЫХ (ADT)

8.1. data — создание новых типов

data Bool = False | True

deriving (Eq, Ord, Show, Read, Enum, Bounded)

data Color = Red | Green | Blue

deriving (Eq, Show)

data Point = Point Double Double

deriving (Show)

data Shape

= Circle Double

| Rectangle Double Double

| Triangle Double Double Double

deriving (Show)

8.2. Работа с ADT через паттерн-матчинг

area :: Shape -> Double

area (Circle r) = pi * r * r

area (Rectangle w h) = w * h

area (Triangle a b c) =

let p = (a + b + c) / 2

in sqrt (p * (p - a) * (p - b) * (p - c))

8.3. Record syntax

data Person = Person

{ firstName :: String

, lastName :: String

, age :: Int

, height :: Float

} deriving (Show)

p = Person "Иван" "Иванов" 30 1.80

firstName p

age p

Обновление (на самом деле создание нового):

p2 = p { age = 31 }

8.4. Параметризованные типы

data Maybe a = Nothing | Just a

data Either a b = Left a | Right b

data Tree a = Empty | Node a (Tree a) (Tree a)

8.5. Рекурсивные типы

data List a = Nil | Cons a (List a)

data Nat = Zero | Succ Nat

data Expr

= Const Int

| Add Expr Expr

| Mul Expr Expr

| Var String

8.6. newtype

newtype Zoom = Zoom Double

deriving (Show)

Отличие от data: в runtime нет накладных расходов, это просто обёртка.

newtype Identity a = Identity a

newtype Sum a = Sum a

newtype Product a = Product a

8.7. type — синонимы

type String = [Char]

type Name = String

type Point = (Double, Double)

type AssocList k v = [(k, v)]

8.8. Примеры проектирования

-- Деньги

newtype Money = Money Integer deriving (Eq, Ord, Show)

-- Идентификаторы

newtype UserId = UserId Int deriving (Eq, Show)

newtype OrderId = OrderId Int deriving (Eq, Show)

-- Состояния

data OrderStatus

= Pending

| Paid

| Shipped

| Delivered

| Cancelled

deriving (Eq, Show)

Упражнения к главе 8

1. Создайте тип данных для представления двоичного дерева поиска.

2. Напишите функции insert, lookup, toList для него.

3. Создайте тип Expr для арифметических выражений и функцию eval.

4. Используя record syntax, опишите сущность "Книга" и функции работы с ней.

5. Сделайте newtype для Email и Phone и функции валидации.

ГЛАВА 9. TYPE CLASSES: EQ, ORD, SHOW, READ, NUM И ДРУГИЕ

9.1. Что такое type class

Type class — это интерфейс (набор методов), который могут реализовывать разные типы.

class Eq a where

(==) :: a -> a -> Bool

(/=) :: a -> a -> Bool

x /= y = not (x == y) -- реализация по умолчанию

9.2. Основные классы

Eq, Ord, Show, Read, Enum, Bounded

Num, Real, Integral, Fractional, Floating, RealFrac, RealFloat

9.3. deriving

data Color = Red | Green | Blue

deriving (Eq, Ord, Show, Read, Enum, Bounded)

Теперь можно:

Red == Green

Red < Green

show Red

read "Red" :: Color

succ Red

minBound :: Color

9.4. Ручная реализация

instance Eq Color where

Red == Red = True

Green == Green = True

Blue == Blue = True

_ == _ = False

9.5. Ограничения в сигнатурах

elem :: Eq a => a -> [a] -> Bool

sort :: Ord a => [a] -> [a]

show :: Show a => a -> String

9.6. Num и компания

(+) (-) (*) negate abs signum fromInteger

Для целых: div, mod, quot, rem

Для дробных: (/), recip, fromRational

9.7. Создание собственного класса

class Describable a where

describe :: a -> String

instance Describable Bool where

describe True = "Истина"

describe False = "Ложь"

instance Describable Int where

describe n = "Число " ++ show n

9.8. Подклассы

class Eq a => Ord a where

compare :: a -> a -> Ordering

(<) (<=) (>) (>=)

max, min

class Functor f where ...

9.9. Полезные советы

• Всегда добавляйте deriving (Eq, Show) минимум.

• Для ключей Map/Set нужен Ord.

• Не злоупотребляйте Read (лучше парсеры).

• Используйте newtype + instance, когда нужно особое поведение (Monoid для Sum/Product).

Упражнения к главе 9

1. Создайте тип DaysOfWeek и реализуйте Eq, Ord, Show вручную.

2. Сделайте класс YesNo (как в LYAH) и instances для Bool, Maybe, списков, Int.

3. Реализуйте instance Num для типа, представляющего комплексные числа (упрощённо).

ГЛАВА 10. MAYBE, EITHER И ОБРАБОТКА ОШИБОК

10.1. Проблема частичных функций

head [] — runtime exception

"abc" !! 5 — exception

div 5 0 — exception

В чистом функциональном мире мы предпочитаем явно обозначать возможное отсутствие значения или ошибку.

10.2. Maybe

data Maybe a = Nothing | Just a

safeHead :: [a] -> Maybe a

safeHead [] = Nothing

safeHead (x:_) = Just x

safeDiv :: Double -> Double -> Maybe Double

safeDiv _ 0 = Nothing

safeDiv x y = Just (x / y)

10.3. Работа с Maybe

fromMaybe :: a -> Maybe a -> a

fromMaybe def Nothing = def

fromMaybe _ (Just x) = x

maybe :: b -> (a -> b) -> Maybe a -> b

isJust, isNothing

mapMaybe

catMaybes

10.4. Either

data Either a b = Left a | Right b

По соглашению Left — ошибка, Right — успех.

safeDivE :: Double -> Double -> Either String Double

safeDivE _ 0 = Left "Деление на ноль"

safeDivE x y = Right (x / y)

10.5. Цепочки вычислений

До появления do-нотации и монад приходилось писать так:

f :: Int -> Maybe Int

f x = case safeHead [1..x] of

Nothing -> Nothing

Just y -> case safeDiv (fromIntegral y) 2 of

Nothing -> Nothing

Just z -> Just (round z)

Это неудобно. Именно поэтому появились Functor, Applicative и Monad.

10.6. Either для накопления ошибок (позже)

Обычный Either останавливается на первой ошибке.

Для накопления используют Validation из пакетов (semigroupoids и т.д.) или свои типы.

10.7. Практические рекомендации

• Предпочитайте Maybe/Either исключениям в чистом коде.

• В IO-исключениях используйте Control.Exception, но аккуратно.

• Для парсинга и валидации — Either или специальные типы.

• Всегда обрабатывайте оба случая.

Упражнения к главе 10

1. Напишите safeTail, safeLast, safeInit.

2. Реализуйте функцию lookup в ассоциативном списке, возвращающую Maybe.

3. Напишите функцию, которая читает число из строки (используя reads) и возвращает Either.

4. Создайте цепочку из трёх функций, возвращающих Maybe, и обработайте все случаи.

ГЛАВА 11. ФУНКТОРЫ (FUNCTOR)

11.1. Идея

Functor — это то, что можно "отобразить" (map over).

class Functor f where

fmap :: (a -> b) -> f a -> f b

Инфиксный оператор: <$>

fmap = (<$>)

11.2. Instances

instance Functor Maybe where

fmap _ Nothing = Nothing

fmap f (Just x) = Just (f x)

instance Functor [] where

fmap = map

instance Functor (Either e) where

fmap _ (Left e) = Left e

fmap f (Right x) = Right (f x)

instance Functor ((->) r) where

fmap = (.)

11.3. Законы функторов

1. fmap id = id

2. fmap (f . g) = fmap f . fmap g

Они гарантируют, что fmap не меняет структуру, а только значения внутри.

11.4. Примеры

fmap (+1) (Just 5) -- Just 6

fmap (*2) [1,2,3] -- [2,4,6]

fmap (++ "!") (Right "Hi") -- Right "Hi!"

fmap length (Just "hello") -- Just 5

11.5. Functors вложенные

fmap (fmap (*2)) [Just 1, Nothing, Just 3]

-- [Just 2, Nothing, Just 6]

11.6. Полезные функции

void :: Functor f => f a -> f ()

(<$) :: a -> f b -> f a

Упражнения к главе 11

1. Реализуйте instance Functor для Tree a.

2. Реализуйте instance Functor для Pair a b (мапит только второй компонент).

3. Проверьте законы функтора для Maybe на нескольких примерах.

4. Напишите функцию, которая применяет функцию ко всем значениям внутри Either в списке.

ГЛАВА 12. APPLICATIVE

12.1. Зачем нужен Applicative

Functor позволяет применять функцию одного аргумента.

А что если функция находится внутри контекста, и аргументы тоже?

class Functor f => Applicative f where

pure :: a -> f a

(<*>) :: f (a -> b) -> f a -> f b

12.2. Maybe

instance Applicative Maybe where

pure = Just

Nothing <*> _ = Nothing

_ <*> Nothing = Nothing

Just f <*> Just x = Just (f x)

Пример:

(+) <$> Just 3 <*> Just 5 -- Just 8

(+) <$> Just 3 <*> Nothing -- Nothing

12.3. Списки

instance Applicative [] where

pure x = [x]

fs <*> xs = [f x | f <- fs, x <- xs]

(*) <$> [1,2,3] <*> [10,100]

-- [10,100,20,200,30,300]

12.4. Either

instance Applicative (Either e) where

pure = Right

Left e <*> _ = Left e

_ <*> Left e = Left e

Right f <*> Right x = Right (f x)

12.5. Полезные комбинаторы

liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c

Yaş həddi:
12+
Litresdə buraxılış tarixi:
15 sentyabr 2026
Yazılma tarixi:
2026
Həcm:
190 səh. 1 illustrasiya
Müəllif hüququ sahibi:
Автор
Yükləmə formatı:

Oxşar kitablar