- -
- 100%
- +
( 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
liftA3 ...
*> и <*
sequenceA
traverse
for
12.6. Когда использовать
• Когда нужно применить n-арную функцию к значениям в контексте.
• Валидация нескольких полей.
• Независимые эффекты (в отличие от монад, где есть зависимость).
Упражнения к главе 12
1. Реализуйте instance Applicative для Tree (если возможно).
2. Напишите функцию, которая складывает два Maybe Int с помощью Applicative.
3. Используя liftA2, создайте функцию безопасного деления двух Maybe Double.
4. Объясните разницу между fmap и <*> .
ГЛАВА 13. МОНАДЫ (MONAD) — ОТ ТЕОРИИ К ПРАКТИКЕ
13.1. Определение
class Applicative m => Monad m where
return :: a -> m a -- то же, что pure
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
m >> k = m >>= \_ -> k
13.2. Maybe как монада
instance Monad Maybe where
return = Just
Nothing >>= _ = Nothing
Just x >>= f = f x
Теперь цепочки выглядят красиво:
safeHead [1,2,3] >>= \x ->
safeDiv (fromIntegral x) 2 >>= \y ->
Just (y * 10)
Или с do-нотацией:
do
x <- safeHead [1,2,3]
y <- safeDiv (fromIntegral x) 2
return (y * 10)
13.3. do-нотация
Это синтаксический сахар:
do
a <- ma
b <- mb
return (a + b)
разворачивается примерно в:
ma >>= \a ->
mb >>= \b ->
return (a + b)
13.4. Список как монада
instance Monad [] where
return x = [x]
xs >>= f = concatMap f xs
do
x <- [1,2,3]
y <- [10,20]
return (x * y)
-- [10,20,20,40,30,60]
Это похоже на list comprehension.
13.5. Either как монада
instance Monad (Either e) where
return = Right
Left e >>= _ = Left e
Right x >>= f = f x
13.6. Законы монад
1. return a >>= f ≡ f a
2. m >>= return ≡ m
3. (m >>= f) >>= g ≡ m >>= (\x -> f x >>= g)
13.7. Полезные функции
mapM, mapM_
forM, forM_
sequence, sequence_
when, unless
join
liftM, liftM2 (устаревают в пользу <$> и liftA2)
filterM
foldM
replicateM
13.8. Writer, Reader, State — краткий обзор
Позже, когда понадобится:
• Reader — передача окружения
• Writer — накопление лога
• State — изменяемое состояние в чистом виде
• RWS — комбинация
В реальном коде часто используют mtl или transformers.
13.9. Практический пример — безопасные вычисления
type Error = String
type Result a = Either Error a
safeDiv :: Double -> Double -> Result Double
safeDiv _ 0 = Left "деление на ноль"
safeDiv x y = Right (x / y)
compute :: Double -> Double -> Double -> Result Double
compute a b c = do
x <- safeDiv a b
y <- safeDiv x c
return (y + 1)
Упражнения к главе 13
1. Перепишите цепочку Maybe без do-нотации и с do-нотацией.
2. Напишите функцию, которая берёт список Maybe и возвращает Maybe списка (sequence).
3. Реализуйте mapM самостоятельно через рекурсию.
4. Напишите небольшой "калькулятор" на Either, который поддерживает +, -, *, / и обрабатывает ошибки.
ЧАСТЬ III. ПРАКТИКА И УГЛУБЛЕНИЕ (К УРОВНЮ MIDDLE)
ГЛАВА 14. ВВОД-ВЫВОД (IO) И РАБОТА С ФАЙЛАМИ
14.1. Почему IO особенный
IO a — это описание действия, которое, будучи выполненным, произведёт значение типа a (и, возможно, побочные эффекты).
main :: IO ()
14.2. Основные действия
putStr, putStrLn, putChar
getLine, getChar, getContents
print -- putStrLn . show
readFile, writeFile, appendFile
interact
14.3. do-нотация в IO
main = do
putStrLn "Как вас зовут?"
name <- getLine
putStrLn $ "Привет, " ++ name ++ "!"
14.4. return и pure в IO
return "hello" :: IO String
14.5. Работа с файлами
main = do
content <- readFile "input.txt"
writeFile "output.txt" (map toUpper content)
14.6. Ленивый vs строгий IO
readFile — ленивый (может привести к проблемам с блокировками файлов).
Для больших файлов и продакшена лучше использовать Data.Text.IO или ByteString + strict.
14.7. Исключения в IO
import Control.Exception
try, catch, handle, bracket, finally
bracket — очень важный комбинатор для ресурсов:
bracket
(openFile "file.txt" ReadMode)
hClose
(\h -> do ...)
14.8. Аргументы командной строки
import System.Environment
main = do
args <- getArgs
prog <- getProgName
...
14.9. Случайные числа, время и т.д.
import System.Random
import Data.Time
Упражнения к главе 14
1. Напишите программу, которая читает файл и выводит количество строк, слов и символов.
2. Сделайте интерактивный todo-list в консоли (добавление, просмотр, удаление).
3. Напишите программу, которая копирует файл.
4. Обработайте случай отсутствия файла с помощью catch.
ГЛАВА 15. ЛЕНИВОСТЬ, СТРОГОСТЬ И ПРОИЗВОДИТЕЛЬНОСТЬ
15.1. Ленивость по умолчанию
Выражение не вычисляется, пока его значение не понадобится.
Создаются thunk'и.
15.2. Проблемы ленивости
• Утечки памяти (накопление больших цепочек thunk'ов)
• Непредсказуемое использование памяти
• Сложность рассуждения о производительности
15.3. Средства контроля
seq :: a -> b -> b
($!) :: (a -> b) -> a -> b
BangPatterns: f !x = ...
StrictData
foldl' вместо foldl
deepseq / force
15.4. Пример утечки
sum [1..10000000] с foldl (+) 0 может съесть много памяти.
foldl' (+) 0 — нормально.
15.5. Профилирование
ghc -O2 -prof -fprof-auto Program.hs
./Program +RTS -p
15.6. Рекомендации
• Используйте foldl' для свёрток.
• Для больших структур данных смотрите в сторону strict-версий или unboxed types.
• Не бойтесь ! в record-полях, когда нужна строгость.
• Измеряйте, а не гадайте.
Упражнения к главе 15
1. Сравните foldl и foldl' на большом списке (по времени и памяти).
2. Напишите строгую версию списка (data StrictList a = SNil | SCons !a !(StrictList a)).
3. Используйте seq, чтобы заставить вычисление произойти.
ГЛАВА 16. ТЕСТИРОВАНИЕ (HUNIT, QUICKCHECK, TASTY)
16.1. Зачем тестировать
Даже с мощной системой типов нужны тесты.
16.2. HUnit — юнит-тесты
import Test.HUnit
test1 = TestCase (assertEqual "описание" ожидаемое реальное)
16.3. QuickCheck — property-based testing
import Test.QuickCheck
prop_reverse :: [Int] -> Bool
prop_reverse xs = reverse (reverse xs) == xs
quickCheck prop_reverse
16.4. tasty — современный фреймворк
Объединяет HUnit, QuickCheck, SmallCheck и другие.
16.5. Организация тестов в проекте
test-suite в cabal-файле.
16.6. Полезные практики
• Пишите свойства, а не только примеры.
• Используйте Arbitrary instances.
• Тестируйте граничные случаи.
• Интегрируйте тесты в CI.
Упражнения к главе 16
1. Напишите QuickCheck-свойства для своих функций сортировки и поиска.
2. Создайте HUnit-тесты для функций работы с Maybe/Either.
3. Соберите небольшой test-suite с tasty.
ГЛАВА 17. ПАРАЛЛЕЛИЗМ И КОНКУРЕНТНОСТЬ
17.1. Разница
Parallelism — использование нескольких ядер для ускорения.
Concurrency — управление несколькими потоками выполнения (даже на одном ядре).
17.2. parallel пакет
import Control.Parallel.Strategies
parMap, using, rpar, rseq, parList и т.д.
17.3. async
import Control.Concurrent.Async
concurrently, mapConcurrently, race, withAsync
17.4. STM (Software Transactional Memory)
import Control.Concurrent.STM
Очень мощная абстракция для безопасной работы с разделяемым состоянием.
17.5. Базовые примитивы
forkIO, MVar, Chan, QSem и т.д.
17.6. Рекомендации
• Для чистых вычислений — Strategies / parallel.
• Для IO-конкурентности — async.
• Для сложного разделяемого состояния — STM.
• Избегайте голых MVar, если можно обойтись более высокоуровневыми инструментами.
Упражнения к главе 17
1. Распараллельте вычисление тяжёлой чистой функции на списке.
2. Напишите программу, которая одновременно качает несколько URL (с помощью async и http-client).
3. Создайте простой пример с STM.
ГЛАВА 18. ПАРСЕРЫ И РАБОТА С ТЕКСТОМ
18.1. Почему не regex и read
Для серьёзного парсинга нужны комбинаторы.
18.2. Attoparsec / Megaparsec
Megaparsec — более удобный и информативный в ошибках.
Attoparsec — очень быстрый (особенно с ByteString).
18.3. Базовые идеи
satisfy, char, string, takeWhile, many, some, choice, (<|>), try
18.4. Пример простого парсера
parseNumber = ...
parseExpr = ...
18.5. Работа с Text и ByteString
Data.Text
Data.Text.Encoding
Data.ByteString
Data.ByteString.Lazy
18.6. JSON — Aeson
decode, encode, FromJSON, ToJSON
deriveJSON
Упражнения к главе 18
1. Напишите парсер простых арифметических выражений.
2. Спарсите CSV-файл.
3. Напишите FromJSON / ToJSON для своего типа данных.
ГЛАВА 19. ЛУЧШИЕ ПРАКТИКИ, СТИЛЬ КОДА И АРХИТЕКТУРА
19.1. Стиль
• Следуйте Hindley-Milner style guide / style guide от Johan Tibell / Facebook и т.д.
• Имена: camelCase для функций, PascalCase для типов.
• Явные экспорты.
• Не слишком длинные строки.
• Осмысленные имена.
19.2. Организация кода
• Тонкий слой IO на границе.
• Чистая бизнес-логика внутри.
• Новые типы (newtype) для идентификаторов и единиц измерения.
• Избегайте "stringly-typed" программирования.
19.3. Обработка ошибок
• Maybe для отсутствия значения.
• Either / ExceptT для ошибок с информацией.
• Исключения — только для действительно исключительных ситуаций в IO.
19.4. Производительность
• Профилируйте.
• Используйте правильные структуры данных (Vector, HashMap, Text, ByteString).
• Strictness там, где нужно.
19.5. Документация
Haddock-комментарии.
Примеры в документации.
19.6. Архитектурные подходы
• ReaderT + IO для окружения.
• mtl-стиль или concretе monad stacks.
• Free / Final Tagless (для продвинутых).
• Effect systems (polysemy, effectful) — современный тренд.
Упражнения к главе 19
1. Возьмите свой старый код и приведите его к хорошему стилю.
2. Добавьте newtype для всех идентификаторов.
3. Разделите чистую логику и IO.
ГЛАВА 20. МИНИ-ПРОЕКТЫ И ПЕРЕХОД К МИДЛ-УРОВНЮ
20.1. Что должен уметь Middle Haskell-разработчик
• Уверенно писать чистый и IO-код.
• Понимать и использовать Functor / Applicative / Monad / Traversable / Foldable.
• Уметь проектировать ADT.
• Писать тесты (unit + property).
• Работать с текстовыми форматами и парсерами.
• Понимать основы производительности и ленивости.
• Читать чужой код на Hackage.
• Использовать Cabal/Stack, HLS, hoogle.
20.2. Мини-проекты для портфолио
1. Консольный менеджер задач с сохранением в файл / JSON.
2. Простой статический сайт-генератор.
3. Парсер и интерпретатор небольшого языка.
4. Клиент к какому-нибудь публичному API (с async и aeson).
5. Небольшая игра (текстовая RPG или тетрис в терминале).
6. Утилита командной строки с optparse-applicative.
20.3. Дальнейшее развитие
• Книги: "Haskell in Depth", "Parallel and Concurrent Programming in Haskell", "Algebra-Driven Design".




