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

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

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

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

Учитель Начальной

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


ГЛАВА 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

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