Ru/IO

From HaskellWiki
< Ru
Revision as of 17:02, 18 September 2007 by Deniok (talk | contribs) (добавил пример с case)
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 "Здравствуй, мир, это я!"
          print "Haskell жжот, C++ ...!"

"Действия", выполнямые в do - это в свою очередь вызовы других процедур. Мы также можем присваивать "переменным" значения, возвращаемые из этих процедур, и организовывать условное выполнение:

main = do print "Эй, парень, как тебя звать-то?"
          name <- getLine
          if name=="Булат"
            then do print "Благодарю, Создатель"
            else do print ("Здорово, " ++ name)

Аналогичным образом можно применять case:

main = do print "Hey, kid, what is your name?"
          name <- getLine
          case name of "Bulat"   -> do print "Thank you, Creator"
                       "Haskell" -> do print "Good day, Mr.Curry"
                       _         -> do print ("Hi, " ++ name)

Для организации циклов, как обычно, используется рекурсия.

Возвратить значение из порцедуры можно с помощью "return". Опишем процедуру, которая вводит непустую строку:

myGetLine = do str <- getLine
               if str==""
                 then do print "Пожалуйста, введите непустую строку"
                         myGetLine
                 else return str

и пример её применения:

main = do print "Эй, парень, как тебя звать-то?"
          name <- myGetLine
          if name=="Bulat"
            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]

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

Ссылки