Изучай Haskell во имя добра! - Миран Липовача
Шрифт:
Интервал:
Закладка:
main = do
line <– getLine
if null line
then return ()
else do
putStrLn $ reverseWords line
main
reverseWords :: String –> String
reverseWords = unwords . map reverse . words
Чтобы лучше понять, как работает программа, сохраните её в файле reverse.hs, скомпилируйте и запустите:
$ ghc reverse.hs
[1 of 1] Compiling Main ( reverse.hs, reverse.o )
Linking reverse ...
$ ./reverse
уберитесь в проходе номер 9
ьсетиребу в едохорп ремон 9
козёл ошибки осветит твою жизнь
лёзок икбишо титевсо юовт ьнзиж
но это всё мечты
он отэ ёсв ытчем
Для начала посмотрим на функцию reverseWords. Это обычная функция, которая принимает строку, например "эй ты мужик", и вызывает функцию words, чтобы получить список слов ["эй", "ты","мужик"]. Затем мы применяем функцию reverse к каждому элементу списка, получаем ["йэ","ыт","кижум"] и помещаем результат обратно в строку, используя функцию unwords. Конечным результатом будет "йэ ыт кижум".
Теперь посмотрим на функцию main. Сначала мы получаем строку с терминала с помощью функции getLine. Далее у нас имеется условное выражение. Запомните, что в языке Haskell каждое ключевое слово if должно сопровождаться секцией else, так как каждое выражение должно иметь некоторое значение. Наш оператор записан так, что если условие истинно (в нашем случае – когда введут пустую строку), мы выполним одно действие ввода-вывода; если оно ложно – выполним действие ввода-вывода из секции else. По той же причине в блоке do условные операторы if должны иметь вид if <условие> then <действие ввода-вывода> else <действие ввода-вывода>.
Вначале посмотрим, что делается в секции else. Поскольку можно поместить только одно действие ввода-вывода после ключевого слова else, мы используем блок do для того, чтобы «склеить» несколько операторов в один. Эту часть можно было бы написать так:
else (do
putStrLn $ reverseWords line
main)
Подобная запись явно показывает, что блок do может рассматриваться как одно действие ввода-вывода, но и выглядит она не очень красиво. В любом случае внутри блока do мы можем вызвать функцию reverseWords со строкой – результатом действия getLine и распечатать результат. После этого мы выполняем функцию main. Получается, что функция main вызывается рекурсивно, и в этом нет ничего необычного, так как сама по себе функция main – тоже действие ввода-вывода. Таким образом, мы возвращаемся к началу программы в следующей рекурсивной итерации.
Ну а что случится, если мы получим на вход пустую строку? В этом случае выполнится часть после ключевого слова then. То есть выполнится выражение return (). Если вам приходилось писать на императивных языках вроде C, Java или на Python, вы наверняка уверены, что знаете, как работает функция return – и, возможно, у вас возникнет искушение пропустить эту часть текста. Но не стоит спешить: функция return в языке Haskell работает совершенно не так, как в большинстве других языков! Её название сбивает с толку, но на самом деле она довольно сильно отличается от своих «тёзок». В императивных языках ключевое слово return обычно прекращает выполнение метода или процедуры и возвращает некоторое значение вызывающему коду. В языке Haskell (и особенно в действиях ввода-вывода) одноимённая функция создаёт действие ввода-вывода из чистого значения. Если продолжать аналогию с коробками, она берёт значение и помещает его в «коробочку». Получившееся в результате действие ввода-вывода на самом деле не выполняет никаких действий – оно просто инкапсулирует некоторое значение. Таким образом, в контексте системы ввода-вывода return "ха-ха" будет иметь тип IO String. Какой смысл преобразовывать чистое значение в действие ввода-вывода, которое ничего не делает? Зачем «пачкать» нашу программу больше необходимого? Нам нужно некоторое действие ввода-вывода для второй части условного оператора, чтобы обработать случай пустой строки. Вот для чего мы создали фиктивное действие ввода-вывода, которое ничего не делает, записав return ().
Вызов функции return не прекращает выполнение блока do – ничего подобного! Например, следующая программа успешно выполнится вся до последней строчки:
main = do
return ()
return "ХА-ХА-ХА"
line <– getLine
return "ЛЯ-ЛЯ-ЛЯ"
return 4
putStrLn line
Всё, что делает функция return, – создаёт действия ввода-вывода, которые не делают ничего, кроме как содержат значения, и все они отбрасываются, поскольку не привязаны к образцам. Мы можем использовать функцию return вместе с символом <– для того, чтобы связывать значения с образцами.
main = do
let a = "ад"
b = "да!"
putStrLn $ a ++ " " ++ b
Как вы можете видеть, функция return выполняет обратную операцию по отношению к операции <–. В то время как функция return принимает значение и помещает его в «коробку», операция <– принимает (и исполняет) «коробку», а затем привязывает полученное из неё значение к имени. Но всё это выглядит лишним, так как в блоках do можно использовать выражение let для привязки к именам, например так:
main = do
let a = "hell"
b = "yeah"
putStrLn $ a ++ " " ++ b
При работе с блоками do мы чаще всего используем функцию return либо для создания действия ввода-вывода, которое ничего не делает, либо для того, чтобы блок do возвращал нужное нам значение, а не результат последнего действия ввода-вывода. Во втором случае мы используем функцию return, чтобы создать действие ввода-вывода, которое будет всегда возвращать нужное нам значение, и эта функция return должна находиться в самом конце блока do.
Некоторые полезные функции для ввода-вывода
В стандартной библиотеке языка Haskell имеется масса полезных функций и действий ввода-вывода. Давайте рассмотрим некоторые из них и увидим, как ими пользоваться.
Функция putStr
Функция putStr похожа на функцию putStrLn – она принимает строку как параметр и возвращает действие ввода-вывода, которое печатает строку на терминале. Единственное отличие: функция putStr не выполняет перевод на новую строку после печати, как это делает putStrLn.
main = do
putStr "Привет, "
putStr "я "
putStrLn "Энди!"
Если мы скомпилируем эту программу, то при запуске получим:
Привет, я Энди!
Функция putChar
Функция putChar принимает символ и возвращает действие ввода-вывода, которое напечатает его на терминале.
main = do
putChar 'A'
putChar 'Б'
putChar 'В'
Функция putStr определена рекурсивно с помощью функции putChar. Базовый случай для функции putStr – это пустая строка. Если печатаемая строка пуста, функция возвращает пустое действие ввода-вывода, то есть return (). Если строка не пуста, функция выводит на терминал первый символ этой строки, вызывая функцию putChar, а затем выводит остальные символы, снова рекурсивно вызывая саму себя.
putStr :: String –> IO ()
putStr [] = return ()
putStr (x:xs) = do
putChar x
putStr xs
Как вы заметили, мы можем использовать рекурсию в системе ввода-вывода подобно тому, как делаем это в чистом коде. Точно так же образом мы определяем базовые случаи, а затем думаем, что будет результатом. В результате мы получим действие, которое выведет первый символ, а затем остаток строки.
Функция print
Функция print принимает значение любого типа – экземпляра класса Show (то есть мы знаем, как представить значение этого типа в виде строки), вызывает функцию show, чтобы получить из данного значения строку, и затем выводит её на экран. По сути, это putStrLn.show. Это выражение сначала вызывает функцию show на переданном параметре, а затем «скармливает» результат функции putStrLn, которая возвращает действие ввода-вывода; оно, в свою очередь, печатает заданное значение.
main = do
print True
print 2
print "ха-ха"
print 3.2
print [3,4,3]
После компиляции и запуска получаем:
True