Установка
Для программирования на Haskell вам понадобится ваш любимый текстовый редактор и компилятор Haskell. В данном курсе используется компилятор GHC как наиболее широко используемый, тем не менее, большинство примеров и упражнений могут быть выполнены при помощи любого из используемых компиляторов. Проще всего будет просто установить Haskell Platform, поскольку там будут сразу все необходимые библиотеки вкупе с компилятором GHC.
GHC может как компилировать код из исходников, так и работать в интерактивном режиме. В интерактивном режиме можно загружать файлы с кодом на Haskell и использовать определенные в них функции. Для обучения рекомендуется использовать преимущественно интерактивный режим GHC.
Запускаем!
Итак, вы установили GHC и теперь можете приступить к знакомству. Для запуска в интерактивном режиме,
из командной строки выполните команду ghci:
$ ghci
GHCi, version 7.4.2: http://www.haskell.org/ghc/ :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Prelude>
Приглашение для ввода начинается с Prelude> и по мере загрузки библиотек и внешних файлов будет расти
и сильно мешать. В примерах кода далее будет использоваться приглашение ghci>, которое можно выставить
следующим образом:
Prelude> :set prompt "ghci> "
ghci>
Приступим!
Базовые вычисления
Для начала, несколько простых арифметических примеров, не нуждающихся в пояснении:
ghci> 14 + 2
16
ghci> 12 * 3
36
ghci> 125 / (7 - 2)
25.0
ghci> 5 * (-3)
-15
Работа с булевой алгеброй также не представляет труда. True и False представляют
соответственно ИСТИНУ и ЛОЖЬ, операторы &&, || и not соответствуют логическим
операторам И, ИЛИ и НЕ соответственно:
ghci> True && False
False
ghci> False || (True && not False)
True
Проверка на равенство осуществляется при помощи операторов == и /=. Символ /= в Haskell испольуется
для большего сходства с реальным символом ≠, используемым в математике. Для сравнения используются
привычные операторы <, >, <=, >=:
ghci> 5 == 10
False
ghci> 6 > 3
True
ghci> True /= False
True
ghci> "string" == "long string"
False
ghci> "abc" <= "xyz"
True
ghci> (5 == 3) || (5 <= 10)
True
Вы можете попытаться сравнить несравнимые сущности, но это приведет к ошибке:
ghci> 50 > "123"
<interactive>:4:1:
No instance for (Num [Char])
arising from the literal `50'
Possible fix: add an instance declaration for (Num [Char])
In the first argument of `(>)', namely `50'
In the expression: 50 > "123"
In an equation for `it': it = 50 > "123"
В этом пугающем сообщении говорится, что "123" не является числом, и поэтому не может
быть использован в сравнении с числом 50. Аналогичная ошибка произдёт и в результате попытки
вычислить 12 + "34" или "True" == True. Позже мы подробнее разберем природу этих
ошибок.
Функции
Возможно, вы уже догадались, что мы уже использовали функции в примерах выше. Например, функция *
принимает 2 аргумента и возвращает их произведение, а функция not принимает один аргумент и
возвращает его отрицание. При этом * используется в инфиксной записи, в то время как not используется
в префиксной.
Функции, чьи имена целиком состоят из специальных символов, по умолчанию используются в инфиксной записи.
Например, *, ==, ++, <|> и пр. Функции, чьи имена используют буквы, по умолчанию используются
в префиксной записи. Например: not, my_func, f, g', h_1 и т.д.
В большинстве языков программирования вызов функции записывается как имя функции со список параметров в
круглых скобках через запятую: f(x, y, z). В Haskell достаточно написать имя функции, а затем список аргументов
через пробел: f x y z. Уже известная нам функция not является примером функции от одного аргумента.
Другим примером могут служить функции succ и pred:
ghci> succ 4
5
ghci> pred 8
7
Как вы догадались, эти функции соответственно увеличивают или уменьшают значение на 1. Функции min и max
принимают 2 аргумента на вход:
ghci> max 10 45
45
ghci> min 1.9 2.0
1.9
ghci> max "abc" "xyz"
"xyz"
Применение функции к аргументу (использование пробела) является операцией с наивысшим приоритетом. Это значит, что следующие два выражения эквивалентны:
ghci> max 10 45 - pred 8 + 4
42
ghci> (max 10 45) - (pred 8) + 4
42
Если же необходимо передать выражение в качестве аргумента функции, то нужно использовать скобки:
ghci> max 45 (10 * pred 6) + 1
51
Заметьте, что выражение f f 3 означает вызов функции f с двумя аргументами — f и 3. Чтобы дважды
применить функцию f, необходимо поставить скобки: f (f 3).
Иногда префиксные функции удобнее использовать в инфиксном варианте и наоборот. Например, функция div вычисляет
частное двух целых чисел, но выражение div 18 3 + 5 теряет читаемость из-за префиксной записи. Для того, чтобы
использовать любую функцию от двух аргументов (на самом деле от двух и более), достаточно поместить её имя в обратные
кавычки:
ghci> div 18 3 + 5
11
ghci> 18 `div` 3 + 5
11
Чтобы использовать инфиксную функцию в префиксной записи, достаточно поместить её в круглые скобки:
ghci> 5 * 3 + 2
17
ghci> (+) (5 * 3) 2
17
Несмотря на эти возможности, в большинстве случаев рекомендуется пользоваться записью по умолчанию.
Определение функций
Чтобы определить свою первую функцию, откройте текстовый редактор и запишите следующее:
triple x = x + x + x
Определение функции напоминает математическое уравнение, в левой части которого происходит вызов функции.
Сохраните файл c именем my-functions.hs и запустите ghci из директории, в которой находится этот файл.
Загрузите файл в GHCi:
ghci> :l my-functions
[1 of 1] Compiling Main ( my-functions.hs, interpreted )
Ok, modules loaded: Main.
ghci> triple 3
9
ghci> triple (-4.5)
-13.5
Так же просто определяются и функции от нескольких переменных:
sumOfSquares x y = x ^ 2 + y ^ 2
Добавьте эту функцию в my-functions.hs и снова загрузите файл при помощи :l my-functions:
ghci> sumOfSquares 3 4
25
ghci> triple (sumOfSquares 2 3)
39
Допустим, нам необходимо определить функцию absolute, возвращающую модуль аргумента. Для этого нам потребуется
условный оператор:
absolute x = if x >= 0 then x else (-x)
Вероятно, вы встречались с условным оператором в других языках программирования. Отличие условного оператора в Haskell
заключается в том, что выражение if a then b else c является выражением и может быть использовано в качестве
подвыражения:
ghci> 1 + if ("cat" > "dog") then 2 else 3
4
То, что условный оператор является, на самом деле, условным выражением приводит к тому, что нельзя опустить ветку else.
В императивных языках программирования, если ветка не указана и условие ложное, оператор просто не выполняется. В Haskell
выражение обязано иметь значение и поэтому if False then 10 не может быть выражением.
В именах функций допускается символ ' (штрих), который помогает заменить имена вроде f1 на более математичное f':
absolute' x = if x > 0 then (-x) else x
В программах на Haskell ' в конце имени функции обычно либо означает, что функция энергичная (в противоположность ленивой),
либо используется для слегка измененной версии функции. Поскольку ' является допустимым символов в имени функции, мы можем
определить такую функцию:
what'sUp = "What's up, bro?!"
Помимо имени эта функция еще отличается отсутствием аргументов. Если у функции нет аргументов, её называют определением или
константой. Кроме того, стоит отметить, что мы не могли назвать функцию What'sUp, и обязаны начать со строчной буквы (почему,
будет ясно позже).
Упражнения
1.2 Реализуйте функцию my_factorial, вычисляющую факториал:
ghci> my_factorial 5
120
Списки
Список является одной из основных структур данных, используемых функциональными программистами. В Haskell списки однородные. Это значит, что он может хранить только элементы одного типа (например, числа или символы):
ghci> let list = [1, 2, 3]
ghci> list
[1, 2, 3]
Ключевое слово let используется в GHCi для определение новых функций или констант. Точно так же вы можете написать list = [1, 2, 3]
в файле и загрузить определение в GHCi. Для временных констант вроде [1, 2, 3] обычно удобнее использовать let.
Как вы могли заметить, списки записываются в квадратных скобках с помощью перечисления элементов через запятую. Попытка использовать элементы различных типов приводит к ошибке:
ghci> [1, 2, '3']
<interactive>:29:2:
No instance for (Num Char)
arising from the literal `1'
Possible fix: add an instance declaration for (Num Char)
In the expression: 1
In the expression: [1, 2, '3']
In an equation for `it': it = [1, 2, '3']
Естественно, символ '3' не является числом, о чём и говорится в этом сообщении. Тем не менее, мы могли бы составить список
из символов:
ghci> ['1', '2', '3']
"123"
Как видите, строка в действительности является просто списком символов! То есть запись "str" является всего лишь синтаксическим
сахаром для ['s', 't', 'r']. Поскольку строки являются списками, мы можем применять к ним любые функции над списками.
Довольно частой операцией со списками является конкатенация. Для этой операции используется оператор ++:
ghci> [1, 2, 3] ++ [4, 5, 6]
[1, 2, 3, 4, 5, 6]
ghci> "hello, " ++ "world!"
"hello, world!"
ghci> ['f', 'o', 'o'] ++ ['b', 'a', 'r']
"foobar"
Конкатенация списков — дорогая операция в том смысле, что второй список "дописывается" в конец первого. Для этого Haskell вынужден
пройти до конца первого списка (чем длиннее список — тем дольше выполнение операции, даже если второй список короткий). Таким образом,
добавление элемента в конец миллионного списка потребует значительного времени. С другой стороны, добавление в начало списка при
помощи оператора : происходит мгновенно:
ghci> 1 : [2, 3]
[1, 2, 3]
ghci> 'c' : "at"
"cat"
Стоит отметить, что оператор : в качестве первого аргумента принимает элемент и добавляет его в список, в то время как ++ принимает
на вход два списка. Даже если необходимо добавить элемент в конец списка при помощи ++, нужно поместить элемент в квадратные скобки:
ghci> [1, 2, 3] ++ [4]
[1, 2, 3, 4]
На самом деле, запись [1, 2, 3] является синтаксическим сахаром для 1 : 2 : 3 : [], где [] — пустой список.
Заметьте, что [], [[]], [[], [], [] — не одно и то же! [] — пустой список, [[]] — список из одного элемента (которым является
пустой список), [[], [], []] — список из трёх пустых списков.
Над списком определены две основные операции (помимо :) — head и tail.
head возвращает первый элемент списка («голову» списка), а tail — весь список без первого элемента («хвост» списка):
ghci> head [1, 2, 3]
1
ghci> head "Hello!"
'H'
ghci> tail [1, 2, 3]
[2, 3]
ghci> tail ["element"]
[]
ghci> head (tail "abcde")
'b'
ghci> head "hello" : tail "hello"
"hello"
При попытке получить голову или хвост пустого списка, GHCi выведет сообщение об ошибке:
ghci> head []
*** Exception: Prelude.head: empty list
Проверить, является ли список пустым, можно при помощи функции null:
ghci> null [1, 2, 3]
False
ghci> let my_head 4 xs = if null xs then d else head xs
ghci> my_head 4 [1, 2, 3]
1
ghci> my_head 4 []
4
Чтобы обратиться к произвольному элементу списка по его индексу, можно воспользоваться оператором !!:
ghci> [1, 2, 3, 4] !! 2
3
Нумерация ведется с нуля. Обратите внимание, что для вычисления list !! 1000000 Haskell должен пройти миллион элементов, чтобы достать
из списка требуемый элемент.
Упражнения
1.1 Реализуйте функцию my_remove_second, удаляющую второй элемент из списка:
ghci> my_remove_second [1, 2, 3, 4]
[1, 3, 4]
ghci> my_remove_second "Hi"
"H"
1.2. Реализуйте функцию my_last, возвращающую последний элемент в списке:
ghci> my_last [1, 2, 3, 4, 5]
5
ghci> my_last "Hello!"
'!'
1.3. ★ Реализуйте функцию my_reverse, разворачивающую список:
ghci> my_reverse [1, 2, 3, 4, 5]
[5, 4, 3, 2, 1]
Кортежи
В отличие от списков, кортежи могут содержать элементы различных типов, но имеют фиксированное количество и порядок элементов. Кортежи записываются в круглых скобках:
ghci> let x = (1, 2)
ghci> ('a', 1, "Hello, world!")
('a', 1, "Hello, world!")
Чаще всего используются двухместные кортежи. Для них определены операции fst и snd, возвращающие первый и второй элемент соответственно:
ghci> fst ('a', 20)
'a'
ghci> snd (23, "hello")
"hello"
ghci> fst (snd (1, (2, 3)))
2