Установка
Для программирования на 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