Difference between revisions of "Ru/IO"
(+IO_inside link) |
(немножко вики-маркап + введение в монады) |
||
Line 1: | Line 1: | ||
+ | == Ввод и вывод - это грязь == |
||
⚫ | В императивных языках вроде C++ нет разделения функций на чистые и имеющие побочные эффекты, любая функция рассматривается как потенциально "грязная". С одной стороны, это облегчает модификацию программы (любая чисто вычислительная функция может быть переделана в имеющую побочные эффекты), с другой стороны - усложняет понимание программы, её отладку и модификацию |
||
⚫ | В императивных языках вроде '''C++''' нет разделения функций на чистые и имеющие побочные эффекты, любая функция рассматривается как потенциально "грязная". С одной стороны, это облегчает модификацию программы (любая чисто вычислительная функция может быть переделана в имеющую побочные эффекты), с другой стороны - усложняет понимание программы, её отладку и модификацию. Какая-нибудь скромная функция ''sin'' может иметь совершенно нескромные побочные эффекты, например стереть системные файлы. |
||
⚫ | В отличие от них, в Haskell все функции чётко поделены на два класса, и чистые функции не могут вызыыать нечистые. Для удобства дальнейшего изложения давайте условимся называть чистые функции просто функциями, а нечистые - процедурами. Итак, функция - это просто способ вычисления выходного значения по входным, а процедура выполняет некоторое действие (и может иметь выходное значение). |
||
⚫ | В отличие от них, в '''Haskell''' все функции чётко поделены на два класса, и чистые функции не могут вызыыать нечистые. Для удобства дальнейшего изложения давайте условимся называть чистые функции просто '''функциями''', а нечистые - '''процедурами'''. Итак, функция - это просто однозначный способ вычисления выходного значения по входным, а процедура выполняет некоторое действие (и может иметь выходное значение). |
||
⚫ | Вычисления внутри функций производятся по мере необходимости и в том порядке, в каком в них возникает необходимость. В отличие от этого процедура описывает последовательность операций, которые выполняются обязательно и обязательно в указанном порядке. Поэтому способ записи, применяемый для определения функций, не годится для процедур, и для них используется специальная do-нотация, сходная с императивными языками |
||
⚫ | Вычисления внутри функций производятся по мере необходимости и в том порядке, в каком в них возникает необходимость. В отличие от этого процедура описывает последовательность операций, которые выполняются обязательно и обязательно в указанном порядке. Поэтому способ записи, применяемый для определения функций, не годится для процедур, и для них используется специальная '''do'''-нотация, сходная с императивными языками (как '''С++''' или '''Python'''). |
||
⚫ | |||
+ | |||
+ | == Процедуры в Haskell == |
||
+ | |||
⚫ | |||
Простейшая процедура, выполняющая два действия. Как мы уже говорили, они выполняются строго в заданном порядке: |
Простейшая процедура, выполняющая два действия. Как мы уже говорили, они выполняются строго в заданном порядке: |
||
Line 13: | Line 17: | ||
</haskell> |
</haskell> |
||
− | "Действия", выполнямые в do - это в свою очередь вызовы других процедур. Мы можем присваивать "переменным" значения, возвращаемые из этих процедур, и организовывать условное выполнение: |
+ | "Действия", выполнямые в do - это в свою очередь вызовы других процедур. Мы также можем присваивать "переменным" значения, возвращаемые из этих процедур, и организовывать условное выполнение: |
+ | |||
<haskell> |
<haskell> |
||
main = do print "Hey, kid, what is your name?" |
main = do print "Hey, kid, what is your name?" |
||
Line 27: | Line 32: | ||
</haskell> |
</haskell> |
||
− | Для организации циклов, как обычно, используется рекурсия. |
+ | Для организации циклов, как обычно, используется рекурсия. |
+ | |||
+ | Возвратить значение из порцедуры можно с помощью "return". Опишем процедуру, которая вводит непустую строку: |
||
+ | |||
<haskell> |
<haskell> |
||
myGetLine = do str <- getLine |
myGetLine = do str <- getLine |
||
Line 37: | Line 45: | ||
и пример её применения: |
и пример её применения: |
||
+ | |||
<haskell> |
<haskell> |
||
main = do print "Hey, kid, what is your name?" |
main = do print "Hey, kid, what is your name?" |
||
Line 46: | Line 55: | ||
− | Совершенно аналогично функциям, процедуры могут иметь входные параметры. Для описания чистых вычислений внутри процедур можно использовать let-блоки, однако вычисления в let-блоке не могут ссылаться на имена, определённые в нижеследующих действиях или let-блоках: |
+ | Совершенно аналогично функциям, процедуры могут иметь входные параметры. Для описания чистых вычислений внутри процедур можно использовать ''let''-блоки, однако вычисления в ''let''-блоке не могут ссылаться на имена, определённые в нижеследующих действиях или ''let''-блоках: |
<haskell> |
<haskell> |
||
Line 67: | Line 76: | ||
</haskell> |
</haskell> |
||
+ | == Процедуры как параметры == |
||
⚫ | Наконец, давайте вернёмся к истокам и вспомним, что "процедуры" в хаскеле - это всего лишь грязные функции, которые могут иметь побочные эффекты, а функции в хаскеле являются "первоклассными" значениями. Это значит, что процедуры, как и любые другие функции, можно передавать в качестве параметров, сохранять в структурах данных, "добивать" параметрами. Различие всего одно - функция, применённая ко всем своим параметрами, является уже значением - это значение может храниться невычисленным только благодаря lazy evaluation. Процедура же, даже со всеми своими параметрами, остаётся процедурой (или если хотите действием) и выполняется ровно в тот момент, когда она вызвано в do-нотации. Пример: |
||
+ | |||
⚫ | Наконец, давайте вернёмся к истокам и вспомним, что "процедуры" в хаскеле - это всего лишь грязные функции, которые могут иметь побочные эффекты, а функции в хаскеле являются "первоклассными" значениями. Это значит, что процедуры, как и любые другие функции, можно передавать в качестве параметров, сохранять в структурах данных, "добивать" параметрами. Различие всего одно - функция, применённая ко всем своим параметрами, является уже значением - это значение может храниться невычисленным только благодаря [[Ru/Laziness|lazy evaluation]]. Процедура же, даже со всеми своими параметрами, остаётся процедурой (или если хотите действием) и выполняется ровно в тот момент, когда она вызвано в do-нотации. Пример: |
||
+ | |||
<haskell> |
<haskell> |
||
main = do example (print "Hi") |
main = do example (print "Hi") |
||
Line 76: | Line 88: | ||
</haskell> |
</haskell> |
||
− | Здесь "Hi" печатается не в момент вызова example (для него операция печати - это всего лишь пассивный параметр), а в тот момент, когда вызов action вставлен в do-последовательность |
+ | Здесь "Hi" печатается не в момент вызова example (для него операция печати - это всего лишь пассивный параметр), а в тот момент, когда вызов action вставлен в do-последовательность. |
+ | |||
+ | == Немного глубже в 'do' == |
||
+ | |||
+ | Поначалу кажется, что '''do''' - какая-то чёрная магия, или встроенный синтаксис для грязных процедур. На самом деле всё гораздо интереснее. Рассмотрим сперва cтруктуру блока: |
||
+ | |||
+ | <haskell> |
||
+ | main = do proc1 |
||
+ | proc2 |
||
+ | x <- proc3 |
||
+ | proc4 x |
||
+ | proc5 |
||
+ | return (x+1) |
||
+ | </haskell> |
||
+ | |||
+ | В нём могут быть вызовы процедур, типа proc1 .. proc6, или что-то вроде присваивания (x <- proc3), ниже которого доступно новое значение (x). |
||
+ | |||
+ | На деле Haskell сворачивает '''do''' блок и сводит его к нескольким операторам: (>>), (>>=) и return. Первый из них означает последовательность: |
||
+ | |||
+ | <haskell> |
||
+ | main1 = do proc1 |
||
+ | proc2 |
||
+ | |||
+ | main2 = proc1 >> proc2 -- то же самое! |
||
+ | </haskell> |
||
+ | |||
+ | А вот второй используется для "присваиваний". На самом деле каждое выражение (x <- proc3) и весь последующий "хвост" превращается в анонимную функцию (лямбду) с параметром x, и приклеивается к голове с помощью (>>=). Вспомним как пишутся лямбды (\x -> x + 1) и внимательно переведём наш пример, расставляя необязательные скобки: |
||
+ | |||
+ | <haskell> |
||
+ | main = do proc1 |
||
+ | proc2 |
||
+ | x <- proc3 |
||
+ | proc4 x |
||
+ | proc5 |
||
+ | return (x+1) |
||
+ | main = proc1 >> proc2 >>= (\x -> proc3 >> proc4 x >> proc5 >> (return (x+1))) |
||
+ | </haskell> |
||
+ | |||
+ | В контексте процедур, x >> y означает нечто вроде "сначала делать x, потом y", а выражения типа (x >>= (\y -> z)) означает "сделать x, взять результат х, подставить его в лямбду вместо y, вычислить z, и сделать z". |
||
+ | |||
+ | Зачем это всё знать? А все дело в том, что все эти операторы (и '''do''') используются не только в процедурах, а и, к примеру, в массивах. Их смысл можно даже переопределять! |
||
+ | |||
+ | Подробнее о массивах: |
||
+ | <haskell> |
||
+ | a1 = do x <- [10,100,1000] -- перебрать все эти числа как x |
||
+ | y <- [1,2,3] -- перебрать 1..3 как y |
||
+ | return (x*y) -- слить (x*y) в массив-результат |
||
+ | |||
+ | -- Результат: [10,20,30,100,200,300,1000,2000,3000] |
||
+ | </haskell> |
||
+ | |||
+ | Но об этих тонкостях лучше прочитать в статье [[Ru/Monad | о монадах]]. |
||
+ | == Ссылки == |
||
+ | * [[IO_inside]] |
||
− | Хотите знать больше? :) Читайте http://haskell.org/haskellwiki/IO_inside |
Revision as of 14:20, 18 September 2007
Ввод и вывод - это грязь
В императивных языках вроде C++ нет разделения функций на чистые и имеющие побочные эффекты, любая функция рассматривается как потенциально "грязная". С одной стороны, это облегчает модификацию программы (любая чисто вычислительная функция может быть переделана в имеющую побочные эффекты), с другой стороны - усложняет понимание программы, её отладку и модификацию. Какая-нибудь скромная функция sin может иметь совершенно нескромные побочные эффекты, например стереть системные файлы.
В отличие от них, в Haskell все функции чётко поделены на два класса, и чистые функции не могут вызыыать нечистые. Для удобства дальнейшего изложения давайте условимся называть чистые функции просто функциями, а нечистые - процедурами. Итак, функция - это просто однозначный способ вычисления выходного значения по входным, а процедура выполняет некоторое действие (и может иметь выходное значение).
Вычисления внутри функций производятся по мере необходимости и в том порядке, в каком в них возникает необходимость. В отличие от этого процедура описывает последовательность операций, которые выполняются обязательно и обязательно в указанном порядке. Поэтому способ записи, применяемый для определения функций, не годится для процедур, и для них используется специальная do-нотация, сходная с императивными языками (как С++ или Python).
Процедуры в Haskell
Главная выполняемая функция в программе на Haskell - main - является процедурой, и на ней мы рассмотрим примеры описания процедур.
Простейшая процедура, выполняющая два действия. Как мы уже говорили, они выполняются строго в заданном порядке:
main = do print "Hello, World!"
print "Haskell rules, C++ sucks!"
"Действия", выполнямые в do - это в свою очередь вызовы других процедур. Мы также можем присваивать "переменным" значения, возвращаемые из этих процедур, и организовывать условное выполнение:
main = do print "Hey, kid, what is your name?"
name <- getLine
if name=="Bulat"
then do print "Thank you, Creator"
else do print ("Hi, "++name)
Аналогичным образом можно применять case:
...
Для организации циклов, как обычно, используется рекурсия.
Возвратить значение из порцедуры можно с помощью "return". Опишем процедуру, которая вводит непустую строку:
myGetLine = do str <- getLine
if str==""
then do print "Пожалуйств, введите непустую строку"
myGetLine
else return str
и пример её применения:
main = do print "Hey, kid, what is your name?"
name <- myGetLine
if name=="Bulat"
then do print "Thank you, Creator"
else do print ("Hi, "++name)
Совершенно аналогично функциям, процедуры могут иметь входные параметры. Для описания чистых вычислений внутри процедур можно использовать let-блоки, однако вычисления в let-блоке не могут ссылаться на имена, определённые в нижеследующих действиях или let-блоках:
math x y = do
let x2 = x*2
x3 = x2*x
xy = x*y
print ("x=" ++ (show x))
print ("x в квадрате=" ++ (show x2))
print ("x в кубе=" ++ (show x3))
print ("произведение x и y=" ++ (show xy))
main = do math 2 2
math 3 4
А теперь забацаем пример, который включает все вышеприведённые извраты:
...
Процедуры как параметры
Наконец, давайте вернёмся к истокам и вспомним, что "процедуры" в хаскеле - это всего лишь грязные функции, которые могут иметь побочные эффекты, а функции в хаскеле являются "первоклассными" значениями. Это значит, что процедуры, как и любые другие функции, можно передавать в качестве параметров, сохранять в структурах данных, "добивать" параметрами. Различие всего одно - функция, применённая ко всем своим параметрами, является уже значением - это значение может храниться невычисленным только благодаря lazy evaluation. Процедура же, даже со всеми своими параметрами, остаётся процедурой (или если хотите действием) и выполняется ровно в тот момент, когда она вызвано в do-нотации. Пример:
main = do example (print "Hi")
example action = do print "Before"
action
print "After"
Здесь "Hi" печатается не в момент вызова example (для него операция печати - это всего лишь пассивный параметр), а в тот момент, когда вызов action вставлен в do-последовательность.
Немного глубже в 'do'
Поначалу кажется, что do - какая-то чёрная магия, или встроенный синтаксис для грязных процедур. На самом деле всё гораздо интереснее. Рассмотрим сперва cтруктуру блока:
main = do proc1
proc2
x <- proc3
proc4 x
proc5
return (x+1)
В нём могут быть вызовы процедур, типа proc1 .. proc6, или что-то вроде присваивания (x <- proc3), ниже которого доступно новое значение (x).
На деле Haskell сворачивает do блок и сводит его к нескольким операторам: (>>), (>>=) и return. Первый из них означает последовательность:
main1 = do proc1
proc2
main2 = proc1 >> proc2 -- то же самое!
А вот второй используется для "присваиваний". На самом деле каждое выражение (x <- proc3) и весь последующий "хвост" превращается в анонимную функцию (лямбду) с параметром x, и приклеивается к голове с помощью (>>=). Вспомним как пишутся лямбды (\x -> x + 1) и внимательно переведём наш пример, расставляя необязательные скобки:
main = do proc1
proc2
x <- proc3
proc4 x
proc5
return (x+1)
main = proc1 >> proc2 >>= (\x -> proc3 >> proc4 x >> proc5 >> (return (x+1)))
В контексте процедур, x >> y означает нечто вроде "сначала делать x, потом y", а выражения типа (x >>= (\y -> z)) означает "сделать x, взять результат х, подставить его в лямбду вместо y, вычислить z, и сделать z".
Зачем это всё знать? А все дело в том, что все эти операторы (и do) используются не только в процедурах, а и, к примеру, в массивах. Их смысл можно даже переопределять!
Подробнее о массивах:
a1 = do x <- [10,100,1000] -- перебрать все эти числа как x
y <- [1,2,3] -- перебрать 1..3 как y
return (x*y) -- слить (x*y) в массив-результат
-- Результат: [10,20,30,100,200,300,1000,2000,3000]
Но об этих тонкостях лучше прочитать в статье о монадах.