Difference between revisions of "Ru/IO"
m (массив->список) |
(правил первую часть) |
||
Line 1: | Line 1: | ||
+ | == Функции и процедуры == |
||
− | == Ввод и вывод - это грязь == |
||
− | В императивных языках вроде '''C++''' нет разделения функций на чистые и имеющие побочные эффекты, любая функция рассматривается как потенциально "грязная". С одной стороны, это облегчает модификацию программы ( |
+ | В императивных языках вроде '''C++''' нет разделения функций на чистые и имеющие побочные эффекты, любая функция рассматривается как потенциально "грязная". С одной стороны, это облегчает модификацию программы (к любой чисто вычислительной функции могут быть добавлены побочные эффекты), с другой стороны - усложняет понимание программы, её отладку и модификацию. Какая-нибудь скромная функция ''sin'' может иметь совершенно нескромные побочные эффекты, например стереть системные файлы. |
− | В отличие от них, в '''Haskell''' все функции чётко поделены на два класса |
+ | В отличие от них, в '''Haskell''' все функции чётко поделены на два класса. Для удобства дальнейшего изложения давайте условимся называть чистые функции просто '''функциями''', а нечистые - '''процедурами'''. Итак, функция - это просто однозначный способ вычисления выходного значения по входным, а процедура выполняет некоторое действие (хотя может иметь и выходное значение). |
Вычисления внутри функций производятся по мере необходимости и в том порядке, в каком в них возникает необходимость. В отличие от этого процедура описывает последовательность операций, которые выполняются обязательно и обязательно в указанном порядке. Поэтому способ записи, применяемый для определения функций, не годится для процедур, и для них используется специальная '''do'''-нотация, сходная с императивными языками (как '''С++''' или '''Python'''). |
Вычисления внутри функций производятся по мере необходимости и в том порядке, в каком в них возникает необходимость. В отличие от этого процедура описывает последовательность операций, которые выполняются обязательно и обязательно в указанном порядке. Поэтому способ записи, применяемый для определения функций, не годится для процедур, и для них используется специальная '''do'''-нотация, сходная с императивными языками (как '''С++''' или '''Python'''). |
||
+ | |||
+ | Функции не могут вызывать процедуры, и это означает, что Haskell гарантирует отсутствие побочных эффектов в чистых вычислениях. По своему опыту могу сказать, что в первое время программировать с этим ограничением было неудобно, но потом привыкаешь и начинаешь просто думать по-другому, автоматически разделяя в уме алгоритмы чистых вычислений и императивную логику программы с тем, чтобы записать их отдельно друг от друга. |
||
== Процедуры в Haskell == |
== Процедуры в Haskell == |
||
Line 28: | Line 30: | ||
</haskell> |
</haskell> |
||
− | Аналогичным образом можно применять case: |
+ | Аналогичным образом можно применять и '''case''': |
<haskell> |
<haskell> |
||
− | main = do print " |
+ | main = do print "Эй, парень, как тебя звать-то?" |
name <- getLine |
name <- getLine |
||
− | case name of " |
+ | case name of "Булат" -> do print "Благодарю, Создатель" |
− | " |
+ | "Deniok" -> do print "Благодарю, Косоздатель" |
− | _ |
+ | _ -> do print ("Здорово, " ++ name) |
</haskell> |
</haskell> |
||
Для организации циклов, как обычно, используется рекурсия. |
Для организации циклов, как обычно, используется рекурсия. |
||
− | + | Для возврата результата из процедуры используется '''return'''. Опишем рекурсивную процедуру, которая дожидается ввода непустой строки: |
|
<haskell> |
<haskell> |
||
Line 55: | Line 57: | ||
main = do print "Эй, парень, как тебя звать-то?" |
main = do print "Эй, парень, как тебя звать-то?" |
||
name <- myGetLine |
name <- myGetLine |
||
− | if name==" |
+ | if name=="Булат" |
then do print "Благодарю, Создатель" |
then do print "Благодарю, Создатель" |
||
else do print ("Здорово, " ++ name) |
else do print ("Здорово, " ++ name) |
||
Line 84: | Line 86: | ||
== Процедуры как параметры == |
== Процедуры как параметры == |
||
− | Наконец, давайте вернёмся к истокам и вспомним, что "процедуры" в хаскеле - это всего лишь |
+ | Наконец, давайте вернёмся к истокам и вспомним, что "процедуры" в хаскеле - это всего лишь функции, которые могут иметь побочные эффекты, а функции в хаскеле являются "первоклассными" значениями. Это значит, что процедуры, как и любые другие функции, можно передавать в качестве параметров, сохранять в структурах данных, "добивать" параметрами и т.д. Различие всего одно - функция, применённая ко всем своим параметрами, является уже значением - это значение может храниться невычисленным только благодаря [[Ru/Laziness|lazy evaluation]]. Процедура же, даже со всеми своими параметрами, остаётся процедурой (или если хотите действием) и выполняется ровно в тот момент, когда она вызвана в do-нотации. Пример: |
<haskell> |
<haskell> |
Revision as of 18:51, 18 September 2007
Функции и процедуры
В императивных языках вроде C++ нет разделения функций на чистые и имеющие побочные эффекты, любая функция рассматривается как потенциально "грязная". С одной стороны, это облегчает модификацию программы (к любой чисто вычислительной функции могут быть добавлены побочные эффекты), с другой стороны - усложняет понимание программы, её отладку и модификацию. Какая-нибудь скромная функция sin может иметь совершенно нескромные побочные эффекты, например стереть системные файлы.
В отличие от них, в Haskell все функции чётко поделены на два класса. Для удобства дальнейшего изложения давайте условимся называть чистые функции просто функциями, а нечистые - процедурами. Итак, функция - это просто однозначный способ вычисления выходного значения по входным, а процедура выполняет некоторое действие (хотя может иметь и выходное значение).
Вычисления внутри функций производятся по мере необходимости и в том порядке, в каком в них возникает необходимость. В отличие от этого процедура описывает последовательность операций, которые выполняются обязательно и обязательно в указанном порядке. Поэтому способ записи, применяемый для определения функций, не годится для процедур, и для них используется специальная do-нотация, сходная с императивными языками (как С++ или Python).
Функции не могут вызывать процедуры, и это означает, что Haskell гарантирует отсутствие побочных эффектов в чистых вычислениях. По своему опыту могу сказать, что в первое время программировать с этим ограничением было неудобно, но потом привыкаешь и начинаешь просто думать по-другому, автоматически разделяя в уме алгоритмы чистых вычислений и императивную логику программы с тем, чтобы записать их отдельно друг от друга.
Процедуры в Haskell
Главная выполняемая функция в программе на Haskell - main - является процедурой, и на ней мы рассмотрим примеры описания процедур.
Простейшая процедура, выполняющая два действия. Как мы уже говорили, они выполняются строго в заданном порядке:
main = do print "Здравствуй, мир, это я!"
print "Haskell жжот, C++ ...!"
"Действия", выполнямые в do - это в свою очередь вызовы других процедур. Мы также можем присваивать "переменным" значения, возвращаемые из этих процедур, и организовывать условное выполнение:
main = do print "Эй, парень, как тебя звать-то?"
name <- getLine
if name=="Булат"
then do print "Благодарю, Создатель"
else do print ("Здорово, " ++ name)
Аналогичным образом можно применять и case:
main = do print "Эй, парень, как тебя звать-то?"
name <- getLine
case name of "Булат" -> do print "Благодарю, Создатель"
"Deniok" -> do print "Благодарю, Косоздатель"
_ -> do print ("Здорово, " ++ name)
Для организации циклов, как обычно, используется рекурсия.
Для возврата результата из процедуры используется return. Опишем рекурсивную процедуру, которая дожидается ввода непустой строки:
myGetLine = do str <- getLine
if str==""
then do print "Пожалуйста, введите непустую строку"
myGetLine
else return str
и пример её применения:
main = do print "Эй, парень, как тебя звать-то?"
name <- myGetLine
if name=="Булат"
then do print "Благодарю, Создатель"
else do print ("Здорово, " ++ 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 "Привет!!")
example action = do print "До.."
action
print "После.."
Здесь "Привет!!" печатается не в момент вызова example (для него операция печати - это всего лишь пассивный параметр), а в тот момент, когда вызов action вставлен в do-последовательность.
Немного глубже в do
Поначалу кажется, что do - какая-то чёрная магия, или встроенный синтаксис для грязных процедур. На самом деле всё это просто cинтаксический сахар, без которого можно обойтись, но который позволяет склеивать процедуры и делает их написание проще.
Для одного выражения do излишне:
main = do putStr "Привет!"
без do можно записать как:
main = putStr "Привет!"
Как же обойтись без do для нескольких действий?
main = do putStr "Как Вас зовут?"
putStr "Сколько Вам лет?"
putStr "Неплохой денёк сегодня!"
Тут do говорит, что действия идут одно за другим. Это переводится в оператор последовательности (>>):
main = (putStr "Как Вас зовут?") >> (putStr "Сколько Вам лет?") >> (putStr "Неплохой денёк сегодня!")
Более сложными являются примеры с (<-):
main = do a <- readLn
print a
Такой код переводится в:
main = readLn >>= (\a -> print a)
Выражения типа (x >>= (\y -> z)) означает "сделать x, взять его результат, подставить его в лямбду вместо y, вычислить z, и сделать z".
С этим оператором можно писать, к примеру, и так:
main = readLn >>= print
Чтобы закрепить принцип избавления от do, рассмотрим внимательно пример программы в трёх вариантах. Сначала идёт do, потом его перевод со скобками, и под конец - без них, что дозволительно благодаря приоритетам операторов:
main = main3
main1 = do putStr "Как Вас зовут? "
i <- readLn
putStr "Сколько Вам лет? "
v <- readLn
putStr "Привет, "
putStr i
putStr "! Неплохой денёк сегодня!"
main2 = (putStr "Как Вас зовут? ") >>
readLn >>= (
\i -> (
(putStr "Сколько Вам лет? ") >>
readLn >>= (
\v -> (
(putStr "Привет, ") >>
(putStr i) >>
(putStr "! Неплохой денёк сегодня!")
)
)
)
)
main3 = putStr "Как Вас зовут? " >>
readLn >>= \i ->
putStr "Сколько Вам лет? " >>
readLn >>= \v ->
putStr "Привет, " >>
putStr i >>
putStr "! Неплохой денёк сегодня!"
Зачем это всё знать? А все дело в том, что все эти операторы (и 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]
Но об этих тонкостях лучше прочитать в статье о монадах.