Haskel programi
U dosadašnjim lekcijama prikazali smo iskljičivo rad u interaktivnom okruženju GHCi. Sada ćemo se upoznati sa Haskel kompajlerom i naučićemo kako da kompajliramo Haskel kôd u izvršne datoteke. Takođe ćemo se upoznati sa metodama koje nam omogućavaju ulaz i izlaz iz programa (bilo kroz terminal, bilo kroz datoteke), kao i nekim osnovama organizacije Haskell koda.
Hello World!
Većina knjiga o programskim jezicima započinje sa Hello World! programom: jednostavnim programom čija je jedina svrha da ispiše pozdrav u terminal. Sa mnogim Haskel knjigama, pa tako i ovom koju čitate, to nije slučaj. Umesto toga, na početku se prikazuje rad kroz interaktvno okruženje ghci da bi se osnovne osobine jezika dobro upoznale. Tek onda se demonstrira pisanje programa koji se samostalno izvšavaju i koji mogu prilikom izvršavanja da intereaguju sa korisnikom, datotekama, itd... Razlog ovakvog pristupa je jednostavan: pisanje samostalnih Haskel programa zahteva razumevanje nešto naprednijih koncepata Haskel jezika. Ali čitaoca ne treba da brine ova konstatacija jer ćemo mi te koncepte ovde postupno uvoditi. Znanje iz prethodnih lekcija značajno će olakšati čitanje ove lekcije. Ali o tom potom. Pogledajmo prvo Hello world.
Definišimo jednu datoteku, na primer hello.hs, i u nju smestimo naredni kôd (koji ćemo u nastavku pojasniti):
main :: IO () main = putStrLn "Hello World!"
I kao što je u većini drugih programskih jezika uvek potrebno definisati main funkciju koja predstavlja početnu tačku izvršavanja programa, tako i svaki Haskel program koji se kompajlira mora imati definisanu vrednost main tipa IO ().
Kôd možemo iskompajlirati u izvršni program pomoću GHC kompjalera. U sistemskom promptu1 potrebno je pokrenuti kompajler ghc i kao jedini argument proslediti mu ime datoteke koju je potrebno iskompajlirati:
$ ghc hello.hs [1 of 1] Compiling Main ( hello.hs, hello.o ) Linking hello ...
Ako je kompilacija prošla uspešno, u direktorijumu u kom je hello.hs naći će se izvršna datoteka hello (ili hello.exe ako govorimo o Windows sistemima). Pokretanjem datoteke dobijamo dugo očekivani pozdrav:
$ ./hello Hello World!
./hello, potrebno je uneti .\hello.exe. U nastavku lekcije ćemo ignorisati ovu razliku.Gorenavedeni kôd nije dugačak, ali jeste drugačiji od koda kojeg smo do sada pisali. Funkcija putStrLn primenjena na nisku "Hello World!" ne izgleda neobično, ali tip IO () svakako izgleda.
Tip () nam je poznat iz prethodne lekcije. U pitanju je tip koji sadrži samo jednu vrednost, vrednost koja se označava sa (). Tip IO je zapravo apstraktni tip vrste * -> *. Eksplicitnu definiciju definiciju tipa IO ne možemo navesti, ali možemo objasniti šta ovaj tip predstavlja.
Za neki tip T, tip IO T predstavlja proceduru (u najširem smislu te reči), koja se može izvršiti tokom izvršavanja programa, i čiji rezultat je vrednost tipa T. Vrednosti koje poseduju tip oblika IO T nazivamo akcije. Prema tome, main je jedna akcija koja će se izvršiti tokom izvršavanja programa i čiji rezultat je vrednost tipa (), odnosno baš ().
Zapravo main je akcija koja ima poseban status: to je akcija koja se uvek prva izvršava pri radu programa, i poziva ostale akcije. U navedenom programu, akcija main je pozvala akciju putStrLn "Hello World!". Funkcija putStrLn poseduje2 tip String -> IO (), pa vrednost primene putStrLn na neku nisku je akcija tipa IO () koja se može dodeliti imenu main. Dakle, putStrLn je funkcija koja nisku preslikava u akciju koja će ispisati tu nisku, ali vrednost putStrLn sama za sebe nije akcija.
Za sada nismo objasnili zašto tako naizgled jednostavna funkcija poput main ima tako neobičan tip, niti smo objasnili kako se funkcija poput putStrLn može iskoristiti unutar funkcija koje smo do sada pisali. Tip IO je za sada otvara više pitanja nego što daje odgovora, ali ćemo pokušati da kroz ovu lekciju odgovorimo na neka od tih pitanja.
Interakcija sa konzolom
Programi koji ispisuju jednu nisku nisu mnogo korisni, te ćemo prvo pokazati kako se akcije mogu nadovezivati. U tu svrhu koristi se do notacija koja dozvoljava da se akcije navedu u zasebnim redovima jednog bloka. Navedene akcije biće izvršene sekvencijalno, redom kojim su navedene.
main :: IO () main = do putStrLn "Hello World!" putStrLn "Don't worry," putStrLn "Be happy." putStrLn "Bye!"
do. Ova reč otvara blok linija, a svaka akcija mora se navesti u posebnoj liniji.Kompilacijom i izvršavanjem programa dobijamo očekivani rezultat
$ ghc hello.hs [1 of 1] Compiling Main ( hello.hs, hello.o ) Linking hello ... $ ./hello Hello World! Don't worry, Be happy. Bye!
Na linijama unutar do bloka moraju se navesti IO akcije, ali mi te akcije možemo i definisati i van odgovarajućeg bloka. Na primer, možemo kreirati funkciju pozdrav koja uzima ime (nisku) i vraća akciju koja ispisuje pozdrav sa prosleđenim imenom.
pozdrav :: String -> IO () pozdrav x = do putStrLn ("Zdravo " ++ x ++ "!") main :: IO () main = do pozdrav "Nikola" pozdrav "Nikolina"
do bloku nalazi samo jedna akcija (kao u slučaju pozdrav funkcije), tada nije potrebno otvarati do blok. Pogledati kako je u gore definisana main akcija sa jednom akcijom.$ ./hello Zdravo Nikola! Zdravo Nikolina!
Osim putStrLn funkcije, u Prelidu su dostupne i funkcije putChar :: Char -> IO () i putStr :: String -> IO (). Funkcija putStrLn se razlikuje od putStr samo po tome što dobijana akcija uz prosleđenu nisku ispisuje i karakter za novi red. Funkcija putChr daje akciju koja će ispisati jedan karakter.
Uz ispisivanje sadržaja u terminal, neophodno je i prihvatati korisnički unos. Za "prihvatanje" linije koristi se akcija getLine :: IO String dostupna u Prelidu. Tip IO String označava da se radi o akciji čiji rezultat je niska - to će biti niska koju je korisnik uneo. Da bi se toj vrednosti pristupilo, potrebno ju je dodeliti novom imenu uz pomoć strelice <- unutar do bloka:
main :: IO () main = do putStr "Vaše ime je: " ime <- getLine putStrLn ("Zdravo " ++ ime ++ "!")
<- se vrednost tipa T "otpakuje" iz vrednosti tipa IO T.$ ./hello Vaše ime je: Marija Zdravo Marija!
Vrednost koju "otpakujemo" sa <- je "obična" vrednost poput onih sa kojima smo do sada "radili". Otpakovanu vrednost možemo dalje prosleđivati funkcijama. Akcije koristimo samo kada želimo da ispišemo ili učitamo podatke.
inicijali :: String -> String -> String inicijali ime prezime = (head ime) ++ "." ++ (head prezime) ++ "." main :: IO () main = do putStr "Vaše ime: " ime <- getLine putStr "Vaše prezime: " prezime <- getLine putStrLn (inicijali ime prezime)
Unutar do bloka je dozvoljeno definisati vrednosti koristeći let ključnu reč. U do blokovima definicije moraju imati smisleni redosled: definicije koje sadrže definisane (ili "otpakovane") vrednosti moraju se nalaziti nakon tih definicija. Prethodni primer smo mogli i da napišemo ovako:
inicijali :: String -> String -> String inicijali ime prezime = (head ime) ++ "." ++ (head prezime) ++ "." main :: IO () main = do putStr "Vaše ime: " ime <- getLine putStr "Vaše prezime: " prezime <- getLine let inic = inicijali ime prezime putStrLn inic
Zadatak 1. U jednoj od prošlih lekcija smo konstruisali funkciju uBroj :: String -> Int koja prevodi nisku u broj. Koristeći ovu funkciju definisati main akciju koja uzima dva broja, a zatim ispsuje njihov zbir. Pretpostaviti da će obe unete niske predstavljati validan zapis broja.
main :: IO () main = do a <- getLine b <- getLine let zbir = (uBroj a) + (uBroj b) putStrLn (show zbir)
Zadatak 2. Napisati quine, to jest program koji ispisuje sopstveni kôd.
Napisati quine u bilo kom jeziku nije lak zadatak. Dobra početna tačka je program koji ispisuje neku nisku. Odabraćemo nisku koja je početak koda samog programa:
main = putStrLn "main = putStrLn"
Kompilacijom i pokretanjem programa dobijamo početak koda.
$ ghc quine.hs [1 of 1] Compiling Main ( quine.hs, quine.o ) Linking quine ... $ ./quine main = putStrLn
Deluje da je dovoljno da ostatak koda smestimo u nisku:
main = putStrLn "main = putStrLn \"main = putStrLn\""
\". Ovim smo definisali nisku main = putStrLn "main = putStrLn".$ ./quine main = putStrLn "main = putStrLn"
Kako mi dodajemo tekst u nisku to program postaje duži te moramo da dodajemo još više teksta, i tako dalje... Takav proces se nikad neće zaustaviti. Zbog toga moramo konstruisati nisku koju ćemo ispisati. Vratimo se na prvi pokušaj rešenja, tj. program main = putStrLn "main = putStrLn" i dodajmo funkciju (\s -> s ++ s) koja "duplira" nisku. Kôd te funkcije ćemo takođe dodati u nisku:
main = putStrLn ((\s->s++s) "main = putStrLn (\\s->s++s)")
\\$ ./quine main = putStrLn (\s->s++s)main = putStrLn (\s->s++s)
Poredeći kôd i ispis programa vidimo da nedostaje par zagrada, par navodnika, razmak, i kosa crta u lambda funkciji. Zagrade i razmak možemo da pokušamo da dodamo u nisku i telo lambda funkcije:
main = putStrLn ((\s->s++" "++s++")") "main = putStrLn ((\\s->s++\" \"++s++\")\")")
$ ./quine main = putStrLn ((\s->s++" "++s++")") main = putStrLn ((\s->s++" "++s++")"))
Rešenje sad deluje dosta blizu. Neophodno je sad postaviti drugi deo ispisa u navodnike i dodati kose crte. Tj. potrebno je nisku ispisati u obliku u kom bi bila zapisana kao Haskel literal. Upravo to radi show funkcija. Sada je dovoljno umesto \s->s++" "++s++")" napisati \s->s++" "++show s++")".
main = putStrLn ((\s->s++" "++show s++")") "main = putStrLn ((\\s->s++\" \"++show s++\")\")")
$ ./quine main = putStrLn ((\s->s++" "++show s++")") "main = putStrLn ((\\s->s++\" \"++show s++\")\")")
Akcije i funkcije
Haskel početnici često poistovećuju akcije (poput putStrLn "Hello World" :: IO () i getLine :: IO String) sa funkcijama jer pojam funkcije u drugim programskim jezicima obuhvata oba pojma u Haskelu. Na primer, u C jeziku postoji funkcija (funkcija u smislu C jezika) getline koja je u potpunosti analogna Haskel akciji getLine (čak imaju i skoro identično ime). Ali kao što ćemo uskoro videti, Haskel akcije ne mogu biti Haskel funkcije! Prema tome, pojam funkcije u drugim jezicima je širi pojam od pojma funkcije u Haskelu.
U Haskelu funkcije predstavljaju pravilnosti po kojima se vrednostima nekog tipa pridružuju vrednosti drugog tipa. Ključna reč u navedenoj definiciji je "pravilnosti", jer ona označava osobinu funkcija da imaju istu povratnu vrednost kada se primene na iste argumente. Tačnije, ako je \(x=y\), tada je i \(f(x)=f(y)\). U tom smislu, vrednost getLine ne može biti funkcija: njena vrednost prilikom svakog "pozivanja" može biti drugačija (zavisi od toga šta je korisnik konzole uneo). Sličan argument važi za sve ostale procedure čija povratna vrednost zavisi od vrednosti koje se "nalaze" van samog programa. Na primer, to mogu biti procedure koje vraćaju sadržaj neke datoteke, korisnički unos, vreme, sadržaj nekog resursa na internetu, podatke iz baze, i tako dalje. Gledano iz ugla Haskel programa, sve ovakve procedure vraćaju vrednosti iz spoljnog sveta. Pod terminom spoljni svet smatraju se sve vrednosti koje nisu definisane unutar (Haskel) programa.
Osim što su akcije neophodne za "preuzimanje" informacija iz spoljnog sveta, akcije moramo koristiti i kada "šaljemo" informacije u spoljni svet. Na primer, ako štampamo tekst u terminal, čuvamo podatke na disk, šaljemo zahteve veb serverima i tako dalje. Navedene procedure poseduju propratne efekte3 što znači da menjaju stanje spoljnog sveta. Haskel funkcije nemaju propratne efekte, dok Haskel akcije mogu imati propratne efekte.
Takođe, primetimo još jednu razliku između funkcija i akcija. Funkcije uvek moramo primeniti na vrednost (što takođe nazivamo i pozivanje sa vrednošću), tj. moramo napisati kôd poput f x. Sa druge strane, akcije ne primenjujemo na vrednosti, zbog čega za akcije kažemo da se pokreću unutar do bloka.
Apstraktni tip IO predstavlja "most" koji spaja spoljni svet sa programom. Sva komunikacija programa sa spoljnim svetom mora se odviti pokretanjem akcija sa IO tipom, a svaku IO akciju je moguće pokrenuti samo iz neke druge IO akcije. Zaista, nemoguće je pokrenuti4 akcije unutar funkcija. Dakle, akcije su uvek pokrenute od strane drugih akcija, i tako dalje, sve do main akcije.
Naravno, definisanjem akcije ne znači da će ta akcija biti i pokrenuta. Pokretanjem narednog programa videćemo samo dva pozdrava u konzoli. Obratite pažnju da će akcija a3 biti pokrenuta iz a1 akcije a ne iz main akcije.
main :: IO () main = do a1 a1 :: IO () a1 = do putStrLn "Hello" a3 a2 :: IO () a2 = do putStrLn "Ciao" a3 :: IO () a3 = do putStrLn "Zdravo"
Razliku između definisanja i pokretanja akcije je moguće uvideti i u narednom kodu. Znak jednakosti u prvoj liniji do bloka određuje definiciju akcije a koja nije pokrenuta! Sa druge strane, strelica <- u narednoj liniji pokreće akciju sa desne strane, i rezultat pokretanja te akcije dodeljuje imenu r. Kako je putStr :: String -> IO (), to će vrednost r imati tip (a i vrednost) ().
main :: IO () main = do let a = putStr "Hello" r <- putStr "Zdravo" putStrLn "!"
Za korisnike drugih programskih jezika, ovakvo razdvajanje pojma akcija od pojma funkcija deluje kao nepotrebno komplikovanje. Ali zapravo se ovde radi o prednosti Haskel jezika u odnosu na druge jezike. Dok u mnogim drugim jezicima (C/C++, Python, Java, Javaskript, Go, itd..) ne postoji nikakva kontrola interakcije programa sa spoljnim svetom5, u Haskelu je kroz sistem tipova (tj. IO tip) obezbeđeno jasno razdvajanje dela koda koji intereaguje sa spoljnim svetom od dela koda koji to ne čini. Na taj način postignuto je se mnoge potencijalne greške (bagovi), uoče i isprave tokom kompilacije programa.
U navedenim programskim jezicima postoji slična podela koda. Programeri često govore o čistom6 i nečistom7 kodu. Čiste funkcije su one funkcije koje zadovoljavaju dva pravila: za iste parametre vraćaju iste povratne vrednosti i nemaju propratne efekte. To su upravo one dve osobine koje smo naveli za Haskel funkcije. Prema tome možemo reći da su Haskel funkcije uvek čiste dok su akcije nečiste. Primetimo da u navedenim programskim jezicima razlika između čistog i nečistog kod nije uspostavljena kroz kompajler (kao što je to slučaj sa Haskelom), već programer mora sam da zaključi o kakvom kodu se radi.
Rad sa datotekama
Osim interakcije sa korisnikom kroz konzolu, programi moraju učitavati i zapisivati datoteke. Iako postoji mnogo funkcija dostupnih Haskel programerima za interakciju sa datotekama, ovde ćemo spomenuti samo dve funkcije koje se koriste za čitanje i pisanje iz tekstualnih datoteka. To su funkcije readFile :: FilePath -> IO String i writeFile :: FilePath -> String -> IO () obe dostupne u Prelidu.
Tip FilePath koji se pojavljuje u tipu obe navedene funkcije je tipski sinonim za [Char] odnosno String. Definisanjem ovog tipa, kroz same tipove smo dokumentovali čemu služe parametri funkcije. Sada je u potpunosti jasno šta predstavljaju parametri funkcije writeFile: prvi je putanja do datoteke u koju treba zapisati nisku koja je prosleđena kao drugi argument. Sa druge strane, da nismo konstruisali novi tipski sinonim, iz tipa String -> String -> IO () ne bi bilo jasno koji parametar se odnosi na putanju a koji na sadržaj.
Dakle, funkcija writeFile :: FilePath -> String -> IO () uzima putunju do datoteke, nisku, i daje akciju koja kada se pokrene upisuje dati sadržaj u datoteku na navedenoj lokaciji. Funkcija readFile :: FilePath -> IO String je nešto jednostavnija. Ta funkcija uzima putanju do datoteke a vraća akciju koja kada se pokrene učitava sadržaj datoteke u nisku.
Da bismo demonstrirali primenu navedenih funkcija, kreirajmo datoteku pesma.txt i sačuvajmo je u istom direktorijumu u kom se nalazi i kôd programa.
Ono sve što znaš o meni To je stvarno tako malo U dvje riječi sve bi stalo Kada pričala bi ti
pesma.txtProgram koji učitava sadržaj pesme i ispisuje u terminal je sledeći
main :: IO () main = do pesma <- readFile "pesma.txt" putStrLn pesma
getLine daje korisnički unos, tako i pokretanje akcije readFile "pesma.txt" daje sadržaj datoteke. U oba slučaja moramo iskoristiti strelicu <- da bi pokrenuli akciju i rezultat smestili u novo ime.$ ghc main.hs [1 of 1] Compiling Main ( main.hs, main.o ) Linking main ... $ ./main Ono sve što znaš o meni To je stvarno tako malo U dvje riječi sve bi stalo Kada pričala bi ti
Da bi zapisali nisku u datoteku iskoristićemo funkciju writeFile. Na primer, pesmu koju smo učitali sa prethodnim programom, možemo zapisati u novu datoteku na sledeći način:
main :: IO () main = do pesma <- readFile "pesma.txt" writeFile "novaPesma.txt" pesma
pesma.txt će biti prekopiran u datoteku novaPesma.txt, a u terminalu neće biti prikazana bilo kakva poruka. Kao što vidimo, jedina svrha ovog programa je da kopira tekstualnu datoteku sa određenim imenom.Pri radu sa funkcijama readFile i writeFile trebalo bi imati na umu da ove funkcije mogu dovesti do izuzetka koji prekidaju rad programa ako se ne obrade. Na primer, ako pokušamo čitanje datoteke koja ne postoji, ili upisavanje u datoteku za koju nemamo odgovarajuće dozvole. Videćemo uskoro kako se izuzeci mogu kontrolisati.
Zadatak 3. Koristeći funkciju lines :: String -> [String] naći broj liniji u nekoj određenoj datoteci (npr pesma.txt).
Kratak osvrt na module
Pre nego što nastavimo sa akcijama, moramo napraviti kratku digresiju i upoznati se sa Haskel modulima. U Haskelu, modul je osnovna jedinica podele programa. Jedan modul sastoji se iz definicija funkcija, tipova, klasa, i drugih stvari sa kojima ćemo se upoznati... Ime modula mora početi velikim slovom i može sadržati samo alfanumeričke karaktere kao i znak tačke. Moduli omogućuju bolju organizaciju koda: kodu jednog modula nisu dostupne definicije iz drugog modula osim ako se eksplicitno ne uvedu u modul. Ovim je olakšano razdvajanje koda u smislene celine i organizacija koda. Sa druge strane, uvodeći module možemo iskoristiti funkcije ili akcije koje nismo sami napisali8.
Svaka Haskel datoteka čini jedan poseban modul. U dosadašnjem izlaganju, implicitno smo radili sa Main modulom9 koji se podrazumeva ako neki drugi modul nije naveden. Za sve datoteke koje smo napisali, podrazumevalo se da definišu Main modul. Takođe, još jedan modul nam je bi "sakriven pred očima": to je prelid, odnosno Prelude modul koji se implicitno učitava u svaki Haskel program.
Za nas će do kraja ove sekcije biti korisno da znamo kako možemo učitati određene funkcije iz standardnih modula. Da bismo uvezli neku funkciju funkcija iz modula NekiModul potrebno da na početku datoteke, pre svih drugih definicija napišemo liniju import NekiModul (funkcija). Na ovaj način uvedena funkcija je dostupna našem kodu kao da smo je sami napisali. Ako iz modula NekiModul želimo da uvezemo više funkcija, na primer funkc1 i funkc2, kôd ćemo započeti sa import NekiModul (funkc1, funkc2). Ako uvozimo funkcije iz različitih modula, tada poredak uvoza nije bitan - bitno je samo da se svi uvozi nalaze pre definicija i deklaracija.
Naravno, imena funkcija koje uvozimo iz drugih modula ne smeju se podudarati sa funkcijama koje smo sami definisali. Iako nam sistem modula omogućuje prevazilaženje ovog problema, za nas će za sada biti dovoljne samo informacije koje smo ovde dali.
Rad sa argumentima komande linije
Komandna linija, ili terminal, predstavlja interfejs u kom korisnik upisuje komande koje računar treba da izvrši. Specijalno, kroz komandnu liniju korisnik može da pokrene programe, što smo i videli u ovoj lekciji. U terminalima lokalni programi (poput programa koje smo sami iskompajlirali) pokreću se navođenjem putanje do tog programa. Prilikom pokretanja programa moguće je proslediti i argumente komandne linije koji su navode nakon imena programa. Nulti argument je putanja programa. Svi argumenti prosleđuju se pokrenutom programu i na tom programu je da interpretira značenje tih argumenata.
I u Unix i u Windows terminalima moguće je pokrenuti program rm10 koji briše datoteke. Ako u terminalu pokrenemo komandu rm pesma.txt, tada će terminal pokrenuti program rm i proslediće mu listu11 argumenata ["rm", "pesma.txt"].
Koristeći akcije getProgName :: IO String i getArgs :: IO [String] iz modula System.Environment možemo pristupiti argumentima komande linije. Akcija getProgName vraća nulti argument tj. ime programa, dok getArgs vraća sve ostale argumente. U najjednostavnijem slučaju možemo samo ispisati prosleđene argumente:
import System.Environment main :: IO () main = do imePrograma <- getProgName putStrLn imePrograma argumenti <- getArgs putStrLn (show argumenti)
argumenti.hs$ ghc argumenti.hs [1 of 2] Compiling Main ( argumenti.hs, argumenti.o ) $ ./argumenti argumenti [] $ ./argumenti a b C DDD 123 argumenti ["a","b","C","DDD","123"] $ ./argumenti argument1 "drugi argument" argumenti ["argument1","drugi argument"]
Sada kako znamo da pristupimo argumentima komande linije, možemo napraviti jednostavnije verzije nekih poznatih Unix alata:
Zadatak 4. Napisati program koji ispisuje sadržaj tekstualne datoteke čije ime je navedeno kao prvi argument komandne linije (ovo je takozvani cat program). Ako argument nije prisutan, program treba da ispiše poruku o grešci.
Akcije za učitavanje i ispis datoteke postavićemo u posebnu funkciju učitajIspiši. U akciji main proverićemo da li lista argumenata sadrži barem jedan element i na osnovu toga pokrenuti sledeću akciju:
import System.Environment učitajIspiši :: String -> IO () učitajIspiši imeDatoteke = do sadržaj <- readFile imeDatoteke putStrLn sadržaj main :: IO () main = do argumenti <- getArgs if null argumenti then putStrLn "Navedite barem jedan argument!" else učitajIspiši (head argumenti)
Zadatak 5. Napisati program koji ispisuje broj reči u tekstualnoj datoteci čije ime je navedeno kao prvi argument komandne linije (ovo je takozvani wc program). Ako argument nije prisutan, program treba da ispiše poruku o grešci.
Zadatak 6. Napisati program koji sadržaj datoteke čije ime je navedeno kao prvi argument kopira u datoteku čije ime je navedeno kao drugi argument komandne linije (ovo je takozvani cp program). Ako svi argumenti nisu prisutni, program treba da ispiše poruku o grešci.
Zadatak 7. Napisati program koji ispisuje prvih pet linija tekstualne datoteke čije ime je navedeno kao prvi argument komandne linije (ovo je takozvani head program). Ako argument nije prisutan, program treba da ispiše poruku o grešci. Ako je prisutan drugi argument, proveriti da li taj argument predstavlja prirodan broj, i u potvrdnom slučaju ispisati broj linija u skladu sa tim argumentom. Iskoristiti funkcije uBroj i daLiJeBroj iz lekcije o listama.