Курс Хаскель от джуна до мидла
Курс Хаскель от джуна до мидла

Полная версия

Курс Хаскель от джуна до мидла

Настройки чтения
Размер шрифта
Высота строк
Поля
На страницу:
2 из 4

( 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".

На страницу:
2 из 4