diff --git a/ru-ru/haskell-ru.html.markdown b/ru-ru/haskell-ru.html.markdown new file mode 100644 index 00000000..1747c393 --- /dev/null +++ b/ru-ru/haskell-ru.html.markdown @@ -0,0 +1,544 @@ +--- +language: haskell +contributors: + - ["Adit Bhargava", "http://adit.io"] + - ["Aleksey Pirogov", "http://astynax.github.io"] +--- + +Haskell разрабатывался, как чистый функциональный язык программирования, применимый на практике. Язык известен благодаря своей системе типов, и "знаменит" благодаря монадам. Меня[^1] же Haskell заставляет возвращаться к себе снова и снова именно своей элегантностью и я[^1] получаю истинное удовольствие, программируя на Haskell. + +```haskell +-- Однострочные комментарии начинаются с двух дефисов +{- Многострочный комментарий +заключается в пару фигурных скобок с дефисами с внутренней стороны. +-} + +------------------------------------------------------- +-- 1. Примитивные типы и простейшие операции над ними +------------------------------------------------------- + +-- Числа объявляются просто +3 -- 3 + +-- Арифметика тоже выглядит вполне ожидаемо +1 + 1 -- 2 +8 - 1 -- 7 +10 * 2 -- 20 +35 / 5 -- 7.0 + +-- Операция деления всегда возвращает действительное число +35 / 4 -- 8.75 + +-- Делим нацело так +35 `div` 4 -- 8 + +-- Булевы значения - тоже примитивные значения +True +False + +-- Булева алгебра +not True -- False +not False -- True +1 == 1 -- True +1 /= 1 -- False +1 < 10 -- True + +-- В примере выше `not`, это функция, принимающая один аргумент. +-- При вызове функции в Haskell список аргументов +-- не нужно заключать в скобки - аргументы просто +-- перечисляются через пробелы сразу после имени функции. +-- Т.о. типичный вызов выглядит так: +-- func arg1 arg2 arg3... +-- Ниже же будет показано, как определять свои функции. + +-- Строки и символы +"Это строка." +'ы' -- а это символ +'Нельзя заключать длинные строки в одинарные кавычки.' -- ошибка! + +-- Строки можно конкатенировать +"Привет" ++ ", Мир!" -- "Привет, Мир!" + +-- При этом строки - это просто списки символов! +"Я - строка!" !! 0 -- 'Я' + + +---------------------------------------------------- +-- Списки и Кортежи +---------------------------------------------------- + +-- Все элементы списка в Haskell +-- должны иметь один и тот же тип. + +-- Эти два списка - эквивалентны: +[1, 2, 3, 4, 5] +[1..5] + +-- Haskell позволяет определять даже бесконечные списки! +[1..] -- список всех натуральных чисел! + +-- Бесконечные списки возможно в Haskell потому, что он "ленив". +-- В Haskell все вычисления производятся тогда и только тогда, +-- когда их результат потребуется. +-- Эта стратегия так и называется - "lazy evaluation". +-- Скажем, если вам нужен тысячный элемент из +-- списка натуральных чисел (бесконечного) и вы напишете так: + +[1..] !! 999 -- 1000 + +-- То Haskell вычислит элементы этого списка от 1 до 1000... +-- ... и остановится, ведь последующие элементы пока не нужны. +-- Это значит, что остальные элементы нашего +-- "бесконечного" списка не будут вычисляться! По крайней мере, +-- пока не понадобятся и они. + +-- Списки можно объединять +[1..5] ++ [6..10] + +-- И добавлять значения в начало +0:[1..5] -- [0, 1, 2, 3, 4, 5] + +-- А можно обратиться по индексу +[0..] !! 5 -- 5 + +-- Вот ещё несколько функций, часто используемых со списками +head [1..5] -- 1 +tail [1..5] -- [2, 3, 4, 5] +init [1..5] -- [1, 2, 3, 4] +last [1..5] -- 5 + +-- list comprehensions - "формулы" для описания списков +[x*2 | x <- [1..5]] -- [2, 4, 6, 8, 10] + +-- можно указать условие попадания элементов в список +[x*2 | x <- [1..5], x*2 > 4] -- [6, 8, 10] + +-- Списки могут даже состоять из других списков +[[1,2,3],[4,5,6]] !! 1 !! 2 -- 6 (вторая строка, третий столбец) + +-- Кортежи позволяют своим элементам иметь различные типы, +-- но при этом кортежи имеют фиксированную длину. +-- Кортеж: +("haskell", 1) + +-- Часто кортежи из двух элементов называются "парами". +-- Элементы пары можно получать так: +fst ("haskell", 1) -- "haskell" +snd ("haskell", 1) -- 1 + +---------------------------------------------------- +-- 3. Функции +---------------------------------------------------- +-- Простая функция, принимающая два аргумента +add a b = a + b + +-- Внимание! +-- Если вы используете ghci (интерактивный интерпретатор Haskell), +-- вам нужно использовать ключевое слово `let`, примерно так: +-- let add a b = a + b + +-- Вызовем нашу функцию +add 1 2 -- 3 + +-- Функцию можно поместить между первым и вторым аргументами, +-- если заключить её имя в обратные кавычки +1 `add` 2 -- 3 + +{- Вы можете также определять функции, имя которых +вообще не содержит букв! Таки функции и называются "операторами", +и, да, вы можете определять свои операторы! +Скажем, оператор целочисленного деления можно определить так -} +(//) a b = a `div` b +35 // 4 -- 8 +{- Здесь оператор заключен в скобки - как говорят, +поставлен в префиксную позицию. +В префиксной позиции оператор можно не только определять, +но и вызывать -} +(+) 1 2 -- 3 + +-- Охранные выражения (guards) порой удобны, +-- если наша функция ветвится +fib x + | x < 2 = x + | otherwise = fib (x - 1) + fib (x - 2) + +{- Сопоставление с образцом (pattern matching) +чем-то напоминает охранные выражения. +Здесь мы видим три определения функции fib. +При вызове функции по имени Haskell использует +первое определение, к образцу которого +"подойдет" набор аргументов -} +fib 1 = 1 +fib 2 = 2 +fib x = fib (x - 1) + fib (x - 2) + +-- Pattern matching для кортежей выглядит так +foo (x, y) = (x + 1, y + 2) + +{- Pattern matching для списков устроен чуть сложнее. +Пусть `x` - первый элемент списка, а `xs` - остальные элементы. +Тогда операции `head` и `tail` могут быть определены так -} +myHead (x:xs) = x +myTail (x:xs) = xs + +-- Функцию отображения мы можем написать так +myMap func [] = [] +myMap func (x:xs) = func x:(myMap func xs) + +-- При сопоставлении происходит привязка +-- элементов значения с именами в образце +fstPlusThird (a : _ : b : _) = a + b +fstPlusThird [1,2,3,4,5] -- 4 +-- Значения, для которых вместо имени указано `_`, +-- игнорируются. Это удобно, когда важен сам факт +-- совпадения образца +oneElem [_] = True +oneElem _ = False + +startsWith x (y:_) = x == y +startsWith _ _ = False + +startsWith 'H' "Hello!" -- True +startsWith 'H' "hello!" -- False + +{- Обратите внимание на тот факт, +что первый аргумент нашей функции `myMap` - тоже функция! +Функции, подобно `myMap`, принимающие другие функции +в качестве параметров, или, скажем, возвращающие функции +в качестве результата, называются +Функциями Высших Порядков (ФВП, High Order Functions, HOF) +-} + +-- Вместе с ФВП часто используются анонимные функции +myMap (\x -> x + 2) [1..5] -- [3, 4, 5, 6, 7] +-- Такие функции описываются в виде +-- \arg1 arg1 .. -> expression + +-- Популярные в других языках ФВП присутствуют и в Haskell +map (\x -> x * 10) [1..5] -- [10, 20, 30, 40, 50] +filter (\x -> x > 2) [1..5] -- [3, 4, 5] + +{- Функция свертки +(она же `reduce` или `inject` в других языках) +в Haskell представлены функциями `foldr` и `foldl`. +Суть свертки можно представить так: + +foldl f x0 [x1,x2,x3] -> (f (f (f x0 x1) x2) x3) +foldr f x0 [x1,x2,x3] -> (f x1 (f x2 (f x3 x0))) + +Здесь x0 - начальное значения так называемого "аккумулятора" +-} +-- Эти два вызова дают одинаковый результат +foldr (\x acc -> acc + x) 0 [1..5] -- 15 +foldl (\acc x -> acc + x) 0 [1..5] -- 15 +-- Тут можно даже заменить анонимную функцию на оператор +foldr (+) 0 [1..5] -- 15 +foldl (+) 0 [1..5] -- 15 + +-- Зато здесь разница видна +foldr (\x acc -> (x + 10) : acc) [] [1..3] -- [11, 12, 13] +foldl (\acc x -> (x + 10) : acc) [] [1..3] -- [13, 12, 11] + +{- Часто в качестве начального значения +удобно брать крайнее значение списка (крайнее слева или справа). +Для этого есть пара функций - `foldr1` и `foldl1` -} +foldr1 (+) [1..5] -- 15 +foldl1 (+) [1..5] -- 15 + +---------------------------------------------------- +-- 4. Больше о функциях +---------------------------------------------------- + +{- Каррирование (currying) +Если в Haskell при вызове функции передать не все аргументы, +Функция становится "каррированой" - результатом вызова станет +новая функция, которая при вызове и примет оставшиеся аргументы -} + +add a b = a + b +foo = add 10 -- теперь foo будет принимать число + -- и добавлять к нему 10 +foo 5 -- 15 + +-- Для операторов можно "опустить" любой из двух аргументов +-- Используя этот факт можно определить +-- функцию `foo` из кода выше несколько иначе +foo = (+10) +foo 5 -- 15 + +-- Поупражняемся +map (10-) [1..3] -- [9, 8, 7] +filter (<5) [1..10] -- [1, 2, 3, 4] + +{- Композиция функций +Функция (.) соединяет пару функций в цепочку. +К примеру, можно соединить функцию, добавляющую 10, +с функцией, умножающей на 5 -} +foo = (*5) . (+10) + +-- (5 + 10) * 5 = 75 +foo 5 -- 75 + +{- Управление приоритетом вычисления +В Haskell есть функция `$`, которая применяет +свой первый аргумент ко второму с наименьшим приоритетом +(обычное применение функций имеет наивысший приоритет) +Эта функция часто позволяет избежать использования +"лишних" скобок -} +head (tail (tail "abcd")) -- 'c' +head $ tail $ tail "abcd" -- 'c' +-- того же эффекта иногда можно достичь использованием композиции +(head . tail . tail) "abcd" -- 'c' +head . tail . tail $ "abcd" -- 'c' +{- Тут стоит сразу запомнить, что композиция функций +возвращает именно новую функцию, как в последнем примере. +Т.е. можно делать так -} +third = head . tail . tail +-- но не так +third = head $ tail $ tail -- (head (tail (tail))) - ошибка! + +---------------------------------------------------- +-- 5. Сигнатуры типов +---------------------------------------------------- + +{- Haskell обладает очень сильной системой типов. +И типизация в Haskell - строгая. Каждое выражение имеет тип, +который может быть описан сигнатурой. +Сигнатура записывается в форме +expression :: type signature +-} + +-- Типы примитивов +5 :: Integer +"hello" :: String +True :: Bool + +{- Функции тоже имеют тип +`not` принимает булево значение и возвращает булев результат +not :: Bool -> Bool + +Вот функция двух аргументов +add :: Integer -> Integer -> Integer + +Тут то мы и видим предпосылки к каррированию: тип +на самом деле выглядит так (скобки просто обычно опускаются) +add :: (Integer -> Integer) -> Integer +т.е. функция принимает аргумент, +и возвращает функцию от второго аргумента! -} + +-- Считается хорошим тоном указывать сигнатуру определений, +-- которые доступны другим разработчикам (публичны). Пример: +double :: Integer -> Integer +double x = x * 2 + +---------------------------------------------------- +-- 6. Управление потоком исполнения +---------------------------------------------------- + +-- Выражение `if` +haskell = if 1 == 1 then "awesome" else "awful" -- haskell = "awesome" + +-- Выражение `if` можно записать и в несколько строк. +-- Соблюдайте отступы! +haskell = if 1 == 1 + then "awesome" + else "awful" + +-- Так как `if` - выражение, ветка `else` обязательна! +-- И более того, результаты выражений в ветках `then` и `else` +-- должны иметь одинаковый тип! + +-- `case`-выражение выглядит так +case args of -- парсим аргументы командной строки + "help" -> printHelp + "start" -> startProgram + _ -> putStrLn "bad args" + +-- При вычислении результата `case`-выражения производится +-- сопоставление с образцом: +fib x = case x of + 1 -> 1 + 2 -> 1 + _ -> fib (x - 1) + fib (x - 2) + +-- В Haskell нет циклов - вместо них используются рекурсия, +-- отображение, фильтрация и свертка (map/filter/fold) +map (*2) [1..5] -- [2, 4, 6, 8, 10] + +for array func = map func array +for [0..3] $ \i -> show i -- ["0", "1", "2", "3"] +for [0..3] show -- ["0", "1", "2", "3"] + +---------------------------------------------------- +-- 7. Пользовательские типы данных +---------------------------------------------------- + +-- Создадим свой Haskell-тип данных + +data Color = Red | Blue | Green + +-- Попробуем использовать + +say :: Color -> String +say Red = "You are Red!" +say Blue = "You are Blue!" +say Green = "You are Green!" + +-- Типы могут иметь параметры (параметры типов) + +data Maybe a = Nothing | Just a + +-- Все эти выражения имеют тип `Maybe` +Just "hello" -- :: `Maybe String` +Just 1 -- :: `Maybe Int` +Nothing -- :: `Maybe a` для любого `a` + +-- Типы могут быть достаточно сложными +data Figure = Rectangle (Int, Int) Int Int + | Square (Int, Int) Int + | Point (Int, Int) + +area :: Figure -> Int +area (Point _) = 0 +area (Square _ s) = s * s +area (Rectangle _ w h) = w * h + +---------------------------------------------------- +-- 8. Ввод-вывод в Haskell +---------------------------------------------------- + +-- Полноценно объяснить тему ввода-вывода невозможно +-- без объяснения монад, но для использования в простых случаях +-- вводного описания будет достаточно. + +-- Когда программа на Haskell выполняется, +-- вызывается функция с именем `main`. +-- Эта функция должна вернуть значение типа `IO ()` +-- Например + +main :: IO () +main = putStrLn $ "Hello, sky! " ++ (say Blue) +-- `putStrLn` имеет тип `String -> IO ()` + +-- Проще всего реализовать программу с вводом-выводом (IO), +-- если вы реализуете функцию с типом `String -> String`. +-- Далее ФВП +-- interact :: (String -> String) -> IO () +-- сделает всё за нас! + +countLines :: String -> String +countLines = show . length . lines +-- здесь `lines` разделяет строку на список строк +-- по символу перевода строки + +main' :: IO () +main' = interact countLines + +{- Вы можете думать о типе `IO ()`, +как о некотором представлении последовательности +действий, которые должен совершить компьютер. +Такое представление напоминает программу +на императивном языке программирования. Для описания +такой последовательности используется `do`-нотация -} + +sayHello :: IO () +sayHello = do + putStrLn "What is your name?" + name <- getLine -- запрашиваем строку и связываем с "name" + putStrLn $ "Hello, " ++ name + +-- Упражнение: +-- напишите свою реализацию функции `interact`, +-- которая запрашивает и обрабатывает только одну строку + +{- Код функции `sayHello` не будет исполняться +при её определении. Единственное место, где IO-действия +могут быть произведены - функция `main`! +Чтобы эта программа выполнила действия в функции `sayHello`, +закомментируйте предыдущее определение функции `main` +и добавьте новое определение: + +main = sayHello -} + +{- Давайте подробнее рассмотрим, как работает функция `getLine` +Её тип: + getLine :: IO String +Вы можете думать, что значение типа `IO a` представляет +собой компьютерную программу, в результате выполнения которой +генерируется значение типа `a`, в дополнение +к остальным эффектам, производимым при выполнении - таким как +печать текста на экран. Это значение типа `a` мы можем +сохранить с помощью оператора `<-`. Мы даже можем реализовать +свое действие, возвращающее значение: -} + +action :: IO String +action = do + putStrLn "This is a line. Duh" + input1 <- getLine + input2 <- getLine + -- Тип блока `do` будет соответствовать типу последнего + -- выполненного в блоке выражения. + -- Заметим, что `return` - не ключевое слово, а функция + -- типа `a -> IO a` + return (input1 ++ "\n" ++ input2) -- return :: String -> IO String + +-- Теперь это действие можно использовать вместо `getLine`: + +main'' = do + putStrLn "I will echo two lines!" + result <- action + putStrLn result + putStrLn "This was all, folks!" + +{- Тип `IO` - пример "монады". Языку Haskell нужны монады, +чтобы оставаться преимущественно чистым функциональным языком. +Любые функции, взаимодействующие с внешним миром +(производящие ввод-вывод) имеют `IO` в своих сигнатурах. +Это позволяет судить о функции как о "чистой" - такая не будет +производить ввод-вывод. В ином случая функция - не "чистая". + +Такой подход позволяет очень просто разрабатывать многопоточные +программы - чистые функции, запущенные параллельно +не будут конфликтовать между собой в борьбе за ресурсы. -} + +---------------------------------------------------- +-- 9. Haskell REPL +---------------------------------------------------- + +{- Интерактивная консоль Haskell запускается командой `ghci`. +Теперь можно вводить строки кода на Haskell. +Связывание значений с именами производится +с помощью выражения `let`: -} + +let foo = 5 + +-- Тип значения или выражения можно узнать +-- с помощью команды `:t`: + +>:t foo +foo :: Integer + +-- Также можно выполнять действия с типом `IO ()` + +> sayHello +What is your name? +Friend! +Hello, Friend! + +``` + +Многое о Haskell, например классы типов и монады невозможно уместить в столь короткую статью. Огромное количество очень интересных идей лежит в основе языка, и именно благодаря этому фундаменту на языке так приятно писать код. Позволю себе привести ещё один маленький пример кода на Haskell - реализацию быстрой сортировки: + +```haskell +qsort [] = [] +qsort (p:xs) = qsort lesser ++ [p] ++ qsort greater + where lesser = filter (< p) xs + greater = filter (>= p) xs +``` + +Haskell прост в установке, забирайте [здесь](http://www.haskell.org/platform/) и пробуйте! Это же так интересно!. + +Более глубокое погрузиться в язык позволят прекрасные книги +[Learn you a Haskell](http://learnyouahaskell.com/) и +[Real World Haskell](http://book.realworldhaskell.org/). + +[^1]: имеется в виду автор оригинального текста - Adit Bhargava *(примечание переводчика)*