ПРИМЕЧАНИЕ. Если вы используете Windows, то вместо выполнения команды ./helloworld
просто запустите файл
Ну вот и наша первая программа, которая печатает что-то на терминале! Банально до невероятности!
Давайте изучим более подробно, что же мы написали. Сначала посмотрим на тип функции putStrLn
:
ghci> :t putStrLn
putStrLn :: String -> IO ()
ghci> :t putStrLn "Привет, мир"
putStrLn "Привет, мир" :: IO ()
Тип putStrLn
можно прочесть таким образом: putStrLn
принимает строку и возвращает действие ввода-вывода (()
(это пустой кортеж). Действие ввода-вывода – это нечто вызывающее побочные эффекты при выполнении (обычно чтение входных данных или печать на экране); также действие может возвращать некоторые значения. Печать строки на экране не имеет какого-либо значимого результата, поэтому возвращается значение ()
.
ПРИМЕЧАНИЕ. Пустой кортеж имеет значение ()
, его тип – также ()
.
Когда будет выполнено действие ввода-вывода? Вот для чего нужна функция main
. Операции ввода-вывода выполняются, если мы поместим их в функцию main
и запустим нашу программу.
Объединение действий ввода-вывода
Возможность поместить в программу всего один оператор ввода-вывода не очень-то вдохновляет. Но мы можем использовать ключевое слово do
для того, чтобы «склеить» несколько операторов ввода-вывода в один. Рассмотрим пример:
main = do
putStrLn "Привет, как тебя зовут?"
name <– getLine
putStrLn ("Привет, " ++ name ++ ", ну ты и хипстота!")
О, новый синтаксис!.. И он похож на синтаксис императивных языков. Если откомпилировать и запустить эту программу, она будет работать так, как вы и предполагаете. Обратите внимание: мы записали ключевое слово do
и затем последовательность шагов, как сделали бы в императивном языке. Каждый из этих шагов – действие ввода-вывода. Расположив их рядом с помощью ключевого слова do
, мы свели их в одно действие ввода-вывода. Получившееся действие имеет тип IO()
; это тип последнего оператора в цепочке.
По этой причине функция main
всегда имеет тип main :: IO <
, где <
– некоторый конкретный тип. По общепринятому соглашению обычно не пишут декларацию типа для функции main
.
В третьей строке можно видеть ещё один не встречавшийся нам ранее элемент синтаксиса, name
<–
getLine
. Создаётся впечатление, будто считанная со стандартного входа строка сохраняется в переменной с именем name
. Так ли это на самом деле? Давайте посмотрим на тип getLine
.
ghci> :t getLine
getLine :: IO String
Ага!.. Функция getLine
– действие ввода-вывода, которое содержит результирующий тип – строку. Это понятно: действие ждёт, пока пользователь не введёт что-нибудь с терминала, и затем это нечто будет представлено как строка. Что тогда делает выражение name <– getLine
? Можно прочитать его так: «выполнить действие getLine
и затем связать результат выполнения с именем name
». Функция getLine
имеет тип IO String
, поэтому образец name
будет иметь тип String
. Можно представить действие ввода-вывода в виде ящика с ножками, который ходит в реальный мир, что-то в нём делает (рисует граффити на стене, например) и иногда приносит обратно какие-либо данные. Если ящик что-либо принёс, единственный способ открыть его и извлечь данные – использовать конструкцию с символом <–. Получить данные из действия ввода-вывода можно только внутри другого действия ввода-вывода. Таким образом, язык Haskell чётко разделяет чистую и «грязную» части кода. Функция getLine
– не чистая функция, потому что её результат может быть неодинаковым при последовательных вызовах. Вот почему она как бы «запачкана» конструктором типов IO
, и мы можем получить данные только внутри действий ввода-вывода, имеющих в сигнатуре типа маркёр IO
. Так как код для ввода-вывода также «испачкан», любое вычисление, зависящее от «испачканных» IO
-данных, также будет давать «грязный»результат.
Если я говорю «испачканы», это не значит, что мы не сможем использовать результат, содержащийся в типе IO
в чистом коде. Мы временно «очищаем» данные внутри действия, когда связываем их с именем. В выражении name <– getLine
образец name
содержит обычную строку, представляющую содержимое ящика.
Мы можем написать сложную функцию, которая, скажем, принимает ваше имя как параметр (обычная строка) и предсказывает вашу удачливость или будущее всей вашей жизни, основываясь на имени:
main = do
putStrLn "Привет, как тебя зовут?"