Tipovi

Tip predstavlja kolekciju vrednosti. Svaka vrednost u Haskel jeziku pripada jednom i samo jednom tipu. Činjenicu da neka vrednost v pripada tipu T označavamo sa v :: T. Izraz v :: T često čitamo kao "v poseduje tip T".

Haskel poseduje statički sistem tipova što znači da su tipovi svih izraza poznati prilikom kompilacije (ili interpretacije). Time se otklanjaju mnoge greške koje bi mogle zaustaviti program prilikom izvršavanja ili dovele do pogrešnog rezultata1. Jezici poput Python-a ili JavaScript-a nemaju statički sistem tipova, već dinamički sistem tipova. U takvim jezicima, tipovi izraza se određuju prilikom izvršavanja programa što ostavlja veliku mogućnost za pojavljivanje bagova.

Još jedna karakteristika koja odlikuje Haskelov sistem tipova je zaključivanje tipova. Dok je u jezicima poput C-a ili Jave (koji su statički tipizirani kao i Haskel) neophodno navesti tip svake promenljive ili funkcije, Haskel kompajler (skoro) uvek može zaključiti tip izraza bez potrebe da programer eksplicitno navede tip2. Po ovome se Haskel značajno razlikuje od većine ostalih jezika sa statičkim sistemom tipova.

Tipovi čine značajan deo Haskel jezika, i neophodno je dobro razumeti ovaj sistem. U nastavku ove lekcije upoznaćemo se sa nekim osnovnim tipovima u Haskelu, kao i operacijama i funkcijama koje se mogu koristiti sa ovim tipovima.

Tip Bool

Tip Bool predstavlja kolekciju koja sadrži dve logičke vrednosti: True i False3.

Šematski prikaz tipa Bool.

Nad logičkim vrednostima moguće je vršiti naredne logičke operatore (operacije):

  1. Konjunkcija (i) p && q dve logičke vrednosti p i q je logička vrednost koja ima vrednost True ako logičke vrednosti p i q imaju vrednost True.
  2. Disjunkcija (ili) p || q dve logičke vrednosti p i q je logička vrednost koja ima vrednost True ako barem jedna od vrednosti p i q ima vrednost True.

Koristeći navedene operatore i zagrade možemo kreirati logičke izraze proizvoljne složenosti.

ghci> True || False
True
ghci> ((True && False) || False) && (True || True)
False
Kao i aritmetičke izraze, GHCi će i logičke izraze pokušati da svede na jednu vrednost.

Uz navedene operatore koristimo i funkciju not koja negira logičku vrednost4. Ovde je dobro mesto da kažemo par reči o funkcijama u Haskelu, jer funkcije predstavljaju osnovni koncept ovog jezika.

Funkcije su vrednosti koje od vrednosti "prave" nove vrednosti. Na primer, imenom not označena je funkcija koja negira logičku vrednost koja joj je prosleđena, tj. od True "pravi" u False a od False "pravi" True. Da bismo "pozvali" funkciju not potrebno je da nakon imena funkcije napišemo logičku vrednost koju želimo da negiramo. Za razliku od drugih jezika, u Haskelu ime funkcije i argument razdvajamo razmakom, a ne zagradom:

ghci> not True
False
ghci> a = True && False
ghci> not a
True
Funkciju not možemo pozivati nad logičkim vrednostima ili nad imenima kojima su dodeljene logičke vrednosti (u ovom primeru to je ime a).

Funkcija not vraća novu logičku vrednost, te stoga tu vrednost možemo kombinovati ponovo u složenijim logičkim izrazima:

ghci> (not True) || False
False

Zagrade postavljamo oko izraza da bismo uspostavili poredak kojim je potrebno evaluirati izraz. U prethodnom primeru postavili smo zagrade oko logičkog izraza not True, da bismo bili sigurni da se celokupan izraz neće protumačiti kao not (True || False)5. Ali kao što u aritmetici imamo konvenciju o prioritetu računskih operacija, tako i u Haskelu imamo niz pravila koja se tiču prioriteta raznih operatora i poziva funkcija. Za sada je važno znati da primena funkcije na vrednost ima najveći prioritet. Prema tome, izraz not True || False se tumači kao (not True) || False, baš kao što se aritmetički izraz \(5\cdot 4 + 7\) tumači kao \((5\cdot 4) + 7\) a ne kao \(5 \cdot (4 + 7)\).

Logičke vrednosti možemo da poredimo sa operatorima == i /=, koji utvrđuju da li su vrednosti iste odnosno različite. Rezultati ovih poređenja su takođe logičke vrednosti:

ghci> p = True
ghci> q = False
ghci> p == p 
True
ghci> p == q
False
ghci> p /= q
True

Tipovi Int i Integer

Tip Int predstavlja kolekciju celobrojnih vrednosti koje se mogu predstaviti u procesorskoj reči, odnosno u 32 ili 64 bita6.

Nad vrednostima tipa Int moguće je vršiti naredne operacije i funkcije:

+ i *

Sa sabiranjem i množenjem brojeva smo se već upoznali u prethodnoj lekciji. Ovi operatori se ponašaju isto kao i svim drugim programskim jezicima ili u svakodnevnoj aritmetici.

-

Oduzimanje brojeva je takođe poznato iz svakodnevnog života i ostalih programskih jezika. Ali potrebno je naglasiti da se sa znakom - može negirati broj. Znak - ima nizak prioritet, te je često potrebno koristiti zagrade oko izraza koji se negira:

ghci> 3 * -2
<interactive>:1:1: error:
precedence parsing error
cannot mix ‘*’ [infixl 7] and prefix `-' [infixl 6] in the same infix expression
ghci> 3 * (-2)
-6
Izraz 3 * -2 dovodi do greške u parsiranju, te je potrebno staviti zagrade oko broja koji se negira.

div

Za deljenje celobrojnih vrednosti, koristi se funkcija div sa dva parametra. Prvi parametar je deljenik (broj koji se deli) a drugi je delilac (broj kojim se deli). Rezultat deljenja je celobrojna vrednost odnosno vrednost tipa Int.

I pri radu sa funkcijama više parametara, argumenti se razdvajaju razmacima7:

ghci> div 7 2
3
Rezultat je \(3\) jer je \(3\cdot 2 + 1 = 7\).

Ponovo napominjemo da primena funkcije na argument ima najveći prioritet. Stoga se kôd div 7 2 * 2 interpretira kao (div 7 2) * 2 a ne kao div 7 (2 * 2).

mod

Funkcija mod vraća ostatak pri celobrojnom deljenju prvog argumenta sa drugim.

ghci> mod 7 3
1

Za funkcije div i mod i proizvoljne cele brojeve x i y različite od nule važi veza \[\mathtt{(div\; x\; y)*y + (mod\; x \; y) = x}.\]

^

Operatorom ^ je moguće stepenovati celobrojnu vrednost na prirodan stepen. Na primer 7^2 se evaluira u 49.

Ako pokušamo da sa ^ stepenujemo broj na negativan stepen dobićemo izuzetak8:

ghci> 7^(-2)
*** Exception: Negative exponent
Rezultat dizanja prirodnog broja na negativan stepen pozitivan racionalan broj manji od 1, a takav broj se ne može predstaviti tipom Int

Izuzetak je posebna vrednost koja označava da je došlo do neke greške prilikom izvršavanja programa. Pojava izuzetka dovodi do prekida izvršavanja (interpretacije) programa. Mnogo više o izuzecima biće rečeno kasnije.

Poređenja

Vrednosti tipa Int takođe možemo porediti sa operatorima == i /=. Ali možemo koristiti i operatore <, <=, > i >= koji upoređuju brojeve po veličini. Rezultat svih navedenih operacija je logička vrednost:

ghci> 6 == 5
False
ghci> 6 == 3 * 2
True
ghci> 2 < 3
True
ghci> 2 >= 3
False

U Haskelu nije dozvoljeno porediti tipove različitog tipa sa operatorima ==, /=, <, <=, > i >=. U jezicima poput Javaskripta, poređenje poput True == 1 bi bilo validno (i tačno), dok u Haskelu takvo poređenje dovodi do greške:

ghci> True == 1
<interactive>:1:9: error:
    • No instance for (Num Bool) arising from the literal ‘1’
    • In the second argument of ‘(==)’, namely ‘1’
      In the expression: True == 1
      In an equation for ‘it’: it = True == 1

Tip Integer

Integer je takođe tip koji predstavlja celobrojne vrednosti. I nad ovim vrednostima ovog tipa mogu se koristiti funkcije +, -, *, div, mod, rem, ^, ==, /=, <, <=, > i >=. Razlika između tipova Int i Integer je u tome što su vrednosti tipa Integer neograničene9, dok su vrednosti tipa Int ograničene dužinom procesorske reči.

Da bi demonstrirali razliku možemo napraviti mali eksperiment. U jednoj .hs datoteci definišimo jednu Int i jednu Integer konstantu:

a :: Int
a = 7

b :: Integer
b = 7
Obe vrednosti predstavljaju broj \(7\), ali poseduju različit tip.

Nakon učitavanja gorenavedenog koda, možemo stepenovati definisane vrednosti na velike stepene i tako prekoračiti ograničenje koje nameće Int tip. Uvidećemo da su rezultati različiti:

ghci> a^70
254007274765394321
ghci> b^70
143503601609868434285603076356671071740077383739246066639249
Broj \(254007274765394321\) je velik ali nije jednak \(7^{70}\). Evaluacija koda b^70 daje tačnu vrednost.

Pri radu sa tipom Integer trebalo bi imati na umu da su aritmetičke operacije nad ovim tipom značajno sporije nego nad Int tipom. Razlog tome je što aritmetičke operacije sa vrednostima tipa Int odgovaraju procesorskim instrukicjama, što nije slučaj i sa vrednostima tipa Integer.

Tipovi Float i Double

Za reprezentovanje realnih brojeva koriste se tipovi Float i Double koji realan broj predstavljaju u skladu sa IEEE 754 standardom. Jedina razlika između ova dva tipa je u tome što Float koristi jednostruku tačnost (32 bita) a Double dvostruku tačnost (64 bita). Kako moderne arhitekture računara podjednako dobro podržavaju računske operacije u jednostrukoj i dvostrukoj tačnosti, preporučeno je da se uvek koristi Double.

Sa tipovima Float i Double moguće je koristiti operatore +, -, *, ^, ==, /=, <, <=, > i >=. Za deljenje realnih brojeva koristi se operator /. Uz operator ^, za stepenovanje se može koristiti i operator ** koji dozvoljava da stepen bude proizvoljan realan broj10

Takođe, Haskel ima ugrađene funkcije poput sin, cos, tan, asin, acos, atan, exp, log, sqrt itd... Dostupna je takođe i Arhimedova konstanta \(\pi\) pod imenom pi.

ghci> r = 10
ghci> pi * r**2
314.1592653589793

Liste

U Haskelu liste su strukture podataka koje predstavljaju uređenu kolekciju elemenata nekog tipa. U jednoj listi se ne mogu naći dve vrednosti različitog tipa.11

U kodu, lista se konstruiše navođenjem vrednosti između uglastih zagrada, međusobno razdvojenih zarezima. Na primer, lista od prva četiri prirodna broja može da se definiše kao [0,1,2,3]. Razmaci oko elemenata i zagrada nisu od bilo kakvog značaja.12 Prazna lista se zapisuje kao [].

Tip [Bool] sadrži beskonačno mnogo vrednosti, čak i ako tip Bool sadrži samo dve.

Za označavanje tipa liste koristi se specijalna sintaksa. Ako je data lista elemenata tipa A tada ta lista ima tip koji se označava sa [A]13. Na primer, lista elemenata tipa Int poseduje tip [Int].

maliProstiBrojevi :: [Int]
maliProstiBrojevi = [2,3,5,7,11,13,17,19]

logičkeVrednosti :: [Bool]
logičkeVrednosti = [True,False]

važneKonstante :: [Double]
važneKonstante = [3.14159,2.71828,0.57721]
Definicije tri liste različitog tipa.

U specijalnom slučaju, možemo definisati i listu lista, i listu lista lista, itd...

a :: [[Int]]
a = [[1], [2,3], [4,5,6]]

b :: [[[Int]]]
b = [a, [[100], [200]], []]
Lista a je lista lista celih brojeva. Tip ove liste je [[Int]]. Primetimo, da se u listi tipa [[Int]] ne može naći element 5 (jer je to vrednost tipa [Int]), ali može element [5] koji je tipa [5].
Zadatak 1. Koliko elemenata ima lista [[]]?

Lista [[]] ima jedan element. Taj element je prazna lista [].

Liste možemo spajati uz pomoć operatora ++. Nadovezivanjem prazne liste [] na početak ili kraj liste, ta lista se ne menja. Naravno, moguće je spajati samo liste istog tipa.

ghci> [1,2,3,4] ++ [10,11,12]
[1,2,3,4,10,11,12]
ghci> [True,False] ++ []
[True,False]
ghci> [1,2,3] ++ [True]
<interactive>:1:2: error:
    • No instance for (Num Bool) arising from the literal ‘1’
    • In the expression: 1
      In the first argument of ‘(++)’, namely ‘[1,2,3]’
      In the expression: [1,2,3] ++ [True]
Prilikom spajanja lista različitog tipa dolazi do tipske greške.

Za nadovezivanje elemenata na početak liste može se koristi i operator :. Sa leve strane operatora : potrebno je navesti vrednost tipa A, a sa desne strane je potrebno navesti listu elemenata tipa [A]

ghci> 1 : [2,3,4,5]
[1,2,3,4,5]
ghci> [1] ++ [2,3,4,5]
[1,2,3,4,5]
Dva načina nadovezivanja jednog elementa na početak liste.
ghci> [1] : [2,3,4,5]
<interactive>:1:1: error:
    • Non type-variable argument in the constraint: Num [a]
      (Use FlexibleContexts to permit this)
    • When checking the inferred type
        it :: forall a. (Num a, Num [a]) => [[a]]

ghci> 1 ++ [2,3,4,5]
<interactive>:2:1: error:
    • Non type-variable argument in the constraint: Num [a]
      (Use FlexibleContexts to permit this)
    • When checking the inferred type
        it :: forall a. (Num a, Num [a]) => [a]
Česta greška je da se element nadovezuje na početak sa operatorom ++ ili da se liste spajaju pomoću operatora :.

Liste predstavljaju strukturu podataka koja se najčešće koristi u Haskelu. Zbog toga postoje mnoge funkcije koje uzimaju ili vraćaju liste. Sa mnogim takvim funkcijama upoznaćemo se u lekciji Liste. Za sada navodimo samo naredne funkcije:

  1. length vraća dužinu liste.
  2. head vraća prvi element liste ako taj element postoji. Pozivanjem ove funkcije nad praznom listom dolazi do izuzetka.
  3. last vraća poslednji element liste ako taj element postoji. Pozivanjem ove funkcije nad praznom listom dolazi do izuzetka.
  4. init vraća sve elemente liste osim poslednjeg. Pozivanjem ove funkcije nad praznom listom dolazi do izuzetka.
  5. tail vraća sve elemente liste osim prvog. Pozivanjem ove funkcije nad praznom listom dolazi do izuzetka.
  6. reverse vraća prosleđenu listu u obrnutom smeru.
  7. elem je funkcija od dva parametara koja proverava da li se prvi argument nalazi u listi prosleđenoj kao drugi argument. Rezultat provere je logička vrednost.
  8. null proverava da li je lista prazna. Rezultat provere je logička vrednost.
ghci> mojaLista = [10,20,30,40,50,60]
ghci> length mojaLista
6
ghci> head mojaLista
10
ghci> last mojaLista
60
ghci> init mojaLista
[10,20,30,40,50]
ghci> tail mojaLista
[20,30,40,50,60]
ghci> reverse mojaLista
[60,50,40,30,20,10]
ghci> elem 10 mojaLista
True
ghci> elem 100 mojaLista
False
ghci> null
False
ghci> null []
True

Sa operatorom !! možemo pristupiti \(m\)-tom elementu liste. Sa leve strane operatora se navodi lista, a sa desne strane vrednost tipa Int. Elementi su indeksirani od nule. Pokušaj pristupanja indeksu koji je veći od dužine liste dovodi do izuzetka.

ghci> [True, False, True, False, True] !! 2
True
ghci> [True, False, True, False, True] !! 7
*** Exception: Prelude.!!: index too large

Kad god imamo tip čije se vrednosti mogu upoređivati sa ==, /=, <, <=, > i >= tada se i liste elemenata tog tipa mogu porediti sa istim operatorima. Ova poređenja se vrše član po član, a u slučaju da je jedna od lista kraća od druge, tada se ta lista smatra strogo manjom (tj. manjom u smislu operatora <). Specijalno, liste se su jednake ako i samo ako sadrže iste vrednosti poređane istim redosledom.

ghci> [1,2,3] == [1,2,3]
True
ghci> [1,2,3] == [2,3,1]
False
ghci> [1, 2] < [1, 3]
True
Liste se porede član po član. Pošto su prvi elementi obe liste jednaki, poređenje prelazi na druge elemente u listi. Kako je 2 < 3 zaključujemo da je leva lista strogo manja.
ghci> [1, 2] < [1, 2, 3]
True
Pri ovom poređenju, prvo su upoređeni prvi elementi obe liste. Kako su oni jednaki, upoređeni su drugi elementi druge liste, pa je poređenje nastavljeno po trećim elementima. Međutim, jedna od lista nema treći element, te se ona smatra strogo manjom.
ghci> [1, 2, 3, 100] < [1, 2, 4, 5]
True
I pri ovom poređenju, prvo su upoređeni prvi elementi a zatim i drugi elementi. Kako su i drugi elementi jednaki, poređenje je nastavljeno po trećim elementima. Kako je 3 < 4, leva lista se smatra manjom. Primetimo da je leva lista manja od desne čak iako je četvrti član leve liste značajno veći od četvrtog člana desne liste. Dakle, prilikom poređenja, razmatra se samo prvo mesto na kom se liste razlikuju, dok naredne vrednosti ne utiču na poredak.
ghci>[] < [1]
True
Prazna lista je strogo manja od bilo koje druge liste.

Karakteri i niske

Vrednosti tipa Char su karakteri kodirani Unicode standardom. U samom kodu, karakteri se navode između jednostrukih navodnika:

ghci> mojKarakter = 'ш'
ghci> mojKarakter
'ш'

Funkcija fromEnum konvertuje karakter u vrednost tipa Int koja predstavlja poziciju tog karaktera u Unicode kodnoj tabeli

ghci> fromEnum 'A'
65
Nekome ko ne poznaje Unicode ili ASCII može delovati neobično da se 'A' nalazi tek na 65 mestu. Na prvim mestima se nalaze specijalni karakteri, zatim cifre, zatim velika pa mala latinična slova, nakon toga slede interpunkcijski znaci, pa tek onda nelatinična pisma...

Karaktere, kao i brojeve, možemo porediti. To poređenje je takođe ustanovljeno pozicijom u Unicode kodnoj tabeli. Stoga imamo

ghci> 'a' < 'b'
True
ghci> 'a' < 'B'
False
ghci> 'a' < 'ш'
True

Terminom niske nazivamo liste karaktera. Niske se mogu koristiti za prezentovanje tekstualnih vrednosti14. Na primer, pozdrav Hello! se u Haskelu predstavlja kao lista ['H', 'e', 'l', 'l', 'o', '!']. Srećom, niske se mogu zapisati i prostim navođenjem karaktera između dvostrukih navodnika potpuno analogno sintaksi u ostalim programskim jezicima. Tako se niska Hello! može konstruisati i kao literal15 "Hello!":

ghci> "Hello" == ['H', 'e', 'l', 'l', 'o', '!']
True
Poređenjem uviđamo da dva navedena načina konstrukcije niski daju istu vrednost.

Kako su niske u suštini liste, sve funkcije nad listama možemo iskoristiti i za niske.

ghci> niska = "Hello" ++ "!"
ghci> niska
"Hello!"
ghci> length niska
6
ghci> head niska
'H'
ghci> last niska
'!'
ghci> init niska
"Hello"
ghci> tail niska
"ello!"
ghci> reverse niska
"!olleH"
ghci> null ""
True
Obratite pažnju na razliku između niske i pojedinačnog karaktera. Na primer funkcija head vraća prvi element liste, a u ovom primeru to je karakter 'a' (a ne niska "a"). Karakteri se uvek navode između jednostrukih navodnika, a niske između dvostrukih.

Mnogi tipovi u Haskelu (ali svakako ne svi), dozvoljavaju da se nad njima pozove funkcija show. Ova funkcija uzima vrednost i vraća nisku koja prezentuje tu vrednost. Na primer:

ghci> show 2
"2"
ghci> show True
"True"
Niska "2" i vrednost 2 nisu isto, kao što ni logička vrednost True i niska "True" nisu isto, itd...
ghci> show [1, 2, 3]
"[1,2,3]"
Ako je neke vrednosti moguće "prikazati" sa show funkcijom, tada je i listu tih vrednosti moguće "prikazati" sa show funkcijom. Lista će uvek biti prikazana na kanonski način (bez razmaka između elemenata), bez obzira kako smo tu listu konstruisali u kodu.

Uređene \(n\)-torke

Liste sadrže proizvoljan broj elemenata istog tipa. Suprotno tome, uređene \(n\)-torke sadrže fiksiran broj elemenata ne nužnog istog tipa. Uređene \(n\)-torke se konstruišu navođenjem vrednosti odvojenih zarezima između zagrada ( ). Tip uređene \(n\)-torke je uređena \(n\)-torka odgovarajućih tipova.

Primer 1.

Vrednost ("Pozdrav!", True, 100.0) je jedna uređena trojka koja sadrži nisku, logičku vrednost i Float. Ova uređena trojka ima tip ([Char], Bool, Float).

Vrednosti tipa (A, B) možemo elegantno predstaviti u pravugaonom rasporedu u kom vrste odgovaraju tipu A a kolone tipu B. U ovom slučaju broj kolona (a i vrsta) je izuzetno veliki, pa ne možemo predstaviti sve vrednosti na ilustraciji.

Najznačajnije uređene \(n\)-torke su svakako uređene dvojke koje češće nazivamo uređeni parovi. Nad svakim uređenim parom možemo pozvati funkciju fst i snd koje redom vraćaju prvu i drugu koordinatu uređenog para.

ghci> fst (5, 'C')
5
ghci> snd (5, 'C')
'C'
Funkcije fst i snd se mogu koristiti za "izvlačenje" prve i druge koordinate uređenog para.

Uređene trojke, četvorke, itd. se ređe koriste, te za njih ne postoje funkcije poput fst i snd. U narednim sekcijama ćemo videti kako možemo sami konstruisati odgovarajuće funkcije.

Poredak tipova u tipu uređene \(n\)-torke je važan: tip (Int, Char) nije isti kao tip (Char, Int). Takođe broj pojavljivanja nekog tipa uređenoj \(n\)-torci je bitan: tip (Int, Int, Char) nije isti kao tip (Int, Char).

Zadatak 2. Koliko vrednosti sadrži tip (Bool, Bool)?

Navedeni tip sadrži četiri vrednosti: (True, True), (True, False), (False, True) i (False, False).

Zadatak 3. Ako tipovi A₁, A₂ ... Aₙ sadrže redom \(k_1\), \(k_2\) ... \(k_n\) vrednosti, koliko vrednosti sadrži tip (A₁, A₂, … , Aₙ)?