Difference between revisions of "Ru/IO"
(неможко из IO_inside) |
m |
||
(42 intermediate revisions by 4 users not shown) | |||
Line 1: | Line 1: | ||
+ | == Функции и процедуры == |
||
− | == Ввод и вывод - это грязь == |
||
− | В императивных языках вроде '''C++''' нет разделения функций на чистые и имеющие побочные эффекты, любая функция рассматривается как потенциально "грязная". С одной стороны, это облегчает модификацию программы ( |
+ | В императивных языках вроде '''C++''' нет разделения функций на чистые и имеющие побочные эффекты, любая функция рассматривается как потенциально "грязная". С одной стороны, это облегчает модификацию программы (к любой чисто вычислительной функции могут быть добавлены побочные эффекты), с другой стороны - усложняет понимание программы, её отладку и модификацию. Какая-нибудь скромная функция ''sin'' может иметь совершенно нескромные побочные эффекты, например стереть системные файлы. |
− | В отличие от них, в '''Haskell''' все функции чётко поделены на два класса |
+ | В отличие от них, в '''Haskell''' все функции чётко поделены на два класса. Для удобства дальнейшего изложения давайте условимся называть чистые функции просто '''функциями''', а нечистые - '''процедурами'''. Итак, функция - это просто однозначный способ вычисления выходного значения по входным, а процедура выполняет некоторое действие (хотя может иметь и выходное значение). |
Вычисления внутри функций производятся по мере необходимости и в том порядке, в каком в них возникает необходимость. В отличие от этого процедура описывает последовательность операций, которые выполняются обязательно и обязательно в указанном порядке. Поэтому способ записи, применяемый для определения функций, не годится для процедур, и для них используется специальная '''do'''-нотация, сходная с императивными языками (как '''С++''' или '''Python'''). |
Вычисления внутри функций производятся по мере необходимости и в том порядке, в каком в них возникает необходимость. В отличие от этого процедура описывает последовательность операций, которые выполняются обязательно и обязательно в указанном порядке. Поэтому способ записи, применяемый для определения функций, не годится для процедур, и для них используется специальная '''do'''-нотация, сходная с императивными языками (как '''С++''' или '''Python'''). |
||
+ | Функции не могут вызывать процедуры, и это означает, что Haskell гарантирует отсутствие побочных эффектов в чистых вычислениях. По своему опыту могу сказать, что в первое время программировать с этим ограничением было неудобно, но потом привыкаешь и начинаешь просто думать по-другому, автоматически разделяя в уме алгоритмы чистых вычислений и императивную логику программы с тем, чтобы записать их отдельно друг от друга. |
||
− | == Процедуры в Haskell == |
||
+ | |||
+ | == Описание процедур == |
||
Главная выполняемая функция в программе на '''Haskell''' - ''main'' - является процедурой, и на ней мы рассмотрим примеры описания процедур. |
Главная выполняемая функция в программе на '''Haskell''' - ''main'' - является процедурой, и на ней мы рассмотрим примеры описания процедур. |
||
Line 14: | Line 16: | ||
<haskell> |
<haskell> |
||
− | main = do print " |
+ | main = do print "Zdravstvuj, mir, eto ja!" |
− | print "Haskell |
+ | print "Haskell zzhot, C++ ...!" |
</haskell> |
</haskell> |
||
Line 21: | Line 23: | ||
<haskell> |
<haskell> |
||
− | main = do print " |
+ | main = do print "Ej, parenj, kak tebja zvatj-to?" |
name <- getLine |
name <- getLine |
||
if name=="Bulat" |
if name=="Bulat" |
||
− | then do print " |
+ | then do print "Blagodarju, Sozdatelj" |
− | else do print (" |
+ | else do print ("Zdorovo, " ++ name) |
</haskell> |
</haskell> |
||
− | Аналогичным образом можно применять case: |
+ | Аналогичным образом можно применять и '''case''': |
<haskell> |
<haskell> |
||
+ | 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) |
||
</haskell> |
</haskell> |
||
− | Для организации циклов, как обычно, используется рекурсия. |
+ | Для организации циклов, как обычно, используется хвостовая рекурсия. Например, эта программа печатает числа от 1 до 10: |
+ | |||
+ | <haskell> |
||
+ | main = do printRec 1 |
||
+ | |||
+ | printRec 10 = print 10 |
||
+ | printRec i = do print i |
||
+ | printRec (i+1) |
||
+ | </haskell> |
||
− | + | Для возврата результата из процедуры используется '''return'''. Опишем рекурсивную процедуру, которая дожидается ввода непустой строки: |
|
<haskell> |
<haskell> |
||
myGetLine = do str <- getLine |
myGetLine = do str <- getLine |
||
if str=="" |
if str=="" |
||
− | then do print " |
+ | then do print "Pozhalujsta, vvedite nepustuju stroku" |
− | myGetLine |
+ | str <- myGetLine |
− | + | return str |
|
+ | else do return str |
||
</haskell> |
</haskell> |
||
Line 49: | Line 64: | ||
<haskell> |
<haskell> |
||
− | main = do print " |
+ | main = do print "Ej, parenj, kak tebja zvatj-to?" |
name <- myGetLine |
name <- myGetLine |
||
if name=="Bulat" |
if name=="Bulat" |
||
− | then do print " |
+ | then do print "Blagodariu, Sozdatelj" |
− | else do print (" |
+ | else do print ("Zdorovo, " ++ name) |
</haskell> |
</haskell> |
||
Line 64: | Line 79: | ||
xy = x*y |
xy = x*y |
||
print ("x=" ++ (show x)) |
print ("x=" ++ (show x)) |
||
− | print ("x |
+ | print ("x v kvadrate=" ++ (show x2)) |
− | print ("x |
+ | print ("x v kube=" ++ (show x3)) |
− | print (" |
+ | print ("proizvedenie x i y=" ++ (show xy)) |
main = do math 2 2 |
main = do math 2 2 |
||
Line 78: | Line 93: | ||
</haskell> |
</haskell> |
||
− | == Процедуры |
+ | == Процедуры ввода/вывода == |
+ | Для начала скажем, что типы процедур описываются точно так же, как и типы функций, только к типу результата добавляется '''IO'''. Если процедуре нечего возвратить, то используется тип результата '''IO ()'''. Говоря высоким штилем, процедура - это обычная функция, тип результата которой обёрнут в конструктор типов '''IO'''. |
||
− | Наконец, давайте вернёмся к истокам и вспомним, что "процедуры" в хаскеле - это всего лишь грязные функции, которые могут иметь побочные эффекты, а функции в хаскеле являются "первоклассными" значениями. Это значит, что процедуры, как и любые другие функции, можно передавать в качестве параметров, сохранять в структурах данных, "добивать" параметрами. Различие всего одно - функция, применённая ко всем своим параметрами, является уже значением - это значение может храниться невычисленным только благодаря [[Ru/Laziness|lazy evaluation]]. Процедура же, даже со всеми своими параметрами, остаётся процедурой (или если хотите действием) и выполняется ровно в тот момент, когда она вызвано в do-нотации. Пример: |
||
+ | Теперь вы можете прочитать сигнатуры процедур экранного ввода/вывода: |
||
<haskell> |
<haskell> |
||
+ | putChar :: Char -> IO () -- выводит один символ на stdout |
||
− | main = do example (print "Hi") |
||
+ | putStr :: String -> IO () -- выводит строку на stdout |
||
+ | putStrLn :: String -> IO () -- выводит строку на stdout и добавляет от себя перевод каретки |
||
+ | print :: (Show a) => a -> IO () -- печатает любое значение, 'print x = putStrLn (show x)' |
||
+ | getChar :: IO Char -- читает один символ с stdin |
||
− | example action = do print "Before" |
||
− | + | getLine :: IO String -- читает одну строку с stdin |
|
− | + | getContents :: IO String -- читает целиком содержимое stdin |
|
+ | readLn :: (Read a) => IO a -- читает значение любого типа |
||
</haskell> |
</haskell> |
||
+ | Как видите, в отличие от обычных функций, процедура может и не иметь параметров. Более того, она может одновременно не иметь ни параметров, ни результата, как например хорошо известная вам '''main''': |
||
− | Здесь "Hi" печатается не в момент вызова example (для него операция печати - это всего лишь пассивный параметр), а в тот момент, когда вызов action вставлен в do-последовательность. |
||
+ | <haskell> |
||
+ | main :: IO () |
||
+ | </haskell> |
||
+ | Думаю, что все вышеприведённые процедуры ввода/вывода не должны вызвать затруднений. Они позволяют организовать в/в символов, строк и значений произвольных типов, для которых реализованы классы Show и Read (класс Show описывает как произвольное значение этого типа превратить в строку символов, Read - наоборот, как распарсить строковое представление значения). |
||
− | == Немного глубже в 'do' == |
||
− | Поначалу кажется, что '''do''' - какая-то чёрная магия, или встроенный синтаксис для грязных процедур. На самом деле всё это просто cинтаксический сахар, без которого можно обойтись, но который позволяет склеивать процедуры и делает их написание проще. |
||
+ | Единственным роялем в кустах является процедура getContents, которая считывает целиком содержимое входного потока (начиная с текущего положения в нём) и возвращает его в виде одной большой String, использующей символы '\n' для разделения строчек ввода. Вы можете использовать функцию lines чтобы разбить его на отдельные строчки. Её необычность состоит в том, что данные не считываются в момент выполнения этой процедуры. Вместо этого возвращается лениво вычисляемая String, считывающая данные по мере их реального использования и работающая в постоянном объёме памяти (по умолчанию 512 байт - объём буфера файла). Поэтому если вы напишете, к примеру: |
||
− | Для одного выражения '''do''' излишне: |
||
+ | <haskell> |
||
+ | main = do s <- getContents |
||
+ | putStr (head (lines s)) |
||
+ | </haskell> |
||
+ | то из входного файла будет прочитана (и напечатана) только первая строчка. |
||
− | <haskell>main = do putStr "Hello!"</haskell> |
||
+ | Следующая программа печатает кол-во строк в файле. Хотя она читает файл целиком, но это делается постепенно, по мере вычисления функций lines и length, и поэтому она тоже работает в фиксированном объёме памяти: |
||
− | без '''do''' можно записать как: |
||
+ | <haskell> |
||
+ | main = do s <- getContents |
||
+ | print (length (lines s)) |
||
+ | </haskell> |
||
+ | Комбинация в хаскеле ленивых вычислений и ленивой реализации getContents делает его исключительно удобным инструментом для написания программ-фильтров в стиле Unix - даже лучшим, чем традиционные sh/awk/perl/ruby/python, поскольку ленивые вычисления упрощают описание сложных алгоритмов обработки данных. Для написания таких программ удобно использовать процедуру '''interact''', которая получает в качестве параметра чистую функцию, преобразующую входную строку в выходную (таким образом, '''interact''' является higher-order процедурой): |
||
− | <haskell>main = putStr "Hello!"</haskell> |
||
+ | <haskell> |
||
− | |||
+ | interact :: (String->String) -> IO () |
||
− | Как же обойтись без '''do''' для нескольких действий? |
||
+ | </haskell> |
||
+ | К примеру, вышеприведённую программу, печатающую первую строчку файла, можно переписать с использованием '''interact''' так: |
||
<haskell> |
<haskell> |
||
+ | main = interact (head . lines) |
||
− | main = do putStr "Как Вас зовут?" |
||
− | putStr "Сколько Вам лет?" |
||
− | putStr "Неплохой денёк сегодня!" |
||
</haskell> |
</haskell> |
||
+ | Думаю, очевидно, что реализация '''interact''' очень проста: |
||
− | Тут '''do''' говорит, что действия идут одно за другим. Это переводится в оператор последовательности ''(>>)'': |
||
− | |||
<haskell> |
<haskell> |
||
+ | interact :: (String->String) -> IO () |
||
− | main = (putStr "Как Вас зовут?") >> (putStr "Сколько Вам лет?") >> (putStr "Неплохой денёк сегодня!") |
||
+ | interact f = do s <- getContents |
||
+ | putStr (f s) |
||
</haskell> |
</haskell> |
||
+ | Обязательно прочтите страничку [[Simple_Unix_tools]], где описывается, как легко можно реализовать в хаскеле множество стандартных юниксовских фильтров, помимо уже описанных здесь простых вариантов '''head''' и '''wc'''. |
||
− | Более сложными являются примеры с ''(<-)'': |
||
+ | |||
+ | |||
+ | И ещё пара замечаний. По умолчанию для stdout используется построчная буферизация, и это означает, что строки не выводятся до появления символа конца строки ('\n'). Если вы хотите написать интерактивную программу, то вам может потребоваться отключение буферизации stdout: |
||
<haskell> |
<haskell> |
||
− | main = do |
+ | main = do hSetBuffering stdout NoBuffering |
− | + | putStr "Enter yor name: " |
|
+ | s <- getLine |
||
+ | putStr (s++", you are a pretty girl!") |
||
</haskell> |
</haskell> |
||
+ | Второе: на настоящий момент стандартная хаскеловская библиотека не поддерживает Unicode I/O. Используйте библиотеку utf8-string или Streams. |
||
− | Такой код переводится в: |
||
+ | Третье: stdin/stdout обрабатываются в текстовом режиме, с автоматической трансляцией \n в OS-specific line delimiter. В Windows, в частности, это \r\n. Если вам нужно обратывать бинарные файлы, то используйте [[Ru/IO#.D0.A0.D0.B0.D0.B1.D0.BE.D1.82.D0.B0_.D1.81_.D1.84.D0.B0.D0.B9.D0.BB.D0.B0.D0.BC.D0.B8 | Handle API]]. |
||
− | <haskell>main = readLn >>= (\a -> print a)</haskell> |
||
+ | == Немного глубже в do, или Продвинутые возможности описания процедур == |
||
− | Выражения типа ''(x >>= (\y -> z))'' означает "сделать x, взять его результат, подставить его в лямбду вместо y, вычислить z, и сделать z". |
||
+ | Поначалу кажется, что '''do''' - какая-то чёрная магия, или встроенный синтаксис для записи процедур. На самом деле всё это просто cинтаксический сахар, без которого можно обойтись, но который позволяет склеивать процедуры и делает их написание проще. Но об этих тонкостях лучше прочитать в статье [[Ru/Monad | о монадах]]. |
||
− | С этим оператором можно писать, к примеру, и так: |
||
+ | ''здесь будут описаны циклы внутри процедур, return из середины процедуры, возврат значения из if/case, результат последнего вызова как результат всей процедуры, типы процедур и отличие действий (IO a) от процедур (x->y->...->IO a), использование >> и >>='' |
||
− | <haskell>main = readLn >>= print</haskell> |
||
+ | == Процедуры как значения первого класса (first-class values) == |
||
− | Зачем это всё знать? А все дело в том, что все эти операторы (и '''do''') используются не только в процедурах, а и, к примеру, в массивах. Их смысл можно даже переопределять! |
||
+ | Наконец, давайте вернёмся к истокам и вспомним, что "процедуры" в хаскеле - это всего лишь функции, которые могут иметь побочные эффекты, а функции в хаскеле являются "первоклассными" значениями. Это значит, что процедуры, как и любые другие функции, можно передавать в качестве параметров, сохранять в структурах данных, "добивать" параметрами и т.д. Различие всего одно - функция, применённая ко всем своим параметрами, является уже значением - это значение может храниться невычисленным только благодаря [[Ru/Laziness|lazy evaluation]]. Процедура же, даже со всеми своими параметрами, остаётся процедурой (или если хотите действием) и выполняется ровно в тот момент, когда она вызвана в do-нотации. Пример: |
||
− | Подробнее о массивах: |
||
<haskell> |
<haskell> |
||
+ | main = do example (print "Privet!!") |
||
− | a1 = do x <- [10,100,1000] -- перебрать все эти числа как x |
||
− | y <- [1,2,3] -- перебрать 1..3 как y |
||
− | return (x*y) -- слить (x*y) в массив-результат |
||
+ | example action = do print "Do.." |
||
− | -- Результат: [10,20,30,100,200,300,1000,2000,3000] |
||
+ | action |
||
+ | print "Posle.." |
||
</haskell> |
</haskell> |
||
+ | Здесь "Привет!!" печатается не в момент вызова example (для него операция печати - это всего лишь пассивный параметр), а в тот момент, когда вызов action вставлен в '''do'''-последовательность. |
||
− | Но об этих тонкостях лучше прочитать в статье [[Ru/Monad | о монадах]]. |
||
+ | |||
+ | == Библиотеки == |
||
+ | === Работа с файлами === |
||
+ | * [http://www.haskell.ru/io.html Описание работы с файлами в стандарте языка] |
||
+ | * (''english'') [http://www.haskell.org/tutorial/io.html Описание в Gentle Introduction] |
||
+ | * (''english'') [https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.13.9123&rep=rep1&type=pdf Описание в Tackling the Awkward Squad] |
||
+ | * см. модули System.IO, System.Directory, System.Posix.Internals |
||
+ | === Разбор командной строки, запрос переменных среды и работа с процессами === |
||
+ | * см. функции getArgs, getEnv, getEnvironment, модули System.Console.GetOpt, System.Exit, System.Cmd, System.Process |
||
+ | === Обработка исключений (exceptions) и перехват сигналов ОС === |
||
+ | * (''english'') [http://www.haskell.org/tutorial/io.html Описание в Gentle Introduction] |
||
+ | * (''english'') [https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.13.9123&rep=rep1&type=pdf Описание в Tackling the Awkward Squad] |
||
+ | * см. модули Control.Exception, GHC.ConsoleHandler/System.Posix.Signals |
||
+ | === Императивные массивы и хеши === |
||
+ | * (''english'') [http://www.haskell.org/tutorial/arrays.html Описание работы с иммутабельными массивами в Gentle Introduction] |
||
+ | * (''english'') [http://haskell.org/haskellwiki/Modern_array_libraries Описание в Modern array libraries] |
||
+ | * см. модули Data.Array.MArray, Data.HashTable |
||
+ | === Многопоточное программирование === |
||
+ | * (''english'') [https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.13.9123&rep=rep1&type=pdf Описание в Tackling the Awkward Squad] |
||
+ | * (''english'') [http://www.haskell.org/~simonmar/papers/web-server-jfp.pdf Developing a high-performance web server in Concurrent Haskell] |
||
+ | * см. модули Control.Concurrent.* |
||
+ | |||
+ | Пример: |
||
+ | |||
+ | Задача такова: коммуникационный сервер, с какой то периодичностью по очереди по одному COM порту читает два прибора. |
||
+ | прочитанную информацию с этих приборов нужно к примеру записать в файл с меткой времени когда мы прочитали. |
||
+ | и так по циклу... а таких COM портов (потоков) может быть несколько. и на каждом разное количество приборов... одновременно читать приборы нельзя. можно только по очереди... и все надо сохранить в файл как можно быстрее и читать как можно чаще одни типы данных, другие с жестко заданной периодичностью (http://rsdn.ru/Forum/message/2661238.flat.aspx) |
||
+ | |||
+ | <haskell> |
||
+ | 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) -- хвостовая рекурсия используется для организации бесконечного цикла |
||
+ | </haskell> |
||
+ | |||
+ | === STM (Software Transactional Memory) - новый способ многопоточного программирования === |
||
+ | * (''english'') [http://haskell.org/haskellwiki/Software_transactional_memory Всё об STM] |
||
+ | |||
+ | === Интерфейс с С и работа с памятью === |
||
+ | * (''english'') [https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.13.9123&rep=rep1&type=pdf Описание в Tackling the Awkward Squad] |
||
+ | * (''english'') [http://www.cse.unsw.edu.au/~chak/haskell/ffi/ffi.pdf Описание FFI (Foreign Function Interface)] |
||
+ | * (''english'') [http://www.haskell.org/ghc/docs/papers/threads.ps.gz Расширения FFI для многопоточности] |
||
+ | * см. модули Foreign.* |
||
== Ссылки == |
== Ссылки == |
||
+ | * (''на русском'') |
||
+ | ** [http://ru.wikibooks.org/wiki/%D0%9E%D1%81%D0%BD%D0%BE%D0%B2%D1%8B_%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B3%D0%BE_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F/%D0%9E%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D0%B8_%D0%B2%D0%B2%D0%BE%D0%B4%D0%B0/%D0%B2%D1%8B%D0%B2%D0%BE%D0%B4%D0%B0_%D0%B2_Haskell'%D0%B5 Основы функционального программирования: Операции ввода/вывода в Haskell'е] |
||
+ | ** [http://www.haskell.org/bz/FreeArc-sources.zip Исходники FreeArc] - большой императивной программы с обширными комментариями, где вы можете найти примеры организации многопоточности, интерфейса с С, работы с памятью, использования хешей, использования процедур в качестве параметров. Там же вы найдёте реализацию недостающих в ghc библиотек для работы с файлами в Win32 (с поддержкой Unicode имён и размеров файлов >4гб), упакованными строками в UTF8-кодировке, организации многопоточной программы в стиле unix pipes, сериализации и упаковки данных |
||
+ | * (''english'') |
||
+ | ** [[IO_inside]] рассказывает о внутреннем устройстве монады IO, приводит множество практических примеров императивного программирования в Haskell и содержит дополнительные ссылки |
||
+ | ** [https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.13.9123&rep=rep1&type=pdf Tackling the Awkward Squad] описывает императивное программирование, взаимодействие с языком C, многопоточное программирование и обработку исключений (exceptions) |
||
− | + | [[Category:Ru]] |
Latest revision as of 06:01, 1 June 2022
Функции и процедуры
В императивных языках вроде C++ нет разделения функций на чистые и имеющие побочные эффекты, любая функция рассматривается как потенциально "грязная". С одной стороны, это облегчает модификацию программы (к любой чисто вычислительной функции могут быть добавлены побочные эффекты), с другой стороны - усложняет понимание программы, её отладку и модификацию. Какая-нибудь скромная функция sin может иметь совершенно нескромные побочные эффекты, например стереть системные файлы.
В отличие от них, в Haskell все функции чётко поделены на два класса. Для удобства дальнейшего изложения давайте условимся называть чистые функции просто функциями, а нечистые - процедурами. Итак, функция - это просто однозначный способ вычисления выходного значения по входным, а процедура выполняет некоторое действие (хотя может иметь и выходное значение).
Вычисления внутри функций производятся по мере необходимости и в том порядке, в каком в них возникает необходимость. В отличие от этого процедура описывает последовательность операций, которые выполняются обязательно и обязательно в указанном порядке. Поэтому способ записи, применяемый для определения функций, не годится для процедур, и для них используется специальная do-нотация, сходная с императивными языками (как С++ или Python).
Функции не могут вызывать процедуры, и это означает, что Haskell гарантирует отсутствие побочных эффектов в чистых вычислениях. По своему опыту могу сказать, что в первое время программировать с этим ограничением было неудобно, но потом привыкаешь и начинаешь просто думать по-другому, автоматически разделяя в уме алгоритмы чистых вычислений и императивную логику программы с тем, чтобы записать их отдельно друг от друга.
Описание процедур
Главная выполняемая функция в программе на 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
А теперь забацаем пример, который включает все вышеприведённые извраты:
...
Процедуры ввода/вывода
Для начала скажем, что типы процедур описываются точно так же, как и типы функций, только к типу результата добавляется 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.
Немного глубже в do, или Продвинутые возможности описания процедур
Поначалу кажется, что do - какая-то чёрная магия, или встроенный синтаксис для записи процедур. На самом деле всё это просто cинтаксический сахар, без которого можно обойтись, но который позволяет склеивать процедуры и делает их написание проще. Но об этих тонкостях лучше прочитать в статье о монадах.
здесь будут описаны циклы внутри процедур, return из середины процедуры, возврат значения из if/case, результат последнего вызова как результат всей процедуры, типы процедур и отличие действий (IO a) от процедур (x->y->...->IO a), использование >> и >>=
Процедуры как значения первого класса (first-class values)
Наконец, давайте вернёмся к истокам и вспомним, что "процедуры" в хаскеле - это всего лишь функции, которые могут иметь побочные эффекты, а функции в хаскеле являются "первоклассными" значениями. Это значит, что процедуры, как и любые другие функции, можно передавать в качестве параметров, сохранять в структурах данных, "добивать" параметрами и т.д. Различие всего одно - функция, применённая ко всем своим параметрами, является уже значением - это значение может храниться невычисленным только благодаря lazy evaluation. Процедура же, даже со всеми своими параметрами, остаётся процедурой (или если хотите действием) и выполняется ровно в тот момент, когда она вызвана в do-нотации. Пример:
main = do example (print "Privet!!")
example action = do print "Do.."
action
print "Posle.."
Здесь "Привет!!" печатается не в момент вызова example (для него операция печати - это всего лишь пассивный параметр), а в тот момент, когда вызов action вставлен в do-последовательность.
Библиотеки
Работа с файлами
- Описание работы с файлами в стандарте языка
- (english) Описание в Gentle Introduction
- (english) Описание в Tackling the Awkward Squad
- см. модули System.IO, System.Directory, System.Posix.Internals
Разбор командной строки, запрос переменных среды и работа с процессами
- см. функции getArgs, getEnv, getEnvironment, модули System.Console.GetOpt, System.Exit, System.Cmd, System.Process
Обработка исключений (exceptions) и перехват сигналов ОС
- (english) Описание в Gentle Introduction
- (english) Описание в Tackling the Awkward Squad
- см. модули Control.Exception, GHC.ConsoleHandler/System.Posix.Signals
Императивные массивы и хеши
- (english) Описание работы с иммутабельными массивами в Gentle Introduction
- (english) Описание в Modern array libraries
- см. модули Data.Array.MArray, Data.HashTable
Многопоточное программирование
- (english) Описание в Tackling the Awkward Squad
- (english) Developing a high-performance web server in Concurrent Haskell
- см. модули Control.Concurrent.*
Пример:
Задача такова: коммуникационный сервер, с какой то периодичностью по очереди по одному 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) -- хвостовая рекурсия используется для организации бесконечного цикла
STM (Software Transactional Memory) - новый способ многопоточного программирования
- (english) Всё об STM
Интерфейс с С и работа с памятью
- (english) Описание в Tackling the Awkward Squad
- (english) Описание FFI (Foreign Function Interface)
- (english) Расширения FFI для многопоточности
- см. модули Foreign.*
Ссылки
- (на русском)
- Основы функционального программирования: Операции ввода/вывода в Haskell'е
- Исходники FreeArc - большой императивной программы с обширными комментариями, где вы можете найти примеры организации многопоточности, интерфейса с С, работы с памятью, использования хешей, использования процедур в качестве параметров. Там же вы найдёте реализацию недостающих в ghc библиотек для работы с файлами в Win32 (с поддержкой Unicode имён и размеров файлов >4гб), упакованными строками в UTF8-кодировке, организации многопоточной программы в стиле unix pipes, сериализации и упаковки данных
- (english)
- IO_inside рассказывает о внутреннем устройстве монады IO, приводит множество практических примеров императивного программирования в Haskell и содержит дополнительные ссылки
- Tackling the Awkward Squad описывает императивное программирование, взаимодействие с языком C, многопоточное программирование и обработку исключений (exceptions)