Haskell
Haskell за 18 минут: весь язык на одной странице через закомментированный код — типы, функции, списки, монады, ленивость и классы типов.
Haskell — чисто функциональный язык с ленивыми вычислениями и сильной статической типизацией. Никаких присваиваний и циклов: только выражения, функции и рекурсия. Весь язык — ниже, прямо в комментариях к рабочему коду.
Вывод и комментарии
Точка входа — функция main типа IO (). Запуск: runghc file.hs или ghc + бинарник.
-- Однострочный комментарий начинается с двух дефисов
{- Многострочный
комментарий
{- и он вкладывается -} -}
main :: IO () -- сигнатура: main возвращает действие IO
main = do -- do-блок объединяет действия ввода-вывода
putStrLn "Привет, Haskell!" -- печать строки с переводом строки
putStr "без \n" -- печать без перевода строки
print 42 -- print = putStrLn . show, для любого Show
print [1, 2, 3] -- => [1,2,3]
Базовые типы и значения
Тип пишется через :: («имеет тип»). Компилятор почти всегда выводит типы сам, но указывать их — хороший тон.
x :: Int -- машинное целое (ограниченное, ~64 бита)
x = 7
big :: Integer -- целое произвольной точности
big = 2 ^ 200 -- огромное число без переполнения
pi' :: Double -- число с плавающей точкой двойной точности
pi' = 3.14159
flag :: Bool -- True или False
flag = True
letter :: Char -- одиночный символ в одинарных кавычках
letter = 'λ'
greeting :: String -- String = [Char], список символов
greeting = "строка"
-- Арифметика и преобразования:
-- 7 `div` 2 => 3 (целочисленное деление)
-- 7 `mod` 2 => 1 (остаток)
-- 7 / 2 => ошибка для Int; для Double => 3.5
half :: Double
half = fromIntegral x / 2 -- => 3.5 (fromIntegral: Int -> Double)
Функции
Функция — это выражение. Аргументы пишут через пробел, без скобок и запятых.
-- Сигнатура: два Int на входе, Int на выходе
add :: Int -> Int -> Int
add a b = a + b
-- Аппликация: пробел, а не скобки
summ = add 2 3 -- => 5
-- Любую функцию двух аргументов можно вызвать инфиксно через `имя`
infixSum = 2 `add` 3 -- => 5
-- А оператор — вызвать как обычную функцию в скобках
prefixSum = (+) 2 3 -- => 5
-- Свой инфиксный оператор
(.+.) :: Int -> Int -> Int
a .+. b = a + b + 1
-- Лямбда (анонимная функция): \аргументы -> тело
square = \x -> x * x -- square 5 => 25
-- Частичное применение: даём не все аргументы — получаем функцию
addTen :: Int -> Int
addTen = add 10 -- ждёт второй аргумент; addTen 5 => 15
Списки
Список однороден (все элементы одного типа) и реализован как односвязный.
nums :: [Int]
nums = [1, 2, 3, 4, 5]
-- (:) — «cons», добавить элемент в голову; [] — пустой список
front = 0 : nums -- => [0,1,2,3,4,5]
-- (++) — конкатенация двух списков
joined = [1, 2] ++ [3, 4] -- => [1,2,3,4]
-- Голова, хвост, длина, индексация
h = head nums -- => 1
t = tail nums -- => [2,3,4,5]
n = length nums -- => 5
at = nums !! 2 -- => 3 (индекс с нуля)
-- Диапазоны
r1 = [1 .. 10] -- => [1,2,3,4,5,6,7,8,9,10]
r2 = [2, 4 .. 10] -- => [2,4,6,8,10] (шаг по двум первым)
letters = ['a' .. 'e'] -- => "abcde"
-- Генераторы списков (list comprehensions)
squares = [x * x | x <- [1 .. 5]] -- => [1,4,9,16,25]
evens = [x | x <- [1 .. 20], even x] -- => [2,4,..,20]
pairs = [(a, b) | a <- [1, 2], b <- "ab"] -- => [(1,'a'),(1,'b'),(2,'a'),(2,'b')]
-- Бесконечный список (благодаря ленивости — это нормально)
naturals = [1 ..] -- 1, 2, 3, ... без конца
first5 = take 5 naturals -- => [1,2,3,4,5]
Кортежи и сопоставление с образцом
Кортеж группирует значения разных типов фиксированной длины.
point :: (Int, Int)
point = (3, 4)
mixed :: (String, Int, Bool)
mixed = ("возраст", 30, True)
-- Для пар есть fst и snd
fx = fst point -- => 3
fy = snd point -- => 4
-- Сопоставление с образцом: разбираем структуру прямо в аргументах
dist :: (Double, Double) -> Double
dist (x, y) = sqrt (x * x + y * y) -- dist (3, 4) => 5.0
-- Образцы по списку: голова и хвост
myHead :: [a] -> a
myHead (x : _) = x -- _ — «что угодно, не интересует»
myHead [] = error "пустой список"
-- where: локальные определения после тела функции
quadrArea :: Double -> Double -> Double
quadrArea w h = area
where area = w * h -- видно во всём теле функции
-- let ... in: локальные определения внутри выражения
cube :: Int -> Int
cube n = let sq = n * n in sq * n -- cube 3 => 27
Условия и guards
-- if/then/else — это ВЫРАЖЕНИЕ, ветка else обязательна
abs' :: Int -> Int
abs' n = if n < 0 then -n else n
-- Guards (|): набор условий, выбирается первое истинное
grade :: Int -> String
grade score
| score >= 90 = "отлично"
| score >= 75 = "хорошо"
| score >= 60 = "удовлетворительно"
| otherwise = "неуд" -- otherwise = True, ветка по умолчанию
-- case ... of: сопоставление с образцом внутри выражения
describe :: Int -> String
describe n = case n of
0 -> "ноль"
1 -> "один"
_ -> "много" -- _ ловит остальное
Функции высшего порядка
Функции — обычные значения: их можно передавать и возвращать.
-- map: применить функцию к каждому элементу
m = map (* 2) [1, 2, 3] -- => [2,4,6]
-- filter: оставить элементы по предикату
f = filter even [1 .. 10] -- => [2,4,6,8,10]
-- foldr: свёртка справа, foldr (\x acc -> ...) нач список
sumR = foldr (+) 0 [1, 2, 3, 4] -- => 10 (1+(2+(3+(4+0))))
-- foldl: свёртка слева
sumL = foldl (+) 0 [1, 2, 3, 4] -- => 10 ((((0+1)+2)+3)+4)
-- zipWith: попарно объединить два списка функцией
z = zipWith (+) [1, 2, 3] [10, 20, 30] -- => [11,22,33]
-- Композиция (.): (f . g) x = f (g x)
negSquare = negate . (^ 2) -- negSquare 3 => -9
-- Сначала возводим в квадрат, потом меняем знак
Типы данных
Свои типы объявляют через data. Конструкторы — это функции, создающие значения.
-- Перечисление: несколько конструкторов без полей
data Color = Red | Green | Blue
-- Конструктор с полями (как «класс» с одним вариантом)
data Point = Point Double Double
origin :: Point
origin = Point 0.0 0.0
-- Сумма типов: фигура — либо круг, либо прямоугольник
data Shape = Circle Double | Rect Double Double
area :: Shape -> Double
area (Circle r) = pi * r * r
area (Rect w h) = w * h -- area (Rect 3 4) => 12.0
-- Record-синтаксис: именованные поля + автогеттеры
data User = User { name :: String, age :: Int }
bob :: User
bob = User { name = "Боб", age = 25 }
bobName = name bob -- => "Боб" (name — функция-геттер)
older = bob { age = 26 } -- копия с изменённым полем
-- Maybe: значение есть (Just) или его нет (Nothing) — вместо null
safeDiv :: Int -> Int -> Maybe Int
safeDiv _ 0 = Nothing
safeDiv a b = Just (a `div` b) -- safeDiv 10 2 => Just 5
-- Either: результат — ошибка (Left) или успех (Right)
parseAge :: Int -> Either String Int
parseAge n
| n < 0 = Left "возраст отрицателен"
| otherwise = Right n
Классы типов
Класс типов (typeclass) — это интерфейс: набор операций, которые тип может поддерживать. Не путать с ООП-классами.
-- Eq — сравнение на равенство (==, /=)
-- Ord — упорядочивание (<, >, compare)
-- Show — представление строкой (show)
-- Num — числовые операции (+, *, ...)
-- deriving просит компилятор сгенерировать реализации автоматически
data Suit = Hearts | Spades
deriving (Eq, Ord, Show)
check = Hearts == Hearts -- => True
shown = show Spades -- => "Spades"
-- Объявление своего класса
class Describable a where
describe' :: a -> String
-- Реализация (instance) класса для конкретного типа
instance Describable Suit where
describe' Hearts = "черви"
describe' Spades = "пики"
d = describe' Hearts -- => "черви"
-- Ограничение класса в сигнатуре: (Ord a) => ... читается
-- «для любого a, который умеет упорядочиваться»
maxOf :: (Ord a) => a -> a -> a
maxOf x y = if x > y then x else y
Каррирование и точечный стиль
Все функции в Haskell каррированы: функция нескольких аргументов — это цепочка функций одного аргумента.
-- add :: Int -> Int -> Int на самом деле читается как
-- add :: Int -> (Int -> Int)
-- то есть add 2 возвращает функцию (Int -> Int)
add2 :: Int -> Int
add2 = add 2 -- частичное применение «бесплатно»
-- Сечения операторов — частичное применение инфиксных:
incr = (+ 1) -- incr 5 => 6
halve = (/ 2) -- halve 8 => 4.0
tenTo = (10 ^) -- tenTo 3 => 1000
-- Point-free (бесточечный) стиль: определяем функцию
-- через композицию, не называя аргумент явно
-- С аргументом:
sumEvens :: [Int] -> Int
sumEvens xs = sum (filter even xs)
-- Бесточечно — тот же смысл, аргумент «сокращён»:
sumEvens' :: [Int] -> Int
sumEvens' = sum . filter even
Монады и IO
Монада — это шаблон для последовательных вычислений «с контекстом» (эффект ввода-вывода, возможное отсутствие значения и т.д.). Ключевой оператор — >>= (bind).
-- do-нотация — синтаксический сахар над >>= для любой монады.
-- В IO: <- извлекает результат действия в чистое значение.
ask :: IO ()
ask = do
putStrLn "Как тебя зовут?"
name <- getLine -- name :: String, прочитано из ввода
putStrLn ("Привет, " ++ name)
-- Maybe — тоже монада: цепочка вычислений, которые могут провалиться.
-- Если где-то Nothing — вся цепочка становится Nothing.
lookupAge :: Maybe Int
lookupAge = do
a <- safeDiv 100 5 -- Just 20
b <- safeDiv a 2 -- Just 10
return (a + b) -- => Just 30
-- То же самое явно через >>= (bind):
-- m >>= f «достаёт значение из m и передаёт в f»
chained :: Maybe Int
chained = safeDiv 100 5 >>= \a ->
safeDiv a 2 >>= \b ->
return (a + b) -- => Just 30
-- return оборачивает чистое значение в монаду: return 5 :: Maybe Int => Just 5
Ленивость
Haskell вычисляет выражения только когда результат действительно нужен. Это позволяет работать с бесконечными структурами.
-- Бесконечный список не «зависает»: берём лишь нужный кусок
firstTen = take 10 [1 ..] -- => [1,2,3,4,5,6,7,8,9,10]
-- repeat — бесконечный повтор одного значения
threes = take 4 (repeat 3) -- => [3,3,3,3]
-- cycle — бесконечное повторение списка
abc = take 7 (cycle "abc") -- => "abcabca"
-- Бесконечный ряд Фибоначчи через самоссылку (ленивость + рекурсия)
fibs :: [Integer]
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
fib10 = take 10 fibs -- => [0,1,1,2,3,5,8,13,21,34]
-- Аргументы не вычисляются, пока не понадобятся:
-- const берёт первый аргумент и игнорирует второй
lazy = const 42 (1 `div` 0) -- => 42, деления на ноль не случилось
-- undefined / бесконечность безопасны, если до них не «дотянулись»
safe = head (1 : undefined) -- => 1, хвост не вычисляется