LEARN X · ЗА 18 МИН

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, хвост не вычисляется
Поддержать проект