Изучай Haskell во имя добра!

Липовача Миран

8

Ввод-вывод

 

 

Разделение «чистого» и «нечистого»

В этой главе вы узнаете, как вводить данные с клавиатуры и печатать их на экран. Начнём мы с основ ввода-вывода:

• Что такое действия?

• Как действия позволяют выполнять ввод-вывод?

• Когда фактически исполняются действия?

Вводу-выводу приходится иметь дело с некоторыми ограничениями функций языка Haskell, поэтому первым делом мы обсудим, что с этим можно сделать.

Мы уже упоминали, что Haskell – чисто функциональный язык. В то время как в императивных языках вы указываете компьютеру серию шагов для достижения некой цели, в функциональном программировании мы описываем, чем является то или иное понятие. В языке Haskell функция не может изменить некоторое состояние, например поменять значение переменной (если функция изменяет состояние, мы говорим, что она имеет побочные эффекты). Единственное, что могут сделать функции в языке Haskell, – это вернуть нам некоторый результат, основываясь на переданных им параметрах. Если вызвать функцию дважды с одинаковыми параметрами, она всегда вернёт одинаковый результат. Если вы знакомы с императивными языками, может показаться, что это ограничивает свободу наших действий, но мы видели, что на самом деле это даёт весьма мощные возможности. В императивном языке у вас нет гарантии, что простая функция, которая всего-то навсего должна обсчитать пару чисел, не сожжёт ваш дом, не похитит собаку и не поцарапает машину во время вычислений! Например, когда мы создавали бинарное поисковое дерево, то вставляли элемент в дерево не путём модификации дерева в точке вставки. Наша функция добавления нового элемента в дерево возвращала новое дерево, так как не могла изменить старое.

Конечно, это хорошо, что функции не могут изменять состояние: это помогает нам строить умозаключения о наших программах. Но есть одна проблема. Если функция не может ничего изменить, как она сообщит нам о результатах вычислений? Для того чтобы вывести результат, она должна изменить состояние устройства вывода – обычно это экран, который излучает фотоны; они путешествуют к нашему мозгу и изменяют состояние нашего сознания… вот так-то, чувак!

Но не надо отчаиваться, не всё ещё потеряно. Оказывается, в языке Haskell есть весьма умная система для работы с функциями с побочными эффектами, которая чётко разделяет чисто функциональную и «грязную» части нашей программы. «Грязная» часть выполняет всю грязную работу, например отвечает за взаимодействие с клавиатурой и экраном. Разделив «чистую» и «грязную»части, мы можем так же свободно рассуждать о чисто функциональной части нашей программы, получать все преимущества функциональной чистоты, а именно – ленивость, гибкость, модульность, и при этом эффективно взаимодействовать с внешним миром.

 

Привет, мир!

До сих пор для того, чтобы протестировать наши функции, мы загружали их в интерпретатор GHCi. Там же мы изучали функции из стандартной библиотеки. Но теперь, спустя семь глав, мы наконец-то собираемся написать первую программу на языке Haskell! Ура! И, конечно же, это будет старый добрый шедевр «Привет, мир».

Итак, для начинающих: наберите в вашем любимом текстовом редакторе строку

main = putStrLn "Привет, мир"

Мы только что определили имя main; в нём мы вызываем функцию putStrLn с параметром "Привет, мир". На первый взгляд, ничего необычного, но это не так: мы убедимся в этом через несколько минут. Сохраните файл как helloworld.hs.

Сейчас мы собираемся сделать то, чего ещё не пробовали делать. Мы собираемся скомпилировать нашу программу! Я даже разволновался!.. Откройте ваш терминал, перейдите в папку с сохранённым файлом helloworld.hs и выполните следующую команду:

$ ghc helloworld

[1 of 1] Compiling Main   ( helloworld.hs, helloworld.o )

Linking helloworld …

О’кей! При некотором везении вы получите нечто похожее и теперь можете запустить свою программу, вызвав ./helloworld.

$ ./helloworld

Привет, мир

ПРИМЕЧАНИЕ. Если вы используете Windows, то вместо выполнения команды ./helloworld просто запустите файл helloworld.exe .

Ну вот и наша первая программа, которая печатает что-то на терминале! Банально до невероятности!

Давайте изучим более подробно, что же мы написали. Сначала посмотрим на тип функции putStrLn:

ghci> :t putStrLn

putStrLn :: String -> IO ()

ghci> :t putStrLn "Привет, мир"

putStrLn "Привет, мир" :: IO ()

Тип putStrLn можно прочесть таким образом: putStrLn принимает строку и возвращает действие ввода-вывода (I/O action) с результирующим типом () (это пустой кортеж). Действие ввода-вывода – это нечто вызывающее побочные эффекты при выполнении (обычно чтение входных данных или печать на экране); также действие может возвращать некоторые значения. Печать строки на экране не имеет какого-либо значимого результата, поэтому возвращается значение ().

ПРИМЕЧАНИЕ. Пустой кортеж имеет значение () , его тип – также () .

Когда будет выполнено действие ввода-вывода? Вот для чего нужна функция 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 "Привет, как тебя зовут?"

   name <– getLine

   putStrLn $ "Вот твоё будущее: " ++ tellFortune name

Функция tellFortune (или любая другая, которой мы передаём значение name) не должна знать ничего про IO – это обычная функция String –> String.

Посмотрите на этот образец кода. Корректен ли он?

nameTag = "Привет, меня зовут " ++ getLine

Если вы ответили «нет», возьмите с полки пирожок. Если ответили «да», убейте себя об стену… Шучу, не надо! Это выражение не сработает, потому что оператор ++ требует, чтобы оба параметра были списками одинакового типа. Левый параметр имеет тип String (или [Char], если вам угодно), в то время как функция getLine возвращает значение типа IO String. Вы не сможете конкатенировать строку и результат действия ввода-вывода. Для начала нам нужно извлечь результат из действия ввода-вывода, чтобы получить значение типа String, и единственный способ сделать это – выполнить что-то вроде name <– getLine внутри другого действия ввода-вывода. Если мы хотим работать с «нечистыми» данными, то должны делать это в «нечистом» окружении!… Итак, грязь от нечистоты распространяется как моровое поветрие, и в наших интересах делать часть для осуществления ввода-вывода настолько малой, насколько это возможно.

Каждое выполненное действие ввода-вывода заключает в себе результат. Вот почему наш предыдущий пример можно переписать так:

main = do

   foo <- putStrLn "Привет, как тебя зовут?"

   name <– getLine

   putStrLn ("Привет, " ++ name ++ ", ну ты и хипстота!")

Тем не менее образец foo всегда будет получать значение (), так что большого смысла в этом нет. Заметьте: мы не связываем последний вызов функции putStrLn с именем, потому что в блоке do последний оператор, в отличие от предыдущих, не может быть связан с именем. Мы узнаем причины такого поведения немного позднее, когда познакомимся с миром монад. До тех пор можно считать, что блок do автоматически получает результат последнего оператора и возвращает его в качестве собственного результата.

За исключением последней строчки, каждая строка в блоке do может быть использована для связывания. Например, putStrLn "ЛЯ" может быть записана как _ <– putStrLn "ЛЯ". Но в этом нет никакого смысла, так что мы опускаем <– для действий ввода-вывода, не возвращающих значимого результата.

Иногда начинающие думают, что вызов

myLine = getLine

считает значение со стандартного входа и затем свяжет это значение с именем myLine. На самом деле это не так. Такая запись даст функции getLine другое синонимичное имя, в данном случае – myLine. Запомните: чтобы получить значение из действия ввода-вывода, вы должны выполнять его внутри другого действия ввода-вывода и связывать его с именем при помощи символа <–.

Действие ввода-вывода будет выполнено, только если его имя main или если оно помещено в составное действие с помощью блока do. Также мы можем использовать блок do для того, чтобы «склеить» несколько действий ввода-вывода в одно. Затем можно будет использовать его в другом блоке do и т. д. В любом случае действие будет выполнено, только если оно каким-либо образом вызывается из функции main.

Ах, да, есть ещё один способ выполнить действие ввода-вывода! Если напечатать его в интерпретаторе GHCi и нажать клавишу Enter, действие выполнится.

gchi> putStrLn "При-и-и-вет"

При-и-и-вет

Даже если мы просто наберём некоторое число или вызовем некоторую функцию в GHCi и нажмём Enter, интерпретатор GHCi вычислит значение, затем вызовет для него функцию show, чтобы получить строку, и напечатает строку на терминале, используя функцию putStrLn.

 

Использование ключевого слова let внутри блока do

Помните связывания при помощи ключевого слова let? Если уже подзабыли, освежите свои знания. Связывания должны быть такого вида: let < определения > in < выражение > , где < определения > – это имена, даваемые выражениям, а < выражение > использует имена из < определений > . Также мы говорили, что в списковых выражениях часть in не нужна. Так вот, в блоках do можно использовать выражение let таким же образом, как и в списковых выражениях. Смотрите:

import Data.Char

main = do

   putStrLn "Ваше имя?"

   firstName <– getLine

   putStrLn "Ваша фамилия?"

   lastName <– getLine

   let bigFirstName = map toUpper firstName

       bigLastName = map toUpper lastName

   putStrLn $ "Привет, " ++ bigFirstName ++ " "

                         ++ bigLastName

                         ++ ", как дела?"

Видите, как выровнены операторы действий ввода-вывода в блоке do? Обратите внимание и на то, как выровнено выражение let по отношению к действиям ввода-вывода и как выровнены образцы внутри выражения let. Это хороший пример, потому что выравнивание текста очень важно в языке Haskell. Далее мы записали вызов map toUpper firstName, что превратит, например, "Иван" в намного более солидное "ИВАН". Мы связали эту строку в верхнем регистре с именем, которое использовали в дальнейшем при выводе на терминал.

Вам может быть непонятно, когда использовать символ <–, а когда выражение let. Запомните: символ <– (в случае действий ввода-вывода) используется для выполнения действий ввода-вывода и связывания результатов с именами. Выражение map toUpper firstName не является действием ввода-вывода – это чистое выражение. Соответственно, используйте символ <– для связывания результатов действий ввода-вывода с именами, а выражение let – для связывания имён с чистыми значениями. Если бы мы выполнили что-то вроде let firstName = getLine, то просто создали бы синоним функции getLine, для которого значение всё равно должно получаться с помощью символа <–.

 

Обращение строк

Теперь напишем программу, которая будет считывать строки, переставлять в обратном порядке буквы в словах и распечатывать их. Выполнение программы прекращается при вводе пустой строки. Итак:

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

2

"ха-ха"

3.2

[3,4,3]

Как вы могли заметить, это очень полезная функция. Помните, мы говорили о том, что действия ввода-вывода выполняются только из функции main или когда мы выполняем их в интерпретаторе GHCi? После того как мы напечатаем значение (например, 3 или [1, 2, 3]) и нажмём клавишу «Ввод», интерпретатор GHCi вызовет функцию print с введённым значением для вывода на терминал!

ghci> 3

3

ghci> print 3

3

ghci> map (++"!") ["хей","хо","ууу"]

["хей!","хо!","ууу!"]

ghci> print $ map (++"!") ["хей","хо","ууу"]

["хей!","хо!","ууу!"]

Как правило, мы хотим видеть строку на экране, не заключённую в кавычки, поэтому для печати строк обычно используется функция putStrLn. Но для печати значений других типов преимущественно используется функция print.

 

Функция when

Функция when находится в модуле Control.Monad (чтобы к ней обратиться, воспользуйтесь import Control.Monad). Она интересна, потому что выглядит как оператор управления ходом вычислений, но на самом деле это обычная функция. Она принимает булевское значение и действие ввода-вывода. Если булевское значение истинно, она возвращает второй параметр – действие ввода-вывода. Если первый параметр ложен, функция возвращает return (), то есть пустое действие.

Напишем программу, которая запрашивает строку текста и, если строка равна «РЫБА-МЕЧ», печатает её:

import Control.Monad

main = do

   input <- getLine

   when (input == "РЫБА-МЕЧ") $ do

      putStrLn input

Без when нам понадобилось бы написать нечто такое:

main = do

   input <- getLine

   if (input == "РЫБА-МЕЧ")

      then putStrLn input

      else return ()

Как вы видите, функция when позволяет выполнить заданное действие в случае, если некоторое условие истинно, и ничего не делать в противном случае.

 

Функция sequence

Функция sequence принимает список действий ввода-вывода и возвращает одно действие ввода-вывода, последовательно выполняющее действия из списка. Результат выполнения этого действия – список результатов вложенных действий. Сигнатура типа функции: sequence :: [IO a] –> IO [a]. Выполним следующее:

main = do

   a <– getLine

   b <– getLine

   c <– getLine

   print [a,b,c]

То же самое, но с использованием функции sequence:

main = do

   rs <– sequence [getLine, getLine, getLine]

   print rs

Итак, выражение sequence [getLine, getLine, getLine] создаст действие ввода-вывода, которое выполнит функцию getLine три раза. Если мы свяжем это действие с именем, результат будет представлять собой список результатов действий из изначального списка, в нашем случае – то, что пользователь введёт с клавиатуры.

Функция sequence обычно используется, если мы хотим пройтись по списку функциями print или putStrLn. Вызов map print [1,2,3,4] не создаёт действия ввода-вывода – вместо этого создаётся список действий. Такой код на самом деле эквивалентен следующему:

[print 1, print 2, print 3, print 4]

Если мы хотим преобразовать список действий в действие, то необходимо воспользоваться функцией sequence:

ghci> sequence $ map print [1,2,3,4]

1

2

3

4

[(),(),(),()]

Но что это за [(),(),(),()] в конце вывода? При выполнении в GHCi действия ввода-вывода помимо самого действия выводится результат выполнения, но только если этот результат не есть (). Поэтому при выполнении в GHCi putStrLn "ха-ха" просто выводится строка – результатом является (). Если же попробовать ввести getLine, то помимо собственно ввода с клавиатуры будет выведено введённое значение – результатом является IO String.

 

Функция mapM

Поскольку применение функции, возвращающей действие ввода-вывода, к элементам списка и последующее выполнение всех полученных действий очень распространено, для этих целей были введены две вспомогательные функции – mapM и mapM_. Функция mapM принимает функцию и список, применяет функцию к элементам списка, сводит элементы в одно действие ввода-вывода и выполняет их. Функция mapM_ работает так же, но отбрасывает результат действия ввода-вывода. Она используется, когда нам не важен результат комбинированного действия ввода-вывода.

ghci> mapM print [1,2,3]

1

2

3

[(),(),()]

ghci> mapM_ print [1,2,3]

1

2

3

 

Функция forever

Функция forever принимает действие ввода-вывода – параметр и возвращает действие ввода-вывода – результат. Действие-результат будет повторять действие-параметр вечно. Эта функция входит в модуль Control.Monad. Следующая программа будет бесконечно спрашивать у пользователя строку и возвращать её в верхнем регистре:

import Control.Monad

import Data.Char

main = forever $ do

   putStr "Введите что-нибудь: "

   l <– getLine

   putStrLn $ map toUpper l

 

Функция forM

Функция forM (определена в модуле Control.Monad) похожа на функцию mapM, но её параметры поменяны местами. Первый параметр – это список, второй – это функция, которую надо применить к списку и затем свести действия из списка в одно действие. Для чего это придумано? Если творчески использовать лямбда-выражения и ключевое слово do, можно проделывать такие фокусы:

import Control.Monad

main = do

   colors <– forM [1,2,3,4] (\a –> do

      putStrLn $ "С каким цветом ассоциируется число "

                 ++ show a ++ "?"

      color <– getLine

      return color)

   putStrLn "Цвета, ассоциирующиеся с 1, 2, 3 и 4: "

   mapM putStrLn colors

Вот что мы получим при запуске:

С каким цветом ассоциируется число 1?

белый

С каким цветом ассоциируется число 2?

синий

С каким цветом ассоциируется число 3?

красный

С каким цветом ассоциируется число 4?

оранжевый

Цвета, ассоциирующиеся с 1, 2, 3 и 4:

белый

синий

красный

оранжевый

Анонимная функция (\a –> do ...) – это функция, которая принимает число и возвращает действие ввода-вывода. Нам пришлось поместить её в скобки, иначе анонимная функция решит, что следующие два действия ввода-вывода принадлежат ей. Обратите внимание, что мы производим вызов return color внутри блока do. Это делается для того, чтобы действие ввода-вывода, возвращаемое блоком do, содержало в себе цвет. На самом деле мы не обязаны этого делать, потому что функция getLine уже содержит цвет внутри себя. Выполняя color <– getLine и затем return color, мы распаковываем результат getLine и затем запаковываем его обратно, то есть это то же самое, что просто вызвать функцию getLine. Функция forM (вызываемая с двумя параметрами) создаёт действие ввода-вывода, результат которого мы связываем с идентификатором colors. Этот идентификатор – обычный список, содержащий строки. В конце мы распечатываем все цвета, вызывая выражение mapM putStrLn colors.

Вы можете думать, что функция forM имеет следующий смысл: «Создай действие ввода-вывода для каждого элемента в списке. Каков будет результат каждого такого действия, может зависеть от элемента, из которого оно создаётся. После создания списка действий исполни их и привяжи их результаты к чему-либо». Однако мы не обязаны их связывать – результаты можно просто отбросить.

На самом деле мы могли бы сделать это без использования функции forM, но так легче читается. Обычно эта функция используется, когда нам нужно отобразить (map) и объединить (sequence) действия, которые мы тут же определяем в секции do. Таким образом, мы могли бы заменить последнюю строку на выражение forM colors putStrLn.

 

Обзор системы ввода-вывода

В этой главе мы изучили основы системы ввода-вывода языка Haskell. Также мы узнали, что такое действия ввода-вывода, как они позволяют выполнять ввод-вывод, в какой момент они выполняются. Итак, повторим пройденное: действия ввода-вывода – это значения, такие же, как любые другие в языке Haskell. Мы можем передать их в функции как параметры, функции могут возвращать действия ввода-вывода в качестве результата. Они отличаются тем, что если они попадут в функцию main (или их введут в интерпретаторе GHCi), то будут выполнены. В этот момент они могут выводить что-либо на экран или управлять звуковыводящим устройством. Каждое действие ввода-вывода может содержать результат общения с реальным миром.

Не думайте о функции, например о putStrLn, как о функции, которая принимает строку и печатает её на экране. Думайте о ней как о функции, которая принимает строку и возвращает действие ввода-вывода. Это действие при выполнении печатает нечто ценное на вашем терминале.