Personal tools

Ru/IO

From HaskellWiki

< Ru
Jump to: navigation, search

Contents

1 Функции и процедуры

В императивных языках вроде C++ нет разделения функций на чистые и имеющие побочные эффекты, любая функция рассматривается как потенциально "грязная". С одной стороны, это облегчает модификацию программы (к любой чисто вычислительной функции могут быть добавлены побочные эффекты), с другой стороны - усложняет понимание программы, её отладку и модификацию. Какая-нибудь скромная функция sin может иметь совершенно нескромные побочные эффекты, например стереть системные файлы.

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

Вычисления внутри функций производятся по мере необходимости и в том порядке, в каком в них возникает необходимость. В отличие от этого процедура описывает последовательность операций, которые выполняются обязательно и обязательно в указанном порядке. Поэтому способ записи, применяемый для определения функций, не годится для процедур, и для них используется специальная do-нотация, сходная с императивными языками (как С++ или Python).

Функции не могут вызывать процедуры, и это означает, что Haskell гарантирует отсутствие побочных эффектов в чистых вычислениях. По своему опыту могу сказать, что в первое время программировать с этим ограничением было неудобно, но потом привыкаешь и начинаешь просто думать по-другому, автоматически разделяя в уме алгоритмы чистых вычислений и императивную логику программы с тем, чтобы записать их отдельно друг от друга.

2 Описание процедур

Главная выполняемая функция в программе на Haskell - main - является процедурой, и на ней мы рассмотрим примеры описания процедур.

Простейшая процедура, выполняющая два действия. Как мы уже говорили, они выполняются строго в заданном порядке:

main = do print "Zdravstvuj, mir, eto ja!"
          print "Haskell zzhot, C++ ...!"

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

main = do print "Ej, parenj, kak tebja zvatj-to?"
          name <- getLine
          if name=="Bulat"
            then do print "Blagodarju, Sozdatelj"
            else do print ("Zdorovo, " ++ name)

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

main = do print "Ej, parenj, kak tebja zvatj-to?"
          name <- getLine
          case name of "Bulat"  -> do print "Blagodarju, Sozdatelj"
                       "Deniok" -> do print "Blagodarju, Kosozdatelj"
                       _        -> do print ("Zdorovo, " ++ name)

Для организации циклов, как обычно, используется хвостовая рекурсия. Например, эта программа печатает числа от 1 до 10:

main = do printRec 1
 
printRec 10 = print 10
printRec i  = do print i
                 printRec (i+1)

Для возврата результата из процедуры используется return. Опишем рекурсивную процедуру, которая дожидается ввода непустой строки:

myGetLine = do str <- getLine
               if str==""
                 then do print "Pozhalujsta, vvedite nepustuju stroku"
                         str <- myGetLine
                         return str
                 else do return str

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

main = do print "Ej, parenj, kak tebja zvatj-to?"
          name <- myGetLine
          if name=="Bulat"
            then do print "Blagodariu, Sozdatelj"
            else do print ("Zdorovo, " ++ name)

Совершенно аналогично функциям, процедуры могут иметь входные параметры. Для описания чистых вычислений внутри процедур можно использовать let-блоки, однако вычисления в let-блоке не могут ссылаться на имена, определённые в нижеследующих действиях или let-блоках:

math x y = do 
  let x2 = x*2
      x3 = x2*x
      xy = x*y
  print ("x=" ++ (show x))
  print ("x v kvadrate=" ++ (show x2))
  print ("x v kube=" ++ (show x3))
  print ("proizvedenie x i y=" ++ (show xy))
 
main = do math 2 2
          math 3 4

А теперь забацаем пример, который включает все вышеприведённые извраты:

...

3 Процедуры ввода/вывода

Для начала скажем, что типы процедур описываются точно так же, как и типы функций, только к типу результата добавляется IO. Если процедуре нечего возвратить, то используется тип результата IO (). Говоря высоким штилем, процедура - это обычная функция, тип результата которой обёрнут в конструктор типов IO.

Теперь вы можете прочитать сигнатуры процедур экранного ввода/вывода:

putChar  :: Char -> IO ()             -- выводит один символ на stdout
putStr   :: String -> IO ()           -- выводит строку на stdout
putStrLn :: String -> IO ()           -- выводит строку на stdout и добавляет от себя перевод каретки
print    :: (Show a) =>  a -> IO ()   -- печатает любое значение, 'print x = putStrLn (show x)'
 
getChar     :: IO Char                -- читает один символ с stdin
getLine     :: IO String              -- читает одну строку с stdin
getContents :: IO String              -- читает целиком содержимое stdin
readLn      :: (Read a) =>  IO a      -- читает значение любого типа

Как видите, в отличие от обычных функций, процедура может и не иметь параметров. Более того, она может одновременно не иметь ни параметров, ни результата, как например хорошо известная вам main:

main :: IO ()

Думаю, что все вышеприведённые процедуры ввода/вывода не должны вызвать затруднений. Они позволяют организовать в/в символов, строк и значений произвольных типов, для которых реализованы классы Show и Read (класс Show описывает как произвольное значение этого типа превратить в строку символов, Read - наоборот, как распарсить строковое представление значения).


Единственным роялем в кустах является процедура getContents, которая считывает целиком содержимое входного потока (начиная с текущего положения в нём) и возвращает его в виде одной большой String, использующей символы '\n' для разделения строчек ввода. Вы можете использовать функцию lines чтобы разбить его на отдельные строчки. Её необычность состоит в том, что данные не считываются в момент выполнения этой процедуры. Вместо этого возвращается лениво вычисляемая String, считывающая данные по мере их реального использования и работающая в постоянном объёме памяти (по умолчанию 512 байт - объём буфера файла). Поэтому если вы напишете, к примеру:

main = do s <- getContents
          putStr (head (lines s))

то из входного файла будет прочитана (и напечатана) только первая строчка.

Следующая программа печатает кол-во строк в файле. Хотя она читает файл целиком, но это делается постепенно, по мере вычисления функций lines и length, и поэтому она тоже работает в фиксированном объёме памяти:

main = do s <- getContents
          print (length (lines s))

Комбинация в хаскеле ленивых вычислений и ленивой реализации getContents делает его исключительно удобным инструментом для написания программ-фильтров в стиле Unix - даже лучшим, чем традиционные sh/awk/perl/ruby/python, поскольку ленивые вычисления упрощают описание сложных алгоритмов обработки данных. Для написания таких программ удобно использовать процедуру interact, которая получает в качестве параметра чистую функцию, преобразующую входную строку в выходную (таким образом, interact является higher-order процедурой):

interact :: (String->String) -> IO ()

К примеру, вышеприведённую программу, печатающую первую строчку файла, можно переписать с использованием interact так:

main = interact (head . lines)

Думаю, очевидно, что реализация interact очень проста:

interact :: (String->String) -> IO ()
interact f = do s <- getContents
                putStr (f s)

Обязательно прочтите страничку Simple_Unix_tools, где описывается, как легко можно реализовать в хаскеле множество стандартных юниксовских фильтров, помимо уже описанных здесь простых вариантов head и wc.


И ещё пара замечаний. По умолчанию для stdout используется построчная буферизация, и это означает, что строки не выводятся до появления символа конца строки ('\n'). Если вы хотите написать интерактивную программу, то вам может потребоваться отключение буферизации stdout:

main = do hSetBuffering stdout NoBuffering
          putStr "Enter yor name: "
          s <- getLine
          putStr (s++", you are a pretty girl!")

Второе: на настоящий момент стандартная хаскеловская библиотека не поддерживает Unicode I/O. Используйте библиотеку utf8-string или Streams.

Третье: stdin/stdout обрабатываются в текстовом режиме, с автоматической трансляцией \n в OS-specific line delimiter. В Windows, в частности, это \r\n. Если вам нужно обратывать бинарные файлы, то используйте Handle API.

4 Немного глубже в do, или Продвинутые возможности описания процедур

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

здесь будут описаны циклы внутри процедур, return из середины процедуры, возврат значения из if/case, результат последнего вызова как результат всей процедуры, типы процедур и отличие действий (IO a) от процедур (x->y->...->IO a), использование >> и >>=

5 Процедуры как значения первого класса (first-class values)

Наконец, давайте вернёмся к истокам и вспомним, что "процедуры" в хаскеле - это всего лишь функции, которые могут иметь побочные эффекты, а функции в хаскеле являются "первоклассными" значениями. Это значит, что процедуры, как и любые другие функции, можно передавать в качестве параметров, сохранять в структурах данных, "добивать" параметрами и т.д. Различие всего одно - функция, применённая ко всем своим параметрами, является уже значением - это значение может храниться невычисленным только благодаря lazy evaluation. Процедура же, даже со всеми своими параметрами, остаётся процедурой (или если хотите действием) и выполняется ровно в тот момент, когда она вызвана в do-нотации. Пример:

main = do example (print "Privet!!")
 
example action = do print "Do.."
                    action
                    print "Posle.."

Здесь "Привет!!" печатается не в момент вызова example (для него операция печати - это всего лишь пассивный параметр), а в тот момент, когда вызов action вставлен в do-последовательность.

6 Библиотеки

6.1 Работа с файлами

6.2 Разбор командной строки, запрос переменных среды и работа с процессами

  • см. функции getArgs, getEnv, getEnvironment, модули System.Console.GetOpt, System.Exit, System.Cmd, System.Process

6.3 Обработка исключений (exceptions) и перехват сигналов ОС

6.4 Императивные массивы и хеши

6.5 Многопоточное программирование

Пример:

Задача такова: коммуникационный сервер, с какой то периодичностью по очереди по одному COM порту читает два прибора. прочитанную информацию с этих приборов нужно к примеру записать в файл с меткой времени когда мы прочитали. и так по циклу... а таких COM портов (потоков) может быть несколько. и на каждом разное количество приборов... одновременно читать приборы нельзя. можно только по очереди... и все надо сохранить в файл как можно быстрее и читать как можно чаще одни типы данных, другие с жестко заданной периодичностью (http://rsdn.ru/Forum/message/2661238.flat.aspx)

main = do com  <- replicateM 4 newMVar  -- создаём 4 мьютекса для синхронизации работы с 4 компортами
          files <- mapM ((`fileOpen` WriteMode).show) [0..9] -- создаём файлы с именами "0".."9" для записи данных
          mapM_ (forkIO . service com files)    -- запустим отдельный поток для обслуживания каждого прибора
              [ (0, 0x48, 100, 0)   -- список (номер ком-порта, адрес Modbus, частота сканирования, номер файла),
              , (0, 0x88, 0,   1)   --   описывающий откуда и с какой частотой читать данные и куда их записывать
              ...]
          forever (threadDelay$ 10^6)   -- бесконечный пустой цикл, что позволяет крутиться в фоне потокам обслуживания приборов
 
-- |Процедура потока, обслуживающего один прибор с заданными параметрами
service com files (port, addr, delay, filenum) = do
    x <- withMVar (com!port) $ \_ -> do             -- блокируем доступ других потоков к этому ком-порту
        ...                                         -- читаем данные из ком-порта
    hPutStrLn (files!filenum) x                     -- записываем данные в файл
    threadDelay delay                               -- ожидаем заданное число микросекунд
    service com files (port, addr, delay, filenum)  -- хвостовая рекурсия используется для организации бесконечного цикла

6.6 STM (Software Transactional Memory) - новый способ многопоточного программирования

6.7 Интерфейс с С и работа с памятью

7 Ссылки

  • (на русском)
    • Основы функционального программирования: Операции ввода/вывода в Haskell'е
    • Исходники FreeArc - большой императивной программы с обширными комментариями, где вы можете найти примеры организации многопоточности, интерфейса с С, работы с памятью, использования хешей, использования процедур в качестве параметров. Там же вы найдёте реализацию недостающих в ghc библиотек для работы с файлами в Win32 (с поддержкой Unicode имён и размеров файлов >4гб), упакованными строками в UTF8-кодировке, организации многопоточной программы в стиле unix pipes, сериализации и упаковки данных
  • (english)
    • IO_inside рассказывает о внутреннем устройстве монады IO, приводит множество практических примеров императивного программирования в Haskell и содержит дополнительные ссылки
    • Tackling the Awkward Squad описывает императивное программирование, взаимодействие с языком C, многопоточное программирование и обработку исключений (exceptions)