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.