Ru/IO

From HaskellWiki
< Ru
Revision as of 14:26, 18 September 2007 by Tino (talk | contribs) (еще немого маркап)
Jump to navigation Jump to search
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.

Ввод и вывод - это грязь

В императивных языках вроде 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) и внимательно переведём наш пример, расставляя необязательные скобки:

main1 = do proc1
           proc2
           x <- proc3
           proc4 x
           proc5
           return (x+1)
main2 = proc1 >> proc2 >>= (\x -> proc3 >> proc4 x >> proc5 >> (return (x+1)))

-- main1 - то же, что и main2

В контексте процедур, 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]

Но об этих тонкостях лучше прочитать в статье о монадах.

Ссылки