Difference between revisions of "Ru/IO Inside"

From HaskellWiki
< Ru
Jump to navigation Jump to search
m
(7 intermediate revisions by 5 users not shown)
Line 1: Line 1:
Система ввода/вывода Haskell (Haskell I/O) всегда была слишком сложной для новичков. И хотя простой IO код на Haskell выглядит очень похожим на подобный в императивных языках, попытки написать чуть более сложный код часто приводят к замешательству. Это всё от того, что внутреннее устройство Haskell I/O очень сильно отличается. Haskell - чистый язык, и даже I/O система не способна нарушить эту чистоту.
+
Система ввода/вывода Haskell (Haskell I/O) всегда была слишком сложной для новичков. И хотя простой IO код на Haskell выглядит очень похожим на подобный в императивных языках, попытки написать чуть более сложный код часто приводят к замешательству. Это всё от того, что внутреннее устройство Haskell I/O очень сильно отличается. Haskell чистый язык, и даже I/O система не способна нарушить эту чистоту.
   
Цель данной работы - попытка объяснить детали реализации Haskell I/O и постепенно сделать из вас специалиста по работе с IO. Более того, я добавил детальное описание всех ловушек, с которыми вы можете столкнуться. После прочтения этого текста, вы получите звание "Повелитель Haskell I/O", а это тоже самое, что и Бакалавр Информатики и Математики :)
+
Цель данной работы попытка объяснить детали реализации Haskell I/O и постепенно сделать из вас специалиста по работе с IO. Более того, я добавил детальное описание всех ловушек, с которыми вы можете столкнуться. После прочтения этого текста, вы получите звание "Повелитель Haskell I/O", а это тоже самое, что и Бакалавр Информатики и Математики :)
   
 
Если вы никогда не работали с Haskell I/O, лучше будет, если вы сначала прочтёте [[Ru/IO | Введение в Haskell IO]]
 
Если вы никогда не работали с Haskell I/O, лучше будет, если вы сначала прочтёте [[Ru/IO | Введение в Haskell IO]]
Line 7: Line 7:
 
== Haskell - чистый язык ==
 
== Haskell - чистый язык ==
   
Haskell - чистый язык. Это означает, что результат, возвращаемый любой функцией, полностью определён её аргументами. Псевдофункции, такие как rand() или getchar() в C, которые возвращают разные результаты при каждом вызове, невозможны в Haskell. Более того, функции в Haskell не могут иметь побочных эффектов. Это означает, что они не способны влиять на "внешний мир", т.е. не могут производить действия такие как: изменение файлов, вывод на экран, печать, отправка данных по сети и подобное.
+
Haskell чистый язык. Это означает, что результат, возвращаемый любой функцией, полностью определён её аргументами. Псевдофункции, такие как rand() или getchar() в C, которые возвращают разные результаты при каждом вызове, невозможны в Haskell. Более того, функции в Haskell не могут иметь побочных эффектов. Это означает, что они не способны влиять на "внешний мир", т.е. не могут производить действия такие как: изменение файлов, вывод на экран, печать, отправка данных по сети и подобное.
   
 
Эти два ограничения означают, что любой вызов функции может быть заменён на результат работы предыдущего вызова этой же функции с теми же параметрами, и язык '''гарантирует''', что подобная замена не изменит результат всей программы в целом.
 
Эти два ограничения означают, что любой вызов функции может быть заменён на результат работы предыдущего вызова этой же функции с теми же параметрами, и язык '''гарантирует''', что подобная замена не изменит результат всей программы в целом.
Line 13: Line 13:
 
Давайте сравним с C: оптимизирующий компилятор C пытается определить, какая функция не имеет побочных эффектов и не обращается к глобальным переменным. Но если определение неверное, то оптимизация может поменять семантику программы! Во избежание подобных ситуаций C-оптимизаторы очень осторожны при определении или требуют от программистов подсказок о чистоте функций.
 
Давайте сравним с C: оптимизирующий компилятор C пытается определить, какая функция не имеет побочных эффектов и не обращается к глобальным переменным. Но если определение неверное, то оптимизация может поменять семантику программы! Во избежание подобных ситуаций C-оптимизаторы очень осторожны при определении или требуют от программистов подсказок о чистоте функций.
   
В отличие от компилятора C, Haskell-компилятор - это набор чистых математических преобразований, что даёт возможность более высокоуровневой оптимизации. Более того, чистые математические вычисления проще разделить на несколько потоков, запущенных параллельно. В наши дни при наличии многоядерных процессоров это приобретает всё большее значение. И наконец, чистые вычисления больше защищены от ошибок и их легче проверять и доказывать, из-за чего Haskell более надёжен и скорость написания программ на нём выше.
+
В отличие от компилятора C, Haskell-компилятор это набор чистых математических преобразований, что даёт возможность более высокоуровневой оптимизации. Более того, чистые математические вычисления проще разделить на несколько потоков, запущенных параллельно. В наши дни при наличии многоядерных процессоров это приобретает всё большее значение. И наконец, чистые вычисления больше защищены от ошибок и их легче проверять и доказывать, из-за чего Haskell более надёжен и скорость написания программ на нём выше.
   
 
Чистота Haskell позволяет вызывать только те функции, результат которых необходим для вычисления конечного результата функции более высокого уровня.
 
Чистота Haskell позволяет вызывать только те функции, результат которых необходим для вычисления конечного результата функции более высокого уровня.
Line 38: Line 38:
 
# Даже если вдруг будет произведено два вызова функции 'getchar', невозможно определить порядок этих вызовов. Кто из них будет произведён первым? Вы хотите получить два символа в том порядке, в котором ввели, или в обратном порядке? В определении 'get2chars' нет ответа на этот вопрос.
 
# Даже если вдруг будет произведено два вызова функции 'getchar', невозможно определить порядок этих вызовов. Кто из них будет произведён первым? Вы хотите получить два символа в том порядке, в котором ввели, или в обратном порядке? В определении 'get2chars' нет ответа на этот вопрос.
   
  +
Как эта проблема может быть решена с точки зрения программиста?
How can these problems be solved, from the programmer's viewpoint?
 
  +
Можно ввести фиктивный параметр таким образом, что компилятор примет два вызова
Let's introduce a fake parameter of 'getchar' to make each call
 
  +
'getchar' за "разные".
"different" from the compiler's point of view:
 
   
 
<haskell>
 
<haskell>
Line 48: Line 48:
 
</haskell>
 
</haskell>
   
  +
Первая проблема, которую мы описали выше, решена. Теперь компилятор видит, что функции имеют разные параметры, и будет вызывать 'getchar' два раза,
Right away, this solves the first problem mentioned above - now the
 
  +
compiler will make two calls because it sees them as having different
 
  +
Функции 'get2chars' тоже следует добавить фиктивный параметр:
parameters. The whole 'get2chars' function should also have a
 
fake parameter, otherwise we will have the same problem calling it:
 
   
 
<haskell>
 
<haskell>
Line 60: Line 59:
 
</haskell>
 
</haskell>
   
  +
Теперь нужно дать компилятору небольшую подсказку, чтобы он понял, какую функцию следует вычислять первой.
 
  +
В Haskell нет способа задать порядок вычисления... кроме зависимости между данными!
Now we need to give the compiler some clue to determine which function it
 
  +
Как насчёт того, чтобы добавить искусственную зависимость, которая недопустит вычисление второго 'getchar' прежде первого?
should call first. The Haskell language doesn't provide any way to express
 
  +
Сделаем мы это следующим образом: мы будем возвращать из 'getchar' ещё один фиктивный результат, а он, в свою очередь, будет
order of evaluation... except for data dependencies! How about adding an
 
  +
использоваться в качестве параметра в следующем вызове 'getchar':
artificial data dependency which prevents evaluation of the second
 
'getchar' before the first one? In order to achieve this, we will
 
return an additional fake result from 'getchar' that will be used as a
 
parameter for the next 'getchar' call:
 
   
 
<haskell>
 
<haskell>
Line 76: Line 72:
 
</haskell>
 
</haskell>
   
  +
Пока всё отлично. Теперь мы можем гарантировать, что 'a' будет прочитано прежде чем 'b', потому как для чтения 'b' требуется значение
So far so good - now we can guarantee that 'a' is read before 'b'
 
  +
'i', которое возвращается при чтении 'a'!
because reading 'b' needs the value ('i') that is returned by reading 'a'!
 
   
  +
Мы добавили параметр в 'get2chars', но проблема в том, что комплятор Haskell слишком умён!
We've added a fake parameter to 'get2chars' but the problem is that the
 
  +
Он ещё может поверить, что внешняя функция 'getchar' действительно зависит от своего параметра, но для 'get2char' всё выглядит
Haskell compiler is too smart! It can believe that the external 'getchar'
 
  +
таким образом как-будто мы жульничаем, потому что отбрасываем параметр!
function is really dependent on its parameter but for 'get2chars' it
 
  +
will see that we're just cheating because we throw it away! Therefore it won't feel obliged to execute the calls in the order we want. How can we fix this? How about passing this fake parameter to the 'getchar' function?! In this case
 
  +
Поэтому компилятор не считает себя обязанным делать вызовы в том порядке, в котором мы хотим.
the compiler can't guess that it is really unused :)
 
  +
Как же нам это исправить? А может передавать этот фиктивный параметр в 'getchar'?!
  +
Тогда компилятор не поймёт, что параметр на самом деле не используется :)
   
 
<haskell>
 
<haskell>
Line 91: Line 89:
   
   
And more - 'get2chars' has all the same purity problems as the 'getchar'
+
Теперь у функции 'get2chars' те же самые проблемы, что были у 'getchar'.
  +
Если вы захотите вызвать её дважды, то должны найти способ описать порядок этих вызовов. Взгляните:
function. If you need to call it two times, you need a way to describe
 
the order of these calls. Look at:
 
   
 
<haskell>
 
<haskell>
get4chars = [get2chars 1, get2chars 2] -- order of 'get2chars' calls isn't defined
+
get4chars = [get2chars 1, get2chars 2] -- порядок вызовов 'get2chars' не определён.
 
</haskell>
 
</haskell>
   
  +
Но мы уже знаем, как решать такие проблемы. 'get2chars' тоже должен возвращать какое-нибудь фиктивное значение,
We already know how to deal with these problems - 'get2chars' should
 
  +
которое будет использовано для задания порядка вычисления:
also return some fake value that can be used to order calls:
 
   
 
<haskell>
 
<haskell>
Line 109: Line 106:
 
</haskell>
 
</haskell>
   
  +
Но какое значение должна возвращать функция 'get2chars'?
 
  +
Если мы будем использовать постоянное целое число, то слишком умный компилятор решит, что мы опять мухлюем :)
But what's the fake value 'get2chars' should return? If we use some integer constant, the excessively-smart Haskell compiler will guess that we're cheating again :) What about returning the value returned by 'getchar'? See:
 
  +
Давай будет возвращать значение, вычисленное 'getchar'. Смотрите:
   
 
<haskell>
 
<haskell>
Line 118: Line 116:
 
</haskell>
 
</haskell>
   
  +
Хотите верьте, хотите нет, но мы только что полностью воссоздали "монадическую" систему ввода/вывода языка Haskell.
Believe it or not, but we've just constructed the whole "monadic"
 
Haskell I/O system.
 
   
  +
== Добро пожаловать в Реальный мир, детка :) ==
   
  +
Почему бы нам не смотреть на '''''Реальный мир''''', как на ещё один тип данных? Назовём этот тип 'RealWorld'. Функция 'main' имеет следующий тип:
 
== Welcome to the RealWorld, baby :) ==
 
 
The 'main' Haskell function has the type:
 
   
 
<haskell>
 
<haskell>
Line 131: Line 126:
 
</haskell>
 
</haskell>
   
  +
вместо нашего Int используется фиктивный тип 'RealWorld'.
where 'RealWorld' is a fake type used instead of our Int. It's something
 
  +
Реальный мир похож на эстафетную палочку: когда main вызывает другие функции, в результате которых присутствует IO, main передаёт полученный им '''''мир''''' этим функциям.
like the baton passed in a relay race. When 'main' calls some IO function,
 
  +
Все функции, выполняющие ввод-вывод, имеют тип, схожий с типом 'main'. А конкретнее, сам класс типов "IO" определён как синоним типа:
it passes the "RealWorld" it received as a parameter. All IO functions have
 
similar types involving RealWorld as a parameter and result. To be
 
exact, "IO" is a type synonym defined in the following way:
 
   
 
<haskell>
 
<haskell>
Line 141: Line 134:
 
</haskell>
 
</haskell>
   
So, 'main' just has type "IO ()", 'getChar' has type "IO Char" and so
+
Таким образом, 'main' имеет тип "IO ()", 'getChar' имеет тип "IO Char" и так далее.
  +
Например, "IO Char" можно понимать таким образом: получить мир, (может быть, изменить мир) и вернуть Char и изменённый мир.
on. You can think of the type "IO Char" as meaning "take the current RealWorld, do something to it, and return a Char and a (possibly changed) RealWorld". Let's look at 'main' calling 'getChar' two times:
 
  +
Так выглядит 'main', вызывающий 'getChar' последовательно два раза:
   
 
<haskell>
 
<haskell>
Line 154: Line 148:
   
   
Look at this closely: 'main' passes to first 'getChar' the "world" it
+
'main' отдаёт первому 'getChar' полученный мир.
  +
Возвращённый первым 'getChar' мир отдаётся второму 'getChar'.
received. This 'getChar' returns some new value of type RealWorld
 
  +
'main' возвращает тот мир, который был возвращён вторым 'getChar'.
that gets used in the next call. Finally, 'main' returns the "world" it got
 
  +
К чему привела такая схема вызова 'getChar'?
from the second 'getChar'.
 
   
  +
# Можно ли опустить вызов 'getChar', если его результат не нужен? Нет, так как нам всё равно нужен новый 'RealWorld', возвращаемый 'getChar' помимо символа.
# Is it possible here to omit any call of 'getChar' if the Char it read is not used? No, because we need to return the "world" that is the result of the second 'getChar' and this in turn requires the "world" returned from the first 'getChar'.
 
  +
# Можно ли переставить местами вызовы 'getChar'? Нет, так как второй 'getChar' использует 'RealWorld', возвращённый первым.
# Is it possible to reorder the 'getChar' calls? No: the second 'getChar' can't be called before the first one because it uses the "world" returned from the first call.
 
# Is it possible to duplicate calls? In Haskell semantics - yes, but real compilers never duplicate work in such simple cases (otherwise, the programs generated will not have any speed guarantees).
+
# Is it possible to duplicate calls? In Haskell semantics - yes, but real compilers never duplicate work in such simple cases (otherwise, the programs generated will not have any speed guarantees). (Непонятно, что понимается под duplicate calls --[[User:Beroal|Beroal]] 00:48, 18 April 2008 (UTC))
   
   
  +
Повторим ещё раз, что 'RealWorld' похож на эстафетную палочку, которая передаётся между IO-процедурами.
As we already said, RealWorld values are used like a baton which gets passed
 
  +
IO-процедуры, явно или неявно, вызываются из 'main' в строгом порядке.
between all routines called by 'main' in strict order. Inside each
 
  +
Внутри IO-процедуры 'RealWorld' используется таким образом: чтобы «вычислить» новое значение 'RealWorld', мы выполняем императивные команды. Поэтому каждая IO-процедура выполняется именно в тот момент, который мы задали (момент относительно других IO-процедур). Пример:
routine called, RealWorld values are used in the same way. Overall, in
 
order to "compute" the world to be returned from 'main', we should perform
 
each IO procedure that is called from 'main', directly or indirectly.
 
This means that each procedure inserted in the chain will be performed
 
just at the moment (relative to the other IO actions) when we intended it
 
to be called. Let's consider the following program:
 
   
 
<haskell>
 
<haskell>
Line 182: Line 171:
 
</haskell>
 
</haskell>
   
  +
Теперь вы знаете, как транслировать императивные программы на Хаскелле в низкоуровневый код и можете проверить, что IO-процедуры выполняются именно в том порядке и с теми аргументами, которые вы запланировали.
Now you have enough knowledge to rewrite it in a low-level way and
 
check that each operation that should be performed will really be
 
performed with the arguments it should have and in the order we expect.
 
   
   
  +
Пока мы рассматривали линейные алгоритмы. Можно ли задать ветвление? Конечно. Так задаётся управляющая структура 'when':
But what about conditional execution? No problem. Let's define the
 
well-known 'when' operation:
 
   
 
<haskell>
 
<haskell>
Line 198: Line 184:
 
</haskell>
 
</haskell>
   
  +
Мы можем выполнить некоторую IO-процедуру при некотором условии, наложенном на аргумент функции 'when'.
As you can see, we can easily include or exclude from the execution chain
 
  +
Если аргумент 'condition' ложный, то 'action' не будет вызвана, так как реальный компилятор не вызывает функцию, если её результат не нужен для подсчёта окончательного результата (под окончательным подразумевается значение 'RealWorld', возвращаемое 'main').
IO procedures (actions) depending on the data values. If 'condition'
 
will be False on the call of 'when', 'action' will never be called because
 
real Haskell compilers, again, never call functions whose results
 
are not required to calculate the final result (''i.e.'', here, the final "world" value of 'main').
 
   
  +
(Чтобы обеспечить правильное выполнение императивных команд, недостаточно, чтобы 'RealWorld' был абстрактным типом. Значение типа 'RealWorld' (то есть, реальный мир) необходимо запретить копировать. Если компилятор нарушит вышеприведённое условие, то есть, вычислит 'action' при ложном 'condition', то одно и то же значение типа 'RealWorld' будет передано в 'action' возвращено функцией 'when'. То есть компилятор должен уметь копировать мир. Но скопировать мир нельзя, так как под миром подразумеваются не только внутренности компьютера, но и все его внешние устройства, то есть, потенциально, вся Вселенная. То есть, 'RealWorld' — это не обычный тип. Этот тип обозначает данные, которые нельзя копировать. Это свойство 'RealWorld' можно распространить на другие типы данных, например, можно задать монолитный массив, который запрещено копировать из соображений производительности. Подробнее с некопируемыми данными можно познакомиться в функциональном языке Clean, uniqueness type system.)
Loops and more complex control structures can be implemented in
 
the same way. Try it as an exercise!
 
   
  +
Аналогично можно задать циклы и другие управляющие конструкции. Это вам домашнее задание.
   
Finally, you may want to know how much passing these RealWorld
 
values around the program costs. It's free! These fake values exist solely for the compiler while it analyzes and optimizes the code, but when it gets to assembly code generation, it "suddenly" realize that this type is like "()", so
 
all these parameters and result values can be omitted from the final generated code. Isn't it beautiful? :)
 
   
  +
Наконец возникает опасение, что протаскивание 'RealWorld' через все функции увеличит накладные расходы. На самом деле, компилятор, видя, что 'RealWorld' не содержит данных (так же, как и "()"), не включает 'RealWorld' в низкоуровневый императивный код. Круто? :)
   
   
== '>>=' and 'do' notation ==
 
   
  +
== Нотация '>>=' и 'do' ==
All beginners (including me :)) start by thinking that 'do' is some
 
  +
magic statement that executes IO actions. That's wrong - 'do' is just
 
  +
Все новички (включая меня :)) думают, что 'do' — это специальная конструкция для выполнения IO.
syntactic sugar that simplifies the writing of procedures that use IO (and also other monads, but that's beyond the scope of this tutorial). 'do' notation eventually gets translated to statements passing "world" values around like we've manually written above and is used to simplify the gluing of several
 
  +
Это не так — 'do' просто синтаксический сахар, делающий использование IO (и других монад) более удобным.
IO actions together. You don't need to use 'do' for just one statement; for instance,
 
  +
'do' в конечном счёте транслируется в код, передающий 'RealWorld', примерно так же, как мы это делали ранее вручную
  +
'do' склеивает несколько IO-процедур в цепочку.
  +
  +
(Точнее, 'do' транслируется в функциональный код, состоящий из '>>=', '>>' и лямбда-абстракций, а 'RealWorld' подставляется на следующем этапе.
  +
Другие монады 'RealWorld' не используют.)
  +
  +
Каждая строка под 'do' называется командой (statement). IO-процедура — частный случай команды.
  +
'do', применённое к только одной IO-процедуре, не делает ничего. Код:
   
 
<haskell>
 
<haskell>
Line 225: Line 213:
 
</haskell>
 
</haskell>
 
 
  +
транслируется в:
is desugared to:
 
   
 
<haskell>
 
<haskell>
Line 231: Line 219:
 
</haskell>
 
</haskell>
   
  +
Но признаком хорошего стиля считается использовать 'do' даже для одной IO-процедуры, так как это облегчает добавление новых процедур к цепочке в будущем.
But nevertheless it's considered Good Style to use 'do' even for one statement
 
because it simplifies adding new statements in the future.
 
 
   
  +
Посмотрим, как можно транслировать 'do' с несколькими IO-процедурами:
Let's examine how to desugar a 'do' with multiple statements in the
 
following example:
 
   
 
<haskell>
 
<haskell>
Line 244: Line 229:
 
</haskell>
 
</haskell>
   
  +
Здесь 'do' просто выполняет несколько IO-процедур последовательно.
The 'do' statement here just joins several IO actions that should be
 
  +
После трансляции мы увидим, что IO-процедуры объединены в цепочку с помощью операции '>>'.
performed sequentially. It's translated to sequential applications
 
  +
Она и другая операция '>>=' относятся к «связывающим» операциям ('>>=' так и называется: 'bind'):
of one of the so-called "binding operators", namely '>>':
 
   
 
<haskell>
 
<haskell>
Line 255: Line 240:
 
</haskell>
 
</haskell>
   
  +
'>>' просто передает мир от предыдущей IO-процедуры к последующей:
This binding operator just combines two IO actions, executing them
 
sequentially by passing the "world" between them:
 
   
 
<haskell>
 
<haskell>
Line 266: Line 250:
 
</haskell>
 
</haskell>
   
  +
Другой вариант того же определения:
If defining operators this way looks strange to you, read this
 
definition as follows:
 
 
 
 
<haskell>
 
<haskell>
Line 277: Line 260:
 
</haskell>
 
</haskell>
   
  +
Подставьте определение '>>' в код, получившийся после трансляции 'do', чтобы убедиться, что код точно так же манипулирует миром, как мы манипулировали вручную.
Now you can substitute the definition of '>>' at the places of its usage
 
and check that program constructed by the 'do' desugaring is actually the
 
same as we could write by manually manipulating "world" values.
 
   
  +
Есть другая разновидность команд 'do'. Они могут связывать переменную с помощью "<-" (аналогично тому, как связываются переменные в лямбда-абстракции):
 
A more complex example involves the binding of variables using "<-":
 
   
 
<haskell>
 
<haskell>
Line 289: Line 269:
 
</haskell>
 
</haskell>
   
  +
Этот код транслируется в:
This code is desugared into:
 
   
 
<haskell>
 
<haskell>
Line 296: Line 276:
 
</haskell>
 
</haskell>
   
  +
В отличие от лямбда-абстракции, "<-" может связывать только одну переменную.
As you should remember, the '>>' binding operator silently ignores
 
  +
the value of its first action and returns as an overall result
 
  +
Функция '>>' выбрасывает значение предыдущей IO-процедуры.
the result of its second action only. On the other hand, the '>>=' binding operator (note the extra '=' at the end) allows us to use the result of its first action - it gets passed as an additional parameter to the second one! Look at the definition:
 
  +
'>>=', наоборот, позволяет использовать это значение в следующей IO-процедуре.
  +
Поэтому к её имени добавили знак равенства.
  +
Значение предыдущей IO-процедуры передаётся как аргумент последующей IO-процедуре:
   
 
<haskell>
 
<haskell>
Line 308: Line 291:
 
</haskell>
 
</haskell>
   
  +
Расшифруем тип 'action2': "a -> IO b" = "a -> RealWorld -> (b, RealWorld)".
First, what does the type of the second "action" (more precisely, a function which returns an IO action), namely "a -> IO b", mean? By
 
  +
Это значит, что 'action2' имеет два аргумента: аргумент типа 'a', который она использует некоторым неизвестным для нас образом, и аргумент типа 'RealWorld' для передачи управления.
substituting the "IO" definition, we get "a -> RealWorld -> (b, RealWorld)".
 
  +
Все IO-процедуры имеют на один параметр больше, чем указано в их типе.
This means that second action actually has two parameters
 
  +
Этот параметр спрятан в конструкторе типов 'IO'.
- the type 'a' actually used inside it, and the value of type RealWorld used for sequencing of IO actions. That's always the case - any IO procedure has one
 
more parameter compared to what you see in its type signature. This
 
parameter is hidden inside the definition of the type alias "IO".
 
   
  +
Вы можете использовать '>>' и '>>=' наряду с 'do'. Иногда это упрощает код.
Second, you can use these '>>' and '>>=' operations to simplify your
 
  +
В этом примере нет необходимости вводить переменную — результат 'readLn' можно прямо передать в 'print':
program. For example, in the code above we don't need to introduce the
 
variable, because the result of 'readLn' can be send directly to 'print':
 
   
 
<haskell>
 
<haskell>
Line 324: Line 304:
   
   
  +
Наконец, конструкция:
And third - as you see, the notation:
 
   
 
<haskell>
 
<haskell>
Line 331: Line 311:
 
</haskell>
 
</haskell>
   
where 'action1' has type "IO a" and 'action2' has type "IO b",
+
где 'action1' имеет тип "IO a" и 'action2' имеет тип "IO b", транслируется в:
translates into:
 
   
 
<haskell>
 
<haskell>
Line 338: Line 317:
 
</haskell>
 
</haskell>
   
where the second argument of '>>=' has the type "a -> IO b". It's the way
+
где второй аргумент функции '>>=' имеет тип "a -> IO b".
  +
Так транслируется '<-' — связанная им переменная может использоваться в дальнейших IO-процедурах, соединённых в одну большую IO-процедуру.
the '<-' binding is processed - the name on the left-hand side of '<-' just becomes a parameter of subsequent operations represented as one large IO action. Note also that if 'action1' has type "IO a" then 'x' will just have type "a"; you can think of the effect of '<-' as "unpacking" the IO value of 'action1' into 'x'. Note also that '<-' is not a true operator; it's pure syntax, just like 'do' itself. Its meaning results only from the way it gets desugared.
 
  +
Тип введённой переменной определяется типом 'action1': если 'action1' имеет тип "IO a", то 'x' имеет тип "a".
  +
Можно представить себе, что '<-' «раскрывает» IO-процедуру.
  +
'<-' не является функцией, это специальная синтаксическая конструкция, как и 'do'.
  +
Её смысл в том, как она транслируется.
   
  +
Следующий пример:
Look at the next example:
 
   
 
<haskell>
 
<haskell>
Line 351: Line 334:
 
</haskell>
 
</haskell>
   
  +
Код транслируется в:
This code is desugared into:
 
   
 
<haskell>
 
<haskell>
Line 361: Line 344:
 
</haskell>
 
</haskell>
   
  +
Скобки обущены; обе функции '>>' и '>>=' ассоциативны слева направо, но лямбда-абстракция всегда поглощает максимум кода справа от '->', насколько это возможно.
I omitted the parentheses here; both the '>>' and the '>>=' operators are
 
  +
Поэтому переменные 'a' и 'b' видимы во всём коде правее места, где 'a' и 'b' связываются.
left-associative, but lambda-bindings always stretches as far to the right as possible, which means that the 'a' and 'b' bindings introduced
 
  +
В качестве упражнения, расставьте скобки так, как их расставил бы парсер, и транслируйте код до 'RealWorld'.
here are valid for all remaining actions. As an exercise, add the
 
  +
На этом можно закончить изучение 'do' и IO.
parentheses yourself and translate this procedure into the low-level
 
code that explicitly passes "world" values. I think it should be enough to help you finally realize how the 'do' translation and binding operators work.
 
   
   
  +
Постойте! Я забыл про третью монадную функцию: 'return'.
Oh, no! I forgot the third monadic operator - 'return'. It just
 
  +
Она заключает два своих параметра в кортеж:
combines its two parameters - the value passed and "world":
 
   
 
<haskell>
 
<haskell>
Line 376: Line 358:
 
</haskell>
 
</haskell>
   
  +
Попробуйте транслировать пример с 'return':
How about translating a simple example of 'return' usage? Say,
 
   
 
<haskell>
 
<haskell>
Line 384: Line 366:
   
   
  +
Программисты, раннее изучавшие императивные языки, ошибочно полагают, что 'return' в Хаскелле возвращает управление из IO-процедуры, опуская дальнейшие IO-процедуры.
Programmers with an imperative language background often think that
 
  +
Но даже из её типа видно, что это не так.
'return' in Haskell, as in other languages, immediately returns from
 
  +
'return' используется только для того, чтобы вернуть некоторое значение типа 'a' как результат IO-процедуры типа "IO a".
the IO procedure. As you can see in its definition (and even just from its
 
  +
Как правило, 'return' стоит в конце списка команд 'do'.
type!), such an assumption is totally wrong. The only purpose of using
 
  +
Попробуйте транслировать следующий пример:
'return' is to "lift" some value (of type 'a') into the result of
 
a whole action (of type "IO a") and therefore it should generally be used only as the last executed statement of some IO sequence. For example try to
 
translate the following procedure into the corresponding low-level code:
 
   
 
<haskell>
 
<haskell>
Line 399: Line 379:
 
</haskell>
 
</haskell>
   
  +
чтобы убедиться, что 'print' будет выполнена при любых значениях 'a', а значение "()", которые вроде бы должен был вернуть 'main', просто потеряется.
and you will realize that the 'print' statement is executed even for non-negative values of 'a'. If you need to escape from the middle of an IO procedure, you can use the 'if' statement:
 
  +
Потеряется потому, что код будет транслирован в функцию '>>', которая выбрасывает результат IO-процедуры.
  +
Для того, чтобы вернуть управление из середины IO-процедуры, используйте 'if':
   
 
<haskell>
 
<haskell>
Line 408: Line 390:
 
</haskell>
 
</haskell>
   
  +
Также Хаскелл позволяет вставить 'do' внутрь 'if':
Moreover, Haskell layout rules allow us to use the following layout:
 
   
 
<haskell>
 
<haskell>
Line 418: Line 400:
 
</haskell>
 
</haskell>
   
  +
Так удобно выходить из середины длинного списка команд в 'do'.
that may be useful for escaping from the middle of a longish 'do' statement.
 
   
   
  +
Последнее упражнение: напишите реализацию функции 'liftM', которая превращает обычную функцию в монадическую:
Last exercise: implement a function 'liftM' that lifts operations on
 
plain values to the operations on monadic ones. Its type signature:
 
   
 
<haskell>
 
<haskell>
Line 428: Line 409:
 
</haskell>
 
</haskell>
   
  +
Если это для вас трудно, воспользуйтесь следующим функциональным определением:
If that's too hard for you, start with the following high-level
 
definition and rewrite it in low-level fashion:
 
   
 
<haskell>
 
<haskell>
Line 435: Line 415:
 
return (f x)
 
return (f x)
 
</haskell>
 
</haskell>
  +
  +
Не следует забывать, что всё, что является монадой, является и функтором.
  +
Поэтому на IO-процедурах можно использовать функцию 'fmap'.
  +
Вместо громоздкого
  +
  +
<haskell>
  +
action >>= \x -> return (x+1)
  +
</haskell>
  +
  +
или
  +
  +
<haskell>
  +
do x <- action
  +
return (x+1)
  +
</haskell>
  +
  +
можно написать:
  +
  +
<haskell>
  +
fmap (+1) action
  +
</haskell>
  +
  +
Также fmap = liftM, о которой мы говорили выше.
   
   
Line 671: Line 674:
   
 
<haskell>
 
<haskell>
readi h i = do hSeek h i AbsoluteSeek
+
readi h i = do hSeek h AbsoluteSeek i
 
hGetChar h
 
hGetChar h
 
</haskell>
 
</haskell>
Line 690: Line 693:
 
<haskell>
 
<haskell>
 
readfilei name = do h <- openFile name ReadMode
 
readfilei name = do h <- openFile name ReadMode
let readi h i = do hSeek h i AbsoluteSeek
+
let readi h i = do hSeek h AbsoluteSeek i
 
hGetChar h
 
hGetChar h
 
return (readi h)
 
return (readi h)
Line 699: Line 702:
 
<haskell>
 
<haskell>
 
readfilei name = do h <- openFile name ReadMode
 
readfilei name = do h <- openFile name ReadMode
let readi i = do hSeek h i AbsoluteSeek
+
let readi i = do hSeek h AbsoluteSeek i
 
hGetChar h
 
hGetChar h
 
return readi
 
return readi
Line 1,174: Line 1,177:
 
== Further reading ==
 
== Further reading ==
   
This tutorial is largely based on the Simon Peyton Jones' paper [http://research.microsoft.com/%7Esimonpj/Papers/marktoberdorf Tackling the awkward squad: monadic input/output, concurrency, exceptions, and foreign-language calls in Haskell]. I hope that my tutorial improves his original explanation of the Haskell I/O system and brings it closer to the point of view of beginning Haskell programmers. But if you need to learn about concurrency, exceptions and FFI in Haskell/GHC, the original paper is the best source of information.
+
This tutorial is largely based on the Simon Peyton Jones' paper [https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.13.9123&rep=rep1&type=pdf Tackling the awkward squad: monadic input/output, concurrency, exceptions, and foreign-language calls in Haskell]. I hope that my tutorial improves his original explanation of the Haskell I/O system and brings it closer to the point of view of beginning Haskell programmers. But if you need to learn about concurrency, exceptions and FFI in Haskell/GHC, the original paper is the best source of information.
   
 
You can find more information about concurrency, FFI and STM at the [[GHC/Concurrency#Starting points]] page.
 
You can find more information about concurrency, FFI and STM at the [[GHC/Concurrency#Starting points]] page.
Line 1,206: Line 1,209:
   
 
----
 
----
  +
  +
[[Category:Ru]]

Revision as of 06:34, 10 August 2022

Система ввода/вывода Haskell (Haskell I/O) всегда была слишком сложной для новичков. И хотя простой IO код на Haskell выглядит очень похожим на подобный в императивных языках, попытки написать чуть более сложный код часто приводят к замешательству. Это всё от того, что внутреннее устройство Haskell I/O очень сильно отличается. Haskell — чистый язык, и даже I/O система не способна нарушить эту чистоту.

Цель данной работы — попытка объяснить детали реализации Haskell I/O и постепенно сделать из вас специалиста по работе с IO. Более того, я добавил детальное описание всех ловушек, с которыми вы можете столкнуться. После прочтения этого текста, вы получите звание "Повелитель Haskell I/O", а это тоже самое, что и Бакалавр Информатики и Математики :)

Если вы никогда не работали с Haskell I/O, лучше будет, если вы сначала прочтёте Введение в Haskell IO

Haskell - чистый язык

Haskell — чистый язык. Это означает, что результат, возвращаемый любой функцией, полностью определён её аргументами. Псевдофункции, такие как rand() или getchar() в C, которые возвращают разные результаты при каждом вызове, невозможны в Haskell. Более того, функции в Haskell не могут иметь побочных эффектов. Это означает, что они не способны влиять на "внешний мир", т.е. не могут производить действия такие как: изменение файлов, вывод на экран, печать, отправка данных по сети и подобное.

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

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

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

Чистота Haskell позволяет вызывать только те функции, результат которых необходим для вычисления конечного результата функции более высокого уровня. Это называется "ленивое" вычисление. Это замечательная вещь для чистых математических вычислений, но как быть с операциями ввода/вывода? Если функции вроде (putStrLn "Press any key to begin formatting") не способны вернуть какой-либо осмысленный результат, как мы можем быть уверены, что компилятор не опустит вызовы этих функций или не изменит порядок их вычисления? Как в абсолютно ленивом языке мы сможем работать с алгоритмами, которые хранят состояние, или которые производят побочные эффекты? За 18-летнюю историю Haskell (читай История Haskell) было предложено множество различных решений, однако сейчас используется решение, основанное на монадах.

Что такое монада?

Что такое монада? Это нечто из математической теории категорий, и больше я ничего не знаю :) Для того, чтобы понимать как монады используются для решения задач ввода/вывода, и вам не нужно этого знать. Достаточно знания простейшей математики.

Допустим, мы хотим написать на Haskell уже знакомую функцию 'getchar'. Какого типа она должна быть? Давайте попробуем:

getchar :: Char

get2chars = [getchar,getchar]

И что же мы получим, если 'getchar' будет иметь тип 'Char'? Рассмотрим все возможные проблемы подобного определения get2chars:

  1. Так как для компилятора все функции чистые (не имеют побочных эффектов), то он не будет лишний раз вызывать 'getchar', а используют первое вычисленное значение дважды.
  2. Даже если вдруг будет произведено два вызова функции 'getchar', невозможно определить порядок этих вызовов. Кто из них будет произведён первым? Вы хотите получить два символа в том порядке, в котором ввели, или в обратном порядке? В определении 'get2chars' нет ответа на этот вопрос.

Как эта проблема может быть решена с точки зрения программиста? Можно ввести фиктивный параметр таким образом, что компилятор примет два вызова 'getchar' за "разные".

getchar :: Int -> Char

get2chars = [getchar 1, getchar 2]

Первая проблема, которую мы описали выше, решена. Теперь компилятор видит, что функции имеют разные параметры, и будет вызывать 'getchar' два раза,

Функции 'get2chars' тоже следует добавить фиктивный параметр:

getchar   :: Int -> Char
get2chars :: Int -> String

get2chars _ = [getchar 1, getchar 2]

Теперь нужно дать компилятору небольшую подсказку, чтобы он понял, какую функцию следует вычислять первой. В Haskell нет способа задать порядок вычисления... кроме зависимости между данными! Как насчёт того, чтобы добавить искусственную зависимость, которая недопустит вычисление второго 'getchar' прежде первого? Сделаем мы это следующим образом: мы будем возвращать из 'getchar' ещё один фиктивный результат, а он, в свою очередь, будет использоваться в качестве параметра в следующем вызове 'getchar':

getchar :: Int -> (Char, Int)

get2chars _ = [a,b]  where (a,i) = getchar 1
                           (b,_) = getchar i

Пока всё отлично. Теперь мы можем гарантировать, что 'a' будет прочитано прежде чем 'b', потому как для чтения 'b' требуется значение 'i', которое возвращается при чтении 'a'!

Мы добавили параметр в 'get2chars', но проблема в том, что комплятор Haskell слишком умён! Он ещё может поверить, что внешняя функция 'getchar' действительно зависит от своего параметра, но для 'get2char' всё выглядит таким образом как-будто мы жульничаем, потому что отбрасываем параметр!

Поэтому компилятор не считает себя обязанным делать вызовы в том порядке, в котором мы хотим. Как же нам это исправить? А может передавать этот фиктивный параметр в 'getchar'?! Тогда компилятор не поймёт, что параметр на самом деле не используется :)

get2chars i0 = [a,b]  where (a,i1) = getchar i0
                            (b,i2) = getchar i1


Теперь у функции 'get2chars' те же самые проблемы, что были у 'getchar'. Если вы захотите вызвать её дважды, то должны найти способ описать порядок этих вызовов. Взгляните:

get4chars = [get2chars 1, get2chars 2]  -- порядок вызовов 'get2chars' не определён.

Но мы уже знаем, как решать такие проблемы. 'get2chars' тоже должен возвращать какое-нибудь фиктивное значение, которое будет использовано для задания порядка вычисления:

get2chars :: Int -> (String, Int)

get4chars i0 = (a++b)  where (a,i1) = get2chars i0
                             (b,i2) = get2chars i1

Но какое значение должна возвращать функция 'get2chars'? Если мы будем использовать постоянное целое число, то слишком умный компилятор решит, что мы опять мухлюем :) Давай будет возвращать значение, вычисленное 'getchar'. Смотрите:

get2chars :: Int -> (String, Int)
get2chars i0 = ([a,b], i2)  where (a,i1) = getchar i0
                                  (b,i2) = getchar i1

Хотите верьте, хотите нет, но мы только что полностью воссоздали "монадическую" систему ввода/вывода языка Haskell.

Добро пожаловать в Реальный мир, детка :)

Почему бы нам не смотреть на Реальный мир, как на ещё один тип данных? Назовём этот тип 'RealWorld'. Функция 'main' имеет следующий тип:

main :: RealWorld -> ((), RealWorld)

вместо нашего Int используется фиктивный тип 'RealWorld'. Реальный мир похож на эстафетную палочку: когда main вызывает другие функции, в результате которых присутствует IO, main передаёт полученный им мир этим функциям. Все функции, выполняющие ввод-вывод, имеют тип, схожий с типом 'main'. А конкретнее, сам класс типов "IO" определён как синоним типа:

type IO a  =  RealWorld -> (a, RealWorld)

Таким образом, 'main' имеет тип "IO ()", 'getChar' имеет тип "IO Char" и так далее. Например, "IO Char" можно понимать таким образом: получить мир, (может быть, изменить мир) и вернуть Char и изменённый мир. Так выглядит 'main', вызывающий 'getChar' последовательно два раза:

getChar :: RealWorld -> (Char, RealWorld)

main :: RealWorld -> ((), RealWorld)
main world0 = let (a, world1) = getChar world0
                  (b, world2) = getChar world1
              in ((), world2)


'main' отдаёт первому 'getChar' полученный мир. Возвращённый первым 'getChar' мир отдаётся второму 'getChar'. 'main' возвращает тот мир, который был возвращён вторым 'getChar'. К чему привела такая схема вызова 'getChar'?

  1. Можно ли опустить вызов 'getChar', если его результат не нужен? Нет, так как нам всё равно нужен новый 'RealWorld', возвращаемый 'getChar' помимо символа.
  2. Можно ли переставить местами вызовы 'getChar'? Нет, так как второй 'getChar' использует 'RealWorld', возвращённый первым.
  3. Is it possible to duplicate calls? In Haskell semantics - yes, but real compilers never duplicate work in such simple cases (otherwise, the programs generated will not have any speed guarantees). (Непонятно, что понимается под duplicate calls --Beroal 00:48, 18 April 2008 (UTC))


Повторим ещё раз, что 'RealWorld' похож на эстафетную палочку, которая передаётся между IO-процедурами. IO-процедуры, явно или неявно, вызываются из 'main' в строгом порядке. Внутри IO-процедуры 'RealWorld' используется таким образом: чтобы «вычислить» новое значение 'RealWorld', мы выполняем императивные команды. Поэтому каждая IO-процедура выполняется именно в тот момент, который мы задали (момент относительно других IO-процедур). Пример:

main = do a <- ask "What is your name?"
          b <- ask "How old are you?"
          return ()

ask s = do putStr s
           readLn

Теперь вы знаете, как транслировать императивные программы на Хаскелле в низкоуровневый код и можете проверить, что IO-процедуры выполняются именно в том порядке и с теми аргументами, которые вы запланировали.


Пока мы рассматривали линейные алгоритмы. Можно ли задать ветвление? Конечно. Так задаётся управляющая структура 'when':

when :: Bool -> IO () -> IO ()
when condition action world =
    if condition
      then action world
      else ((), world)

Мы можем выполнить некоторую IO-процедуру при некотором условии, наложенном на аргумент функции 'when'. Если аргумент 'condition' ложный, то 'action' не будет вызвана, так как реальный компилятор не вызывает функцию, если её результат не нужен для подсчёта окончательного результата (под окончательным подразумевается значение 'RealWorld', возвращаемое 'main').

(Чтобы обеспечить правильное выполнение императивных команд, недостаточно, чтобы 'RealWorld' был абстрактным типом. Значение типа 'RealWorld' (то есть, реальный мир) необходимо запретить копировать. Если компилятор нарушит вышеприведённое условие, то есть, вычислит 'action' при ложном 'condition', то одно и то же значение типа 'RealWorld' будет передано в 'action' возвращено функцией 'when'. То есть компилятор должен уметь копировать мир. Но скопировать мир нельзя, так как под миром подразумеваются не только внутренности компьютера, но и все его внешние устройства, то есть, потенциально, вся Вселенная. То есть, 'RealWorld' — это не обычный тип. Этот тип обозначает данные, которые нельзя копировать. Это свойство 'RealWorld' можно распространить на другие типы данных, например, можно задать монолитный массив, который запрещено копировать из соображений производительности. Подробнее с некопируемыми данными можно познакомиться в функциональном языке Clean, uniqueness type system.)

Аналогично можно задать циклы и другие управляющие конструкции. Это вам домашнее задание.


Наконец возникает опасение, что протаскивание 'RealWorld' через все функции увеличит накладные расходы. На самом деле, компилятор, видя, что 'RealWorld' не содержит данных (так же, как и "()"), не включает 'RealWorld' в низкоуровневый императивный код. Круто? :)


Нотация '>>=' и 'do'

Все новички (включая меня :)) думают, что 'do' — это специальная конструкция для выполнения IO. Это не так — 'do' просто синтаксический сахар, делающий использование IO (и других монад) более удобным. 'do' в конечном счёте транслируется в код, передающий 'RealWorld', примерно так же, как мы это делали ранее вручную 'do' склеивает несколько IO-процедур в цепочку.

(Точнее, 'do' транслируется в функциональный код, состоящий из '>>=', '>>' и лямбда-абстракций, а 'RealWorld' подставляется на следующем этапе. Другие монады 'RealWorld' не используют.)

Каждая строка под 'do' называется командой (statement). IO-процедура — частный случай команды. 'do', применённое к только одной IO-процедуре, не делает ничего. Код:

  main = do putStr "Hello!"

транслируется в:

  main = putStr "Hello!"

Но признаком хорошего стиля считается использовать 'do' даже для одной IO-процедуры, так как это облегчает добавление новых процедур к цепочке в будущем.

Посмотрим, как можно транслировать 'do' с несколькими IO-процедурами:

main = do putStr "What is your name?"
          putStr "How old are you?"
          putStr "Nice day!"

Здесь 'do' просто выполняет несколько IO-процедур последовательно. После трансляции мы увидим, что IO-процедуры объединены в цепочку с помощью операции '>>'. Она и другая операция '>>=' относятся к «связывающим» операциям ('>>=' так и называется: 'bind'):

main = (putStr "What is your name?")
       >> ( (putStr "How old are you?")
            >> (putStr "Nice day!")
          )

'>>' просто передает мир от предыдущей IO-процедуры к последующей:

(>>) :: IO a -> IO b -> IO b
(action1 >> action2) world0 =
   let (a, world1) = action1 world0
       (b, world2) = action2 world1
   in (b, world2)

Другой вариант того же определения:

action1 >> action2 = action
  where
    action world0 = let (a, world1) = action1 world0
                        (b, world2) = action2 world1
                    in (b, world2)

Подставьте определение '>>' в код, получившийся после трансляции 'do', чтобы убедиться, что код точно так же манипулирует миром, как мы манипулировали вручную.

Есть другая разновидность команд 'do'. Они могут связывать переменную с помощью "<-" (аналогично тому, как связываются переменные в лямбда-абстракции):

main = do a <- readLn
          print a

Этот код транслируется в:

main = readLn
       >>= (\a -> print a)

В отличие от лямбда-абстракции, "<-" может связывать только одну переменную.

Функция '>>' выбрасывает значение предыдущей IO-процедуры. '>>=', наоборот, позволяет использовать это значение в следующей IO-процедуре. Поэтому к её имени добавили знак равенства. Значение предыдущей IO-процедуры передаётся как аргумент последующей IO-процедуре:

(>>=) :: IO a -> (a -> IO b) -> IO b
(action1 >>= action2) world0 =
   let (a, world1) = action1 world0
       (b, world2) = action2 a world1
   in (b, world2)

Расшифруем тип 'action2': "a -> IO b" = "a -> RealWorld -> (b, RealWorld)". Это значит, что 'action2' имеет два аргумента: аргумент типа 'a', который она использует некоторым неизвестным для нас образом, и аргумент типа 'RealWorld' для передачи управления. Все IO-процедуры имеют на один параметр больше, чем указано в их типе. Этот параметр спрятан в конструкторе типов 'IO'.

Вы можете использовать '>>' и '>>=' наряду с 'do'. Иногда это упрощает код. В этом примере нет необходимости вводить переменную — результат 'readLn' можно прямо передать в 'print':

main = readLn >>= print


Наконец, конструкция:

 do x <- action1
    action2

где 'action1' имеет тип "IO a" и 'action2' имеет тип "IO b", транслируется в:

 action1 >>= (\x -> action2)

где второй аргумент функции '>>=' имеет тип "a -> IO b". Так транслируется '<-' — связанная им переменная может использоваться в дальнейших IO-процедурах, соединённых в одну большую IO-процедуру. Тип введённой переменной определяется типом 'action1': если 'action1' имеет тип "IO a", то 'x' имеет тип "a". Можно представить себе, что '<-' «раскрывает» IO-процедуру. '<-' не является функцией, это специальная синтаксическая конструкция, как и 'do'. Её смысл в том, как она транслируется.

Следующий пример:

main = do putStr "What is your name?"
          a <- readLn
          putStr "How old are you?"
          b <- readLn
          print (a,b)

Код транслируется в:

main = putStr "What is your name?"
       >> readLn
       >>= \a -> putStr "How old are you?"
       >> readLn
       >>= \b -> print (a,b)

Скобки обущены; обе функции '>>' и '>>=' ассоциативны слева направо, но лямбда-абстракция всегда поглощает максимум кода справа от '->', насколько это возможно. Поэтому переменные 'a' и 'b' видимы во всём коде правее места, где 'a' и 'b' связываются. В качестве упражнения, расставьте скобки так, как их расставил бы парсер, и транслируйте код до 'RealWorld'. На этом можно закончить изучение 'do' и IO.


Постойте! Я забыл про третью монадную функцию: 'return'. Она заключает два своих параметра в кортеж:

return :: a -> IO a
return a world0  =  (a, world0)

Попробуйте транслировать пример с 'return':

main = do a <- readLn
          return (a*2)


Программисты, раннее изучавшие императивные языки, ошибочно полагают, что 'return' в Хаскелле возвращает управление из IO-процедуры, опуская дальнейшие IO-процедуры. Но даже из её типа видно, что это не так. 'return' используется только для того, чтобы вернуть некоторое значение типа 'a' как результат IO-процедуры типа "IO a". Как правило, 'return' стоит в конце списка команд 'do'. Попробуйте транслировать следующий пример:

main = do a <- readLn
          when (a>=0) $ do
              return ()
          print "a is negative"

чтобы убедиться, что 'print' будет выполнена при любых значениях 'a', а значение "()", которые вроде бы должен был вернуть 'main', просто потеряется. Потеряется потому, что код будет транслирован в функцию '>>', которая выбрасывает результат IO-процедуры. Для того, чтобы вернуть управление из середины IO-процедуры, используйте 'if':

main = do a <- readLn
          if (a>=0)
            then return ()
            else print "a is negative"

Также Хаскелл позволяет вставить 'do' внутрь 'if':

main = do a <- readLn
          if (a>=0) then return ()
            else do
          print "a is negative"
          ...

Так удобно выходить из середины длинного списка команд в 'do'.


Последнее упражнение: напишите реализацию функции 'liftM', которая превращает обычную функцию в монадическую:

liftM :: (a -> b) -> (IO a -> IO b)

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

liftM f action = do x <- action
                    return (f x)

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

action >>= \x -> return (x+1)

или

do x <- action
                    return (x+1)

можно написать:

fmap (+1) action

Также fmap = liftM, о которой мы говорили выше.


Mutable data (references, arrays, hash tables...)

As you should know, every name in Haskell is bound to one fixed (immutable) value. This greatly simplifies understanding algorithms and code optimization, but it's inappropriate in some cases. As we all know, there are plenty of algorithms that are simpler to implement in terms of updatable variables, arrays and so on. This means that the value associated with a variable, for example, can be different at different execution points, so reading its value can't be considered as a pure function. Imagine, for example, the following code:

main = do let a0 = readVariable varA
              _  = writeVariable varA 1
              a1 = readVariable varA
          print (a0, a1)

Does this look strange? First, the two calls to 'readVariable' look the same, so the compiler can just reuse the value returned by the first call. Second, the result of the 'writeVariable' call isn't used so the compiler can (and will!) omit this call completely. To complete the picture, these three calls may be rearranged in any order because they appear to be independent of each other. This is obviously not what was intended. What's the solution? You already know this - use IO actions! Using IO actions guarantees that:

  1. the execution order will be retained as written
  2. each action will have to be executed
  3. the result of the "same" action (such as "readVariable varA") will not be reused

So, the code above really should be written as:

main = do varA <- newIORef 0  -- Create and initialize a new variable
          a0 <- readIORef varA
          writeIORef varA 1
          a1 <- readIORef varA
          print (a0, a1)

Here, 'varA' has the type "IORef Int" which means "a variable (reference) in the IO monad holding a value of type Int". newIORef creates a new variable (reference) and returns it, and then read/write actions use this reference. The value returned by the "readIORef varA" action depends not only on the variable involved but also on the moment this operation is performed so it can return different values on each call.

Arrays, hash tables and any other _mutable_ data structures are defined in the same way - for each of them, there's an operation that creates new "mutable values" and returns a reference to it. Then special read and write operations in the IO monad are used. The following code shows an example using mutable arrays:

 import Data.Array.IO
 main = do arr <- newArray (1,10) 37 :: IO (IOArray Int Int)
           a <- readArray arr 1
           writeArray arr 1 64
           b <- readArray arr 1
           print (a, b)

Here, an array of 10 elements with 37 as the initial value at each location is created. After reading the value of the first element (index 1) into 'a' this element's value is changed to 64 and then read again into 'b'. As you can see by executing this code, 'a' will be set to 37 and 'b' to 64.


Other state-dependent operations are also often implemented as IO actions. For example, a random number generator should return a different value on each call. It looks natural to give it a type involving IO:

rand :: IO Int

Moreover, when you import C routines you should be careful - if this routine is impure, i.e. its result depends on something in the "real world" (file system, memory contents...), internal state and so on, you should give it an IO type. Otherwise, the compiler can "optimize" repetitive calls of this procedure with the same parameters! :)

For example, we can write a non-IO type for:

foreign import ccall
   sin :: Double -> Double

because the result of 'sin' depends only on its argument, but

foreign import ccall
   tell :: Int -> IO Int

If you will declare 'tell' as a pure function (without IO) then you may get the same position on each call! :)

IO actions as values

By this point you should understand why it's impossible to use IO actions inside non-IO (pure) procedures. Such procedures just don't get a "baton"; they don't know any "world" value to pass to an IO action. The RealWorld type is an abstract datatype, so pure functions also can't construct RealWorld values by themselves, and it's a strict type, so 'undefined' also can't be used. So, the prohibition of using IO actions inside pure procedures is just a type system trick (as it usually is in Haskell :)).

But while pure code can't _execute_ IO actions, it can work with them as with any other functional values - they can be stored in data structures, passed as parameters, returned as results, collected in lists, and partially applied. But an IO action will remain a functional value because we can't apply it to the last argument - of type RealWorld.

In order to _execute_ the IO action we need to apply it to some RealWorld value. That can be done only inside some IO procedure, in its "actions chain". And real execution of this action will take place only when this procedure is called as part of the process of "calculating the final value of world" for 'main'. Look at this example:

main world0 = let get2chars = getChar >> getChar
                  ((), world1) = putStr "Press two keys" world0
                  (answer, world2) = get2chars world1
              in ((), world2)

Here we first bind a value to 'get2chars' and then write a binding involving 'putStr'. But what's the execution order? It's not defined by the order of the 'let' bindings, it's defined by the order of processing "world" values! You can arbitrarily reorder the binding statements - the execution order will be defined by the data dependency with respect to the "world" values that get passed around. Let's see what this 'main' looks like in the 'do' notation:

main = do let get2chars = getChar >> getChar
          putStr "Press two keys"
          get2chars
          return ()

As you can see, we've eliminated two of the 'let' bindings and left only the one defining 'get2chars'. The non-'let' statements are executed in the exact order in which they're written, because they pass the "world" value from statement to statement as we described above. Thus, this version of the function is much easier to understand because we don't have to mentally figure out the data dependency of the "world" value.

Moreover, IO actions like 'get2chars' can't be executed directly because they are functions with a RealWorld parameter. To execute them, we need to supply the RealWorld parameter, i.e. insert them in the 'main' chain, placing them in some 'do' sequence executed from 'main' (either directly in the 'main' function, or indirectly in an IO function called from 'main'). Until that's done, they will remain like any function, in partially evaluated form. And we can work with IO actions as with any other functions - bind them to names (as we did above), save them in data structures, pass them as function parameters and return them as results - and they won't be performed until you give them the magic RealWorld parameter!


Example: a list of IO actions

Let's try defining a list of IO actions:

ioActions :: [IO ()]
ioActions = [(print "Hello!"),
             (putStr "just kidding"),
             (getChar >> return ())
            ]

I used additional parentheses around each action, although they aren't really required. If you still can't believe that these actions won't be executed immediately, just recall the real type of this list:

ioActions :: [RealWorld -> ((), RealWorld)]

Well, now we want to execute some of these actions. No problem, just insert them into the 'main' chain:

main = do head ioActions
          ioActions !! 1
          last ioActions

Looks strange, right? :) Really, any IO action that you write in a 'do' statement (or use as a parameter for the '>>'/'>>=' operators) is an expression returning a result of type 'IO a' for some type 'a'. Typically, you use some function that has the type 'x -> y -> ... -> IO a' and provide all the x, y, etc. parameters. But you're not limited to this standard scenario - don't forget that Haskell is a functional language and you're free to compute the functional value required (recall that "IO a" is really a function type) in any possible way. Here we just extracted several functions from the list - no problem. This functional value can also be constructed on-the-fly, as we've done in the previous example - that's also OK. Want to see this functional value passed as a parameter? Just look at the definition of 'when'. Hey, we can buy, sell, and rent these IO actions just like we can with any other functional values! For example, let's define a function that executes all the IO actions in the list:

sequence_ :: [IO a] -> IO ()
sequence_ [] = return ()
sequence_ (x:xs) = do x
                      sequence_ xs

No black magic - we just extract IO actions from the list and insert them into a chain of IO operations that should be performed one after another (in the same order that they occurred in the list) to "compute the final world value" of the entire 'sequence_' call.

With the help of 'sequence_', we can rewrite our last 'main' function as:

main = sequence_ ioActions


Haskell's ability to work with IO actions as with any other (functional and non-functional) values allows us to define control structures of arbitrary complexity. Try, for example, to define a control structure that repeats an action until it returns the 'False' result:

while :: IO Bool -> IO ()
while action = ???

Most programming languages don't allow you to define control structures at all, and those that do often require you to use a macro-expansion system. In Haskell, control structures are just trivial functions anyone can write.


Example: returning an IO action as a result

How about returning an IO action as the result of a function? Well, we've done this each time we've defined an IO procedure - they all return IO actions that need a RealWorld value to be performed. While we usually just execute them as part of a higher-level IO procedure, it's also possible to just collect them without actual execution:

main = do let a = sequence ioActions
              b = when True getChar
              c = getChar >> getChar
          putStr "These 'let' statements are not executed!"

These assigned IO procedures can be used as parameters to other procedures, or written to global variables, or processed in some other way, or just executed later, as we did in the example with 'get2chars'.

But how about returning a parameterized IO action from an IO procedure? Let's define a procedure that returns the i'th byte from a file represented as a Handle:

readi h i = do hSeek h AbsoluteSeek i 
               hGetChar h

So far so good. But how about a procedure that returns the i'th byte of a file with a given name without reopening it each time?

readfilei :: String -> IO (Integer -> IO Char)
readfilei name = do h <- openFile name ReadMode
                    return (readi h)

As you can see, it's an IO procedure that opens a file and returns... another IO procedure that will read the specified byte. But we can go further and include the 'readi' body in 'readfilei':

readfilei name = do h <- openFile name ReadMode
                    let readi h i = do hSeek h AbsoluteSeek i 
                                       hGetChar h
                    return (readi h)

That's a little better. But why do we add 'h' as a parameter to 'readi' if it can be obtained from the environment where 'readi' is now defined? An even shorter version is this:

readfilei name = do h <- openFile name ReadMode
                    let readi i = do hSeek h AbsoluteSeek i
                                     hGetChar h
                    return readi

What have we done here? We've build a parameterized IO action involving local names inside 'readfilei' and returned it as the result. Now it can be used in the following way:

main = do myfile <- readfilei "test"
          a <- myfile 0
          b <- myfile 1
          print (a,b)


This way of using IO actions is very typical for Haskell programs - you just construct one or more IO actions that you need, with or without parameters, possibly involving the parameters that your "constructor" received, and return them to the caller. Then these IO actions can be used in the rest of the program without any knowledge about your internal implementation strategy. One thing this can be used for is to partially emulate the OOP (or more precisely, the ADT) programming paradigm.


Example: a memory allocator generator

As an example, one of my programs has a module which is a memory suballocator. It receives the address and size of a large memory block and returns two procedures - one to allocate a subblock of a given size and the other to free the allocated subblock:

memoryAllocator :: Ptr a -> Int -> IO (Int -> IO (Ptr b),
                                       Ptr c -> IO ())

memoryAllocator buf size = do ......
                              let alloc size = do ...
                                                  ...
                                  free ptr = do ...
                                                ...
                              return (alloc, free)

How this is implemented? 'alloc' and 'free' work with references created inside the memoryAllocator procedure. Because the creation of these references is a part of the memoryAllocator IO actions chain, a new independent set of references will be created for each memory block for which memoryAllocator is called:

memoryAllocator buf size = do start <- newIORef buf
                              end <- newIORef (buf `plusPtr` size)
                              ...

These two references are read and written in the 'alloc' and 'free' definitions (we'll implement a very simple memory allocator for this example):

      ...
      let alloc size = do addr <- readIORef start
                          writeIORef start (addr `plusPtr` size)
                          return addr
                          
      let free ptr = do writeIORef start ptr

What we've defined here is just a pair of closures that use state available at the moment of their definition. As you can see, it's as easy as in any other functional language, despite Haskell's lack of direct support for impure functions.

The following example uses procedures, returned by memoryAllocator, to simultaneously allocate/free blocks in two independent memory buffers:

main = do buf1 <- mallocBytes (2^16)
          buf2 <- mallocBytes (2^20)
          (alloc1, free1) <- memoryAllocator buf1 (2^16)
          (alloc2, free2) <- memoryAllocator buf2 (2^20)
          ptr11 <- alloc1 100
          ptr21 <- alloc2 1000
          free1 ptr11
          free2 ptr21
          ptr12 <- alloc1 100
          ptr22 <- alloc2 1000


Example: emulating OOP with record types

Let's implement the classical OOP example: drawing figures. There are figures of different types: circles, rectangles and so on. The task is to create a heterogeneous list of figures. All figures in this list should support the same set of operations: draw, move and so on. We will represent these operations as IO procedures. Instead of a "class" let's define a structure containing implementations of all the procedures required:

data Figure = Figure { draw :: IO (),
                       move :: Displacement -> IO ()
                     }

type Displacement = (Int, Int)  -- horizontal and vertical displacement in points


The constructor of each figure's type should just return a Figure record:

circle    :: Point -> Radius -> IO Figure
rectangle :: Point -> Point -> IO Figure

type Point = (Int, Int)  -- point coordinates
type Radius = Int        -- circle radius in points


We will "draw" figures by just printing their current parameters. Let's start with a simplified implementation of the 'circle' and 'rectangle' constructors, without actual 'move' support:

circle center radius = do
    let description = "  Circle at "++show center++" with radius "++show radius
    return $ Figure { draw = putStrLn description }

rectangle from to = do
    let description = "  Rectangle "++show from++"-"++show to)
    return $ Figure { draw = putStrLn description }


As you see, each constructor just returns a fixed 'draw' procedure that prints parameters with which the concrete figure was created. Let's test it:

drawAll :: [Figure] -> IO ()
drawAll figures = do putStrLn "Drawing figures:"
                     mapM_ draw figures

main = do figures <- sequence [circle (10,10) 5,
                               circle (20,20) 3,
                               rectangle (10,10) (20,20),
                               rectangle (15,15) (40,40)]
          drawAll figures


Now let's define "full-featured" figures that can actually be moved around. In order to achieve this, we should provide each figure with a mutable variable that holds each figure's current screen location. The type of this variable will be "IORef Point". This variable should be created in the figure constructor and manipulated in IO procedures (closures) enclosed in the Figure record:

circle center radius = do
    centerVar <- newIORef center
    
    let drawF = do center <- readIORef centerVar
                   putStrLn ("  Circle at "++show center
                             ++" with radius "++show radius)
                   
    let moveF (addX,addY) = do (x,y) <- readIORef centerVar
                               writeIORef centerVar (x+addX, y+addY)
                               
    return $ Figure { draw=drawF, move=moveF }

    
rectangle from to = do
    fromVar <- newIORef from
    toVar   <- newIORef to

    let drawF = do from <- readIORef fromVar
                   to   <- readIORef toVar
                   putStrLn ("  Rectangle "++show from++"-"++show to)
                   
    let moveF (addX,addY) = do (fromX,fromY) <- readIORef fromVar
                               (toX,toY)     <- readIORef toVar
                               writeIORef fromVar (fromX+addX, fromY+addY)
                               writeIORef toVar   (toX+addX, toY+addY)

    return $ Figure { draw=drawF, move=moveF }


Now we can test the code which moves figures around:

main = do figures <- sequence [circle (10,10) 5,
                               rectangle (10,10) (20,20)]
          drawAll figures
          mapM_ (\fig -> move fig (10,10)) figures
          drawAll figures


It's important to realize that we are not limited to including only IO actions in a record that's intended to simulate a C++/Java-style interface. The record can also include values, IORefs, pure functions - in short, any type of data. For example, we can easily add to the Figure interface fields for area and origin:

data Figure = Figure { draw :: IO (),
                       move :: Displacement -> IO (),
                       area :: Double,
                       origin :: IORef Point
                     }


Dark side of IO monad

unsafePerformIO

Programmers coming from an imperative language background often look for a way to execute IO actions inside a pure procedure. But what does this mean? Imagine that you're trying to write a procedure that reads the contents of a file with a given name, and you try to write it as a pure (non-IO) function:

readContents :: Filename -> String

Defining readContents as a pure function will certainly simplify the code that uses it. But it will also create problems for the compiler:

  1. This call is not inserted in a sequence of "world transformations", so the compiler doesn't know at what exact moment you want to execute this action. For example, if the file has one kind of contents at the beginning of the program and another at the end - which contents do you want to see? You have no idea when (or even if) this function is going to get invoked, because Haskell sees this function as pure and feels free to reorder the execution of any or all pure functions as needed.
  2. Attempts to read the contents of files with the same name can be factored (i.e. reduced to a single call) despite the fact that the file (or the current directory) can be changed between calls. Again, Haskell considers all non-IO functions to be pure and feels free to omit multiple calls with the same parameters.

So, implementing pure functions that interact with the Real World is considered to be Bad Behavior. Good boys and girls never do it ;)


Nevertheless, there are (semi-official) ways to use IO actions inside of pure functions. As you should remember this is prohibited by requiring the RealWorld "baton" in order to call an IO action. Pure functions don't have the baton, but there is a special "magic" procedure that produces this baton from nowhere, uses it to call an IO action and then throws the resulting "world" away! It's a little low-level magic :) This very special (and dangerous) procedure is:

unsafePerformIO :: IO a -> a

Let's look at its (possible) definition:

unsafePerformIO :: (RealWorld -> (a, RealWorld)) -> a
unsafePerformIO action = let (a, world1) = action createNewWorld
                         in a

where 'createNewWorld' is an internal function producing a new value of the RealWorld type.

Using unsafePerformIO, you can easily write pure functions that do I/O inside. But don't do this without a real need, and remember to follow this rule: the compiler doesn't know that you are cheating; it still considers each non-IO function to be a pure one. Therefore, all the usual optimization rules can (and will!) be applied to its execution. So you must ensure that:

  1. The result of each call depends only on its arguments.
  2. You don't rely on side-effects of this function, which may be not executed if its results are not needed.


Let's investigate this problem more deeply. Function evaluation in Haskell is determined by a value's necessity - the language computes only the values that are really required to calculate the final result. But what does this mean with respect to the 'main' function? To "calculate the final world's" value, you need to perform all the intermediate IO actions that are included in the 'main' chain. By using 'unsafePerformIO' we call IO actions outside of this chain. What guarantee do we have that they will be run at all? None. The only time they will be run is if running them is required to compute the overall function result (which in turn should be required to perform some action in the 'main' chain). This is an example of Haskell's evaluation-by-need strategy. Now you should clearly see the difference:

- An IO action inside an IO procedure is guaranteed to execute as long as it is (directly or indirectly) inside the 'main' chain - even when its result isn't used (because the implicit "world" value it returns will be used). You directly specify the order of the action's execution inside the IO procedure. Data dependencies are simulated via the implicit "world" values that are passed from each IO action to the next.

- An IO action inside 'unsafePerformIO' will be performed only if result of this operation is really used. The evaluation order is not guaranteed and you should not rely on it (except when you're sure about whatever data dependencies may exist).


I should also say that inside 'unsafePerformIO' call you can organize a small internal chain of IO actions with the help of the same binding operators and/or 'do' syntactic sugar we've seen above. For example, here's a particularly convoluted way to compute the integer that comes after zero:

one :: Int
one = unsafePerformIO $ do var <- newIORef 0
                           modifyIORef var (+1)
                           readIORef var

and in this case ALL the operations in this chain will be performed as long as the result of the 'unsafePerformIO' call is needed. To ensure this, the actual 'unsafePerformIO' implementation evaluates the "world" returned by the 'action':

unsafePerformIO action = let (a,world1) = action createNewWorld
                         in (world1 `seq` a)

(The 'seq' operation strictly evaluates its first argument before returning the value of the second one).


inlinePerformIO

inlinePerformIO has the same definition as unsafePerformIO but with addition of INLINE pragma:

-- | Just like unsafePerformIO, but we inline it. Big performance gains as
-- it exposes lots of things to further inlining
{-# INLINE inlinePerformIO #-}
inlinePerformIO action = let (a, world1) = action createNewWorld
                         in (world1 `seq` a)
#endif

Semantically inlinePerformIO = unsafePerformIO in as much as either of those have any semantics at all.

The difference of course is that inlinePerformIO is even less safe than unsafePerformIO. While ghc will try not to duplicate or common up different uses of unsafePerformIO, we aggressively inline inlinePerformIO. So you can really only use it where the IO content is really properly pure, like reading from an immutable memory buffer (as in the case of ByteStrings). However things like allocating new buffers should not be done inside inlinePerformIO since that can easily be floated out and performed just once for the whole program, so you end up with many things sharing the same buffer, which would be bad.

So the rule of thumb is that IO things wrapped in unsafePerformIO have to be externally pure while with inlinePerformIO it has to be really really pure or it'll all go horribly wrong.

That said, here's some really hairy code. This should frighten any pure functional programmer...

write :: Int -> (Ptr Word8 -> IO ()) -> Put ()
write !n body = Put $ \c buf@(Buffer fp o u l) ->
  if n <= l
    then write' c fp o u l
    else write' (flushOld c n fp o u) (newBuffer c n) 0 0 0

  where {-# NOINLINE write' #-}
        write' c !fp !o !u !l =
          -- warning: this is a tad hardcore
          inlinePerformIO
            (withForeignPtr fp
              (\p -> body $! (p `plusPtr` (o+u))))
          `seq` c () (Buffer fp o (u+n) (l-n))

it's used like:

word8 w = write 1 (\p -> poke p w)

This does not adhere to my rule of thumb above. Don't ask exactly why we claim it's safe :-) (and if anyone really wants to know, ask Ross Paterson who did it first in the Builder monoid)

unsafeInterleaveIO

But there is an even stranger operation called 'unsafeInterleaveIO' that gets the "official baton", makes its own pirate copy, and then runs an "illegal" relay-race in parallel with the main one! I can't talk further about its behavior without causing grief and indignation, so it's no surprise that this operation is widely used in countries that are hotbeds of software piracy such as Russia and China! ;) Don't even ask me - I won't say anything more about this dirty trick I use all the time ;)

One can use unsafePerformIO (not unsafeInterleaveIO) to perform I/O operations not in predefined order but by demand. For example, the following code:

do let c = unsafePerformIO getChar
   do_proc c

will perform getChar I/O call only when value of c is really required by code, i.e. it this call will be performed lazily as any usual Haskell computation.

Now imagine the following code:

do let s = [unsafePerformIO getChar, unsafePerformIO getChar, unsafePerformIO getChar]
   do_proc s

Three chars inside this list will be computed on demand too, and this means that their values will depend on the order they are consumed. It is not that we usually need :)


unsafeInterleaveIO solves this problem - it performs I/O only on demand but allows to define exact *internal* execution order for parts of your datastructure. It is why I wrote that unsafeInterleaveIO makes illegal copy of baton :)

First, unsafeInterleaveIO has (IO a) action as a parameter and returns value of type 'a':

do str <- unsafeInterleaveIO myGetContents

Second, unsafeInterleaveIO don't perform any action immediately, it only creates a box of type 'a' which on requesting this value will perform action specified as a parameter.

Third, this action by itself may compute the whole value immediately or... use unsafeInterleaveIO again to defer calculation of some sub-components:

myGetContents = do
   c <- getChar
   s <- unsafeInterleaveIO myGetContents
   return (c:s)

This code will be executed only at the moment when value of str is really demanded. In this moment, getChar will be performed (with result assigned to c) and one more lazy IO box will be created - for s. This box again contains link to the myGetContents call

Then, list cell returned that contains one char read and link to myGetContents call as a way to compute rest of the list. Only at the moment when next value in list required, this operation will be performed again

As a final result, we get inability to read second char in list before first one, but lazy character of reading in whole. bingo!


PS: of course, actual code should include EOF checking. also note that you can read many chars/records at each call:

myGetContents = do
   c <- replicateM 512 getChar
   s <- unsafeInterleaveIO myGetContents
   return (c++s)

Welcome to the machine: the actual GHC implementation

A little disclaimer: I should say that I'm not describing here exactly what a monad is (I don't even completely understand it myself) and my explanation shows only one _possible_ way to implement the IO monad in Haskell. For example, the hbc Haskell compiler implements IO monad via continuations. I also haven't said anything about exception handling, which is a natural part of the "monad" concept. You can read the "All About Monads" guide to learn more about these topics.

But there is some good news: first, the IO monad understanding you've just acquired will work with any implementation and with many other monads. You just can't work with RealWorld values directly.

Second, the IO monad implementation described here is really used in the GHC, yhc/nhc (Hugs/jhc, too?) compilers. Here is the actual IO definition from the GHC sources:

newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #))

It uses the "State# RealWorld" type instead of our RealWorld, it uses the "(# #)" strict tuple for optimization, and it adds an IO data constructor around the type. Nevertheless, there are no significant changes from the standpoint of our explanation. Knowing the principle of "chaining" IO actions via fake "state of the world" values, you can now easily understand and write low-level implementations of GHC I/O operations.


The Yhc/nhc98 implementation

data World = World
newtype IO a = IO (World -> Either IOError a)

This implementation makes the "World" disappear somewhat, and returns Either a result of type "a", or if an error occurs then "IOError". The lack of the World on the right-hand side of the function can only be done because the compiler knows special things about the IO type, and won't overoptimise it.


Further reading

This tutorial is largely based on the Simon Peyton Jones' paper Tackling the awkward squad: monadic input/output, concurrency, exceptions, and foreign-language calls in Haskell. I hope that my tutorial improves his original explanation of the Haskell I/O system and brings it closer to the point of view of beginning Haskell programmers. But if you need to learn about concurrency, exceptions and FFI in Haskell/GHC, the original paper is the best source of information.

You can find more information about concurrency, FFI and STM at the GHC/Concurrency#Starting points page.

The Arrays page contains exhaustive explanations about using mutable arrays.

Look also at the Books and tutorials#Using Monads page, which contains tutorials and papers really describing these mysterious monads :)

An explanation of the basic monad functions, with examples, can be found in the reference guide A tour of the Haskell Monad functions, by Henk-Jan van Tuyl.

Do you have more questions? Ask in the haskell-cafe mailing list.


To-do list

If you are interested in adding more information to this manual, please add your questions/topics here.

Topics:

  • fixIO and 'mdo'
  • ST monad
  • Q monad

Questions:

  • split '>>='/'>>'/return section and 'do' section, more examples of using binding operators
  • IORef detailed explanation (==const*), usage examples, syntax sugar, unboxed refs
  • control structures developing - much more examples
  • unsafePerformIO usage examples: global variable, ByteString, other examples
  • actual GHC implementation - how to write low-level routines on example of newIORef implementation

This manual is collective work, so feel free to add more information to it yourself. The final goal is to collectively develop a comprehensive manual for using the IO monad.