haskell.md

Личный сайт Go-разработчика из Казани

Haskell разрабатывался, как чистый функциональный язык программирования, применимый на практике. Язык известен благодаря своей системе типов, и «знаменит» благодаря монадам. Меня же Haskell заставляет возвращаться к себе снова и снова именно своей элегантностью и я получаю истинное удовольствие, программируя на Haskell.

1-- Однострочные комментарии начинаются с двух дефисов 2{- Многострочный комментарий 3заключается в пару фигурных скобок с дефисами с внутренней стороны. 4-} 5 6------------------------------------------------------- 7-- 1. Примитивные типы и простейшие операции над ними 8------------------------------------------------------- 9 10-- Числа объявляются просто 113 -- 3 12 13-- Арифметика тоже выглядит вполне ожидаемо 141 + 1 -- 2 158 - 1 -- 7 1610 * 2 -- 20 1735 / 5 -- 7.0 18 19-- Операция деления всегда возвращает действительное число 2035 / 4 -- 8.75 21 22-- Делим нацело так 2335 `div` 4 -- 8 24 25-- Булевы значения - тоже примитивные значения 26True 27False 28 29-- Булева алгебра 30not True -- False 31not False -- True 321 == 1 -- True 331 /= 1 -- False 341 < 10 -- True 35 36-- В примере выше `not`, это функция, принимающая один аргумент. 37-- При вызове функции в Haskell список аргументов 38-- не нужно заключать в скобки - аргументы просто 39-- перечисляются через пробелы сразу после имени функции. 40-- Т.о. типичный вызов выглядит так: 41-- func arg1 arg2 arg3... 42-- Ниже же будет показано, как определять свои функции. 43 44-- Строки и символы 45"Это строка." 46'ы' -- а это символ 47ельзя заключать длинные строки в одинарные кавычки.' -- ошибка! 48 49-- Строки можно конкатенировать 50"Привет" ++ ", Мир!" -- "Привет, Мир!" 51 52-- При этом строки - это просто списки символов! 53"Я - строка!" !! 0 -- 'Я' 54 55 56---------------------------------------------------- 57-- 2. Списки и Кортежи 58---------------------------------------------------- 59 60-- Все элементы списка в Haskell 61-- должны иметь один и тот же тип. 62 63-- Эти два списка - эквивалентны: 64[1, 2, 3, 4, 5] 65[1..5] 66 67-- Haskell позволяет определять даже бесконечные списки! 68[1..] -- список всех натуральных чисел! 69 70-- Бесконечные списки возможно в Haskell потому, что он "ленив". 71-- В Haskell все вычисления производятся тогда и только тогда, 72-- когда их результат потребуется. 73-- Эта стратегия так и называется - "lazy evaluation". 74-- Скажем, если вам нужен тысячный элемент из 75-- списка натуральных чисел (бесконечного) и вы напишете так: 76 77[1..] !! 999 -- 1000 78 79-- То Haskell вычислит элементы этого списка от 1 до 1000... 80-- ... и остановится, ведь последующие элементы пока не нужны. 81-- Это значит, что остальные элементы нашего 82-- "бесконечного" списка не будут вычисляться! По крайней мере, 83-- пока не понадобятся и они. 84 85-- Списки можно объединять 86[1..5] ++ [6..10] 87 88-- И добавлять значения в начало 890:[1..5] -- [0, 1, 2, 3, 4, 5] 90 91-- А можно обратиться по индексу 92[0..] !! 5 -- 5 93 94-- Вот ещё несколько функций, часто используемых со списками 95head [1..5] -- 1 96tail [1..5] -- [2, 3, 4, 5] 97init [1..5] -- [1, 2, 3, 4] 98last [1..5] -- 5 99 100-- list comprehensions - "формулы" для описания списков 101[x*2 | x <- [1..5]] -- [2, 4, 6, 8, 10] 102 103-- можно указать условие попадания элементов в список 104[x*2 | x <- [1..5], x*2 > 4] -- [6, 8, 10] 105 106-- Списки могут даже состоять из других списков 107[[1,2,3],[4,5,6]] !! 1 !! 2 -- 6 (вторая строка, третий столбец) 108 109-- Кортежи позволяют своим элементам иметь различные типы, 110-- но при этом кортежи имеют фиксированную длину. 111-- Кортеж: 112("haskell", 1) 113 114-- Часто кортежи из двух элементов называются "парами". 115-- Элементы пары можно получать так: 116fst ("haskell", 1) -- "haskell" 117snd ("haskell", 1) -- 1 118 119---------------------------------------------------- 120-- 3. Функции 121---------------------------------------------------- 122-- Простая функция, принимающая два аргумента 123add a b = a + b 124 125-- Внимание! 126-- Если вы используете ghci (интерактивный интерпретатор Haskell), 127-- вам нужно использовать ключевое слово `let`, примерно так: 128-- let add a b = a + b 129 130-- Вызовем нашу функцию 131add 1 2 -- 3 132 133-- Функцию можно поместить между первым и вторым аргументами, 134-- если заключить её имя в обратные кавычки 1351 `add` 2 -- 3 136 137{- Вы можете также определять функции, имя которых 138вообще не содержит букв! Таки функции и называются "операторами", 139и, да, вы можете определять свои операторы! 140Скажем, оператор целочисленного деления можно определить так -} 141(//) a b = a `div` b 14235 // 4 -- 8 143{- Здесь оператор заключен в скобки - как говорят, 144поставлен в префиксную позицию. 145В префиксной позиции оператор можно не только определять, 146но и вызывать -} 147(+) 1 2 -- 3 148 149-- Охранные выражения (guards) порой удобны, 150-- если наша функция ветвится 151fib x 152 | x < 2 = x 153 | otherwise = fib (x - 1) + fib (x - 2) 154 155{- Сопоставление с образцом (pattern matching) 156чем-то напоминает охранные выражения. 157Здесь мы видим три определения функции fib. 158При вызове функции по имени Haskell использует 159первое определение, к образцу которого 160"подойдет" набор аргументов -} 161fib 1 = 1 162fib 2 = 1 163fib x = fib (x - 1) + fib (x - 2) 164 165-- Pattern matching для кортежей выглядит так 166foo (x, y) = (x + 1, y + 2) 167 168{- Pattern matching для списков устроен чуть сложнее. 169Пусть `x` - первый элемент списка, а `xs` - остальные элементы. 170Тогда операции `head` и `tail` могут быть определены так -} 171myHead (x:xs) = x 172myTail (x:xs) = xs 173 174-- Функцию отображения мы можем написать так 175myMap func [] = [] 176myMap func (x:xs) = func x:(myMap func xs) 177 178-- При сопоставлении происходит привязка 179-- элементов значения с именами в образце 180fstPlusThird (a : _ : b : _) = a + b 181fstPlusThird [1,2,3,4,5] -- 4 182-- Значения, для которых вместо имени указано `_`, 183-- игнорируются. Это удобно, когда важен сам факт 184-- совпадения образца 185oneElem [_] = True 186oneElem _ = False 187 188startsWith x (y:_) = x == y 189startsWith _ _ = False 190 191startsWith 'H' "Hello!" -- True 192startsWith 'H' "hello!" -- False 193 194{- Обратите внимание на тот факт, 195что первый аргумент нашей функции `myMap` - тоже функция! 196Функции, подобно `myMap`, принимающие другие функции 197в качестве параметров, или, скажем, возвращающие функции 198в качестве результата, называются 199Функциями Высших Порядков (ФВП, High Order Functions, HOF) 200-} 201 202-- Вместе с ФВП часто используются анонимные функции 203myMap (\x -> x + 2) [1..5] -- [3, 4, 5, 6, 7] 204-- Такие функции описываются в виде 205-- \arg1 arg1 .. -> expression 206 207-- Популярные в других языках ФВП присутствуют и в Haskell 208map (\x -> x * 10) [1..5] -- [10, 20, 30, 40, 50] 209filter (\x -> x > 2) [1..5] -- [3, 4, 5] 210 211{- Функция свертки 212(она же `reduce` или `inject` в других языках) 213в Haskell представлены функциями `foldr` и `foldl`. 214Суть свертки можно представить так: 215 216foldl f x0 [x1,x2,x3] -> (f (f (f x0 x1) x2) x3) 217foldr f x0 [x1,x2,x3] -> (f x1 (f x2 (f x3 x0))) 218 219Здесь x0 - начальное значения так называемого "аккумулятора" 220-} 221-- Эти два вызова дают одинаковый результат 222foldr (\x acc -> acc + x) 0 [1..5] -- 15 223foldl (\acc x -> acc + x) 0 [1..5] -- 15 224-- Тут можно даже заменить анонимную функцию на оператор 225foldr (+) 0 [1..5] -- 15 226foldl (+) 0 [1..5] -- 15 227 228-- Зато здесь разница видна 229foldr (\x acc -> (x + 10) : acc) [] [1..3] -- [11, 12, 13] 230foldl (\acc x -> (x + 10) : acc) [] [1..3] -- [13, 12, 11] 231 232{- Часто в качестве начального значения 233удобно брать крайнее значение списка (крайнее слева или справа). 234Для этого есть пара функций - `foldr1` и `foldl1` -} 235foldr1 (+) [1..5] -- 15 236foldl1 (+) [1..5] -- 15 237 238---------------------------------------------------- 239-- 4. Больше о функциях 240---------------------------------------------------- 241 242{- Каррирование (currying) 243Если в Haskell при вызове функции передать не все аргументы, 244Функция становится "каррированой" - результатом вызова станет 245новая функция, которая при вызове и примет оставшиеся аргументы -} 246 247add a b = a + b 248foo = add 10 -- теперь foo будет принимать число 249 -- и добавлять к нему 10 250foo 5 -- 15 251 252-- Для операторов можно "опустить" любой из двух аргументов 253-- Используя этот факт можно определить 254-- функцию `foo` из кода выше несколько иначе 255foo = (+10) 256foo 5 -- 15 257 258-- Поупражняемся 259map (10-) [1..3] -- [9, 8, 7] 260filter (<5) [1..10] -- [1, 2, 3, 4] 261 262{- Композиция функций 263Функция (.) соединяет пару функций в цепочку. 264К примеру, можно соединить функцию, добавляющую 10, 265с функцией, умножающей на 5 -} 266foo = (*5) . (+10) 267 268-- (5 + 10) * 5 = 75 269foo 5 -- 75 270 271{- Управление приоритетом вычисления 272В Haskell есть функция `$`, которая применяет 273свой первый аргумент ко второму с наименьшим приоритетом 274(обычное применение функций имеет наивысший приоритет) 275Эта функция часто позволяет избежать использования 276"лишних" скобок -} 277head (tail (tail "abcd")) -- 'c' 278head $ tail $ tail "abcd" -- 'c' 279-- того же эффекта иногда можно достичь использованием композиции 280(head . tail . tail) "abcd" -- 'c' 281head . tail . tail $ "abcd" -- 'c' 282{- Тут стоит сразу запомнить, что композиция функций 283возвращает именно новую функцию, как в последнем примере. 284Т.е. можно делать так -} 285third = head . tail . tail 286-- но не так 287third = head $ tail $ tail -- (head (tail (tail))) - ошибка! 288 289---------------------------------------------------- 290-- 5. Сигнатуры типов 291---------------------------------------------------- 292 293{- Haskell обладает очень сильной системой типов. 294И типизация в Haskell - строгая. Каждое выражение имеет тип, 295который может быть описан сигнатурой. 296Сигнатура записывается в форме 297expression :: type signature 298-} 299 300-- Типы примитивов 3015 :: Integer 302"hello" :: String 303True :: Bool 304 305{- Функции тоже имеют тип 306`not` принимает булево значение и возвращает булев результат 307not :: Bool -> Bool 308 309Вот функция двух аргументов 310add :: Integer -> Integer -> Integer 311 312Тут то мы и видим предпосылки к каррированию: тип 313на самом деле выглядит так (скобки просто обычно опускаются) 314add :: (Integer -> Integer) -> Integer 315т.е. функция принимает аргумент, 316и возвращает функцию от второго аргумента! -} 317 318-- Считается хорошим тоном указывать сигнатуру определений, 319-- которые доступны другим разработчикам (публичны). Пример: 320double :: Integer -> Integer 321double x = x * 2 322 323---------------------------------------------------- 324-- 6. Управление потоком исполнения 325---------------------------------------------------- 326 327-- Выражение `if` 328haskell = if 1 == 1 then "awesome" else "awful" -- haskell = "awesome" 329 330-- Выражение `if` можно записать и в несколько строк. 331-- Соблюдайте отступы! 332haskell = if 1 == 1 333 then "awesome" 334 else "awful" 335 336-- Так как `if` - выражение, ветка `else` обязательна! 337-- И более того, результаты выражений в ветках `then` и `else` 338-- должны иметь одинаковый тип! 339 340-- `case`-выражение выглядит так 341case args of -- парсим аргументы командной строки 342 "help" -> printHelp 343 "start" -> startProgram 344 _ -> putStrLn "bad args" 345 346-- При вычислении результата `case`-выражения производится 347-- сопоставление с образцом: 348fib x = case x of 349 1 -> 1 350 2 -> 1 351 _ -> fib (x - 1) + fib (x - 2) 352 353-- В Haskell нет циклов - вместо них используются рекурсия, 354-- отображение, фильтрация и свертка (map/filter/fold) 355map (*2) [1..5] -- [2, 4, 6, 8, 10] 356 357for array func = map func array 358for [0..3] $ \i -> show i -- ["0", "1", "2", "3"] 359for [0..3] show -- ["0", "1", "2", "3"] 360 361---------------------------------------------------- 362-- 7. Пользовательские типы данных 363---------------------------------------------------- 364 365-- Создадим свой Haskell-тип данных 366 367data Color = Red | Blue | Green 368 369-- Попробуем использовать 370 371say :: Color -> String 372say Red = "You are Red!" 373say Blue = "You are Blue!" 374say Green = "You are Green!" 375 376-- Типы могут иметь параметры (параметры типов) 377 378data Maybe a = Nothing | Just a 379 380-- Все эти выражения имеют тип `Maybe` 381Just "hello" -- :: `Maybe String` 382Just 1 -- :: `Maybe Int` 383Nothing -- :: `Maybe a` для любого `a` 384 385-- Типы могут быть достаточно сложными 386data Figure = Rectangle (Int, Int) Int Int 387 | Square (Int, Int) Int 388 | Point (Int, Int) 389 390area :: Figure -> Int 391area (Point _) = 0 392area (Square _ s) = s * s 393area (Rectangle _ w h) = w * h 394 395---------------------------------------------------- 396-- 8. Ввод-вывод в Haskell 397---------------------------------------------------- 398 399-- Полноценно объяснить тему ввода-вывода невозможно 400-- без объяснения монад, но для использования в простых случаях 401-- вводного описания будет достаточно. 402 403-- Когда программа на Haskell выполняется, 404-- вызывается функция с именем `main`. 405-- Эта функция должна вернуть значение типа `IO ()` 406-- Например 407 408main :: IO () 409main = putStrLn $ "Hello, sky! " ++ (say Blue) 410-- `putStrLn` имеет тип `String -> IO ()` 411 412-- Проще всего реализовать программу с вводом-выводом (IO), 413-- если вы реализуете функцию с типом `String -> String`. 414-- Далее ФВП 415-- interact :: (String -> String) -> IO () 416-- сделает всё за нас! 417 418countLines :: String -> String 419countLines = show . length . lines 420-- здесь `lines` разделяет строку на список строк 421-- по символу перевода строки 422 423main' :: IO () 424main' = interact countLines 425 426{- Вы можете думать о типе `IO ()`, 427как о некотором представлении последовательности 428действий, которые должен совершить компьютер. 429Такое представление напоминает программу 430на императивном языке программирования. Для описания 431такой последовательности используется `do`-нотация -} 432 433sayHello :: IO () 434sayHello = do 435 putStrLn "What is your name?" 436 name <- getLine -- запрашиваем строку и связываем с "name" 437 putStrLn $ "Hello, " ++ name 438 439-- Упражнение: 440-- напишите свою реализацию функции `interact`, 441-- которая запрашивает и обрабатывает только одну строку 442 443{- Код функции `sayHello` не будет исполняться 444при её определении. Единственное место, где IO-действия 445могут быть произведены - функция `main`! 446Чтобы эта программа выполнила действия в функции `sayHello`, 447закомментируйте предыдущее определение функции `main` 448и добавьте новое определение: 449 450main = sayHello -} 451 452{- Давайте подробнее рассмотрим, как работает функция `getLine` 453Её тип: 454 getLine :: IO String 455Вы можете думать, что значение типа `IO a` представляет 456собой компьютерную программу, в результате выполнения которой 457генерируется значение типа `a`, в дополнение 458к остальным эффектам, производимым при выполнении - таким как 459печать текста на экран. Это значение типа `a` мы можем 460сохранить с помощью оператора `<-`. Мы даже можем реализовать 461свое действие, возвращающее значение: -} 462 463action :: IO String 464action = do 465 putStrLn "This is a line. Duh" 466 input1 <- getLine 467 input2 <- getLine 468 -- Тип блока `do` будет соответствовать типу последнего 469 -- выполненного в блоке выражения. 470 -- Заметим, что `return` - не ключевое слово, а функция 471 -- типа `a -> IO a` 472 return (input1 ++ "\n" ++ input2) -- return :: String -> IO String 473 474-- Теперь это действие можно использовать вместо `getLine`: 475 476main'' = do 477 putStrLn "I will echo two lines!" 478 result <- action 479 putStrLn result 480 putStrLn "This was all, folks!" 481 482{- Тип `IO` - пример "монады". Языку Haskell нужны монады, 483чтобы оставаться преимущественно чистым функциональным языком. 484Любые функции, взаимодействующие с внешним миром 485(производящие ввод-вывод) имеют `IO` в своих сигнатурах. 486Это позволяет судить о функции как о "чистой" - такая не будет 487производить ввод-вывод. В ином случая функция - не "чистая". 488 489Такой подход позволяет очень просто разрабатывать многопоточные 490программы - чистые функции, запущенные параллельно 491не будут конфликтовать между собой в борьбе за ресурсы. -} 492 493---------------------------------------------------- 494-- 9. Haskell REPL 495---------------------------------------------------- 496 497{- Интерактивная консоль Haskell запускается командой `ghci`. 498Теперь можно вводить строки кода на Haskell. 499Связывание значений с именами производится 500с помощью выражения `let`: -} 501 502let foo = 5 503 504-- Тип значения или выражения можно узнать 505-- с помощью команды `:t`: 506 507>:t foo 508foo :: Integer 509 510-- Также можно выполнять действия с типом `IO ()` 511 512> sayHello 513What is your name? 514Friend! 515Hello, Friend!

Многое о Haskell, например классы типов и монады невозможно уместить в столь короткую статью. Огромное количество очень интересных идей лежит в основе языка, и именно благодаря этому фундаменту на языке так приятно писать код. Позволю себе привести ещё один маленький пример кода на Haskell - реализацию быстрой сортировки:

1qsort [] = [] 2qsort (p:xs) = qsort lesser ++ [p] ++ qsort greater 3 where lesser = filter (< p) xs 4 greater = filter (>= p) xs

Haskell прост в установке, забирайте здесь и пробуйте! Это же так интересно!.

Более глубокое погрузиться в язык позволят прекрасные книги Learn you a Haskell и Real World Haskell.