Садржај

Улаз и излаз

Видели смо да је циљ функционалног програмирања решавање проблема употребом чистих функција. Међутим, јасно нам је да на тај начин није могуће правити комплетне апликације, јер учитавање података (било из датотеке било са стандардног улаза), испис података, цртање по екрану, коришћење графичког корисничког интерефејса, мрежна комуникација и многе друге акције без којих се не може замислити једна апликација представљају бочне ефекте. Решење које се у језику Haskell нуди је да се функције које производе бочне ефекте (нечисте функције) у потпуности раздвоје од чистих функција. Већи део сваког програма, наравно, треба да се састоји од чистих функција, које се користе унутар нечистих функција.

Размотримо пример исписа података на екран. Све досадашње функције смо дефинисали у .hs датотекама, учитавали их у интерактивни интерпретатор (на пример ghci), а затим их у интерпретатору позивали. То нису били програми, јер нису садржали главну функцију main која би се позвала приликом покретања програма. Размотримо први пример комплетног програма.

main :: IO ()
main = putStrLn "Zdravo svete!"

Овај програм се може компилирати (на пример, помоћу компилатора ghc) и на тај начин добити извршиви програм који, када се покрене, на стандардни излаз исписује Zdravo svete!.

Шта је заправо тип IO () и како је могуће да у чистом функцоналном језику имамо функцију која није чиста? IO је тип који има карактеристике које се у теорији називају монада и које нећемо детаљније коментарисати у овом тексту. Неформално можемо разумети да тип IO преставља акцију која може да врши улаз из неке датотеке или са стандардног улаза и може да врши неки испис или у неку датотеку или на стандардни излаз, при чему може, али не мора да врати податак неког датог типа. Функција main је типа IO (), што означава да је у питању улазно-излазна акција која не враћа податак (тип () се назива unit type и користи се да се нагласи да функција не враћа неку смислену вредност). Иако су функције попут main суштински нечисте, јер имају неке бочне ефекте, формално гледано оне могу да постоје у чистом функционалном језику, јер оне заправо не извршавају ове акције, већ само враћају акције, које онда извршава оперативни систем приликом покретања програма. На пример, тип функције putStrLn је следећи:

putStrLn :: String -> IO ()

Дакле, можемо сматрати да она не исписује дату ниску на екран, већ да за дату ниску она враћа акцију која проузрокује испис те ниске на екран.

Акције се могу комбиновати коришћењем специјалне do нотације.

main :: IO ()
main = do
  putStrLn "Zdravo svima!"
  putStrLn "Želim Vam prijatan dan"

Коришћењем ове нотације можемо остварити и читање података.

main :: IO ()
main = do
   putStrLn "Kako se zoveš: "
   ime <- getLine
   putStrLn ("Zdravo! Ti se zoveš " ++ ime)

Функција getLine има тип:

getLine :: IO String

То је, дакле, улазно-излазна акција, међутим, за разлику од putStrLn "Zdravo" која је улазно-излазна акција која не враћа никакав резултат тј. која је типа IO (), getLine је улазно-излазна акција која враћа резултат типа String. Линијом ime <- getLine се ниска која се добија извршавањем акције getLine именује са ime и у наставку do блока ime означава вредност која се добија извршавањем улазно-излазне акције getLine. Обратите пажњу на то да тип функције getLine није String и да се функција getLine не може позивати из чистих функција (јер оне не могу имати бочне ефекте, а getLine је акција која има бочни ефекат). Са друге стране, унутар функције main можемо без икаквих проблема употребити било коју чисту функцију и/или операцију (нпр. у претходном примеру употребљена је операција конкатенације ++). Тип функција какве су putStrLn и getLine говори да су то акције са бочним ефектима и на основу строгих правила типизираности имамо гаранцију да оне никада неће бити употребљене унутар неке чисте функције, чиме се постиже јасна раздвојеност између чистих функција и функција са бочним ефектима.

Још једна функција са бочним ефектима која се често може користити за програме који раде са улазом и излазом је

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

Овој функцији се предаје чиста функција која прима ниску која ће садржати цео садржај учитан са стандардног улаза, а враћа ниску која садржи цео садржај који треба да буде исписан на стандардни излаз. Дакле, када год имамо програм који учитава фиксан скуп података са стандардног улаза, обрађује их и исписује резултат на стандардни излаз, можемо да скоро сву логику програма имплементирамо унутар неке чисте функције, и на самом крају да употребимо interact да бисмо добили извршиви програм. На пример, наредни програм броји линије учитане са стандардног улаза.

countLines :: String -> String
countLines input = show (length (lines input))

main:: IO ()
main = interact countLines

Овде се користи функција lines:: String -> [String] која прелама стринг који у себи може да садржи ознаке за прелазак у нови ред у листу појединачних линија. Фунција length:: [a] -> Int израчунава дужину добијеног низа линија, а функција show:: Int -> String на крају ту дужину конвертује у стринг који ће бити приказан на екрану.

Наравно, countLines можемо изразити и као композицију функција.

countLines :: String -> String
countLines = show . length . lines

Тема постизања бочних ефеката у чистим функционалним језицима је веома важна, јер без њих није могуће замислити израду комплетних програма. Ми се њом нећемо детаљно бавити, а заинтересоване ученике упућујемо да се о овоме мало више информишу на интернету (на пример, књига Learn you a Haskell for Great Good, која је слободно доступна на интернету, је заиста одличан материјал за детаљније изучавање језика Haskell).

Задаци за самостални рад

  1. Истражи на интернету шта су монаде, шта су функтори и шта су апликативни функтори? Како се ови концепти користе у језику Haskell? Проучи монаде IO, Reader, Writer, State, ST, … Зашто су List и Maybe монаде?

  1. Истражи мало детаљније шта је do нотација и шта се све може навести унутар блока do.

(Created using Swinx, RunestoneComponents and PetljaDoc)
© 2022 Petlja
A- A+