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 srušiti ceo program prilikom izvršavanja1. 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 ostalih jezika sa statičkim sistemom tipova. Statički tipizirani jezici poput Jave i C++-a zahtevaju od programera da eksplicitno navede tip (većine) vrednosti.

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.

Nad logičkim vrednostima moguće je vršiti naredne logičke 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 ove dve 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 binarne 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 vednosti koje "tranformišu" neke druge vrednosti, odnosno od nekih vrednosti prave nove vrednosti. Na primer, sa imenom not je označena funkcija koja negira logičku vrednost koja joj je prosleđena, tj. True "pretvara" u False a False u True. Da bi smo "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 se aritmetički izraz \(5\cdot 4 + 7\) tumači kao \((5\cdot 4) + 7\) a ne kao \(5 \cdot (4 + 7)\).

Osim logičkih operatora, 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:
recedence 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 od dve promenljive. 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 promenljivih argumenti se razdvaju 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 dva argumenta mod vraća ostatak pri celobrojnom deljenju prvog argumenta sa drugim.

ghci> mod 7 3
1

Za funkcije div i rem 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 će se evaluirati 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

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 == 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 se aritmetičke operacije sa Int odbijaju na nivou hardvera, što nije slučaj i sa tipom Integer.

Tipovi Float i Double

Za reprezentovanje realnih brojeva koriste se tipovi Float i Double koji realan broj prezentuju 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 razdvojih 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]

logickeVrednosti :: [Bool]
logickeVrednosti = [True,False]

vazneKonstante :: [Double]
vazneKonstante = [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]]
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 prosleđene liste.
  2. head vraća prvi element prosleđene liste ako taj element postoji. Pozivanjem ove funkcije nad praznom listom dolazi do izuzetka.
  3. last vraća poslednji element prosleđene liste ako taj element postoji. Pozivanjem ove funkcije nad praznom listom dolazi do izuzetka.
  4. init vraća sve elemente prosleđene liste osim poslednjeg. Pozivanjem ove funkcije nad praznom listom dolazi do izuzetka.
  5. tail vraća sve elemente prosleđene 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.
ghci> mojaLista = [10,20,30,40,50,60]
ghci> length a
6
ghci> head a
10
ghci> last a
60
ghci> init a
[10,20,30,40,50]
ghci> tail a
[20,30,40,50,60]
ghci> reverse a
[60,50,40,30,20,10]
ghci> elem 10 a
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. 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

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
Za nekoga ko ne poznaje Unicode možda deluje 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 isto sintaksi u ostalim programskim jezicima. Tako se niska Hello! može konstruisati i kao "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 a
6
ghci> head a
'H'
ghci> last a
'!'
ghci> init a
"Hello"
ghci> tail a
"ello!"
ghci> reverse a
"!olleH"
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"
ghci> show [1, 2, 3]
"[1,2,3]"
Obratite pažnju da niska "2" nije isto što i vrednost 2. I slično za ostale navedene primere. Ako je neke vrednosti moguće "prikazati" sa show funkcijom, tada je i listu takvih 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 A1, A2, A3... sadrže redom \(n_1\), \(n_2\), \(n_3\)... vrednosti, koliko vrednosti sadrži tip (A1, A2, ...., An)?

Zaključivanje tipova

GHC kompajler (skoro) uvek može da zaključi tip izraza. Zbog toga često nije neophodno navoditi tipske dekleracije u izvornim datotekama.15

Zaključivanje tipova nam takođe omogućava da u GHCi okruženju proverimo tipove izraza. To možemo uraditi pomoću naredbe :type nakon koje navodimo izraz čiji tip nas interesuje:

ghci> :type [True, False]
[True, False] :: [Bool]
ghci> :type [True, False] !! 1
[True, False] !! 1 :: Bool
ghci> :type length [True, False]
length [True, False] :: Int
ghci> :type (True, 'a')
(Bool, Char) 
Primetimo da se :type komanda odnosi na ceo ostatak linije. Stoga nije neophodno pisati :type ([True] !! 0), već samo :type [True] !! 0, itd...

Zaključivanje tipova je složen proces, ali možemo demonstrirati osnove ovog procesa. Pri zaključivanju tipa izraza [True, False], kompajler prvo uviđa da se radi o listi. Prema tome, prvo se zaključuje da je tip ovog izraza [a] za neki neodređeni tip a. Zatim, kompajler uvidom u prvi element ove liste zaključuje da je tip a baš Bool, pa je samim tim tip celokunog izraza [Bool]. Da bismo razumeli proces zaključivanja tipa izraza length [True, False], neophodno je da razumemo pojam tipa funkcije što će biti prikazano u narednoj lekciji.

Rezultat :type naredbe će nas iznenaditi u slučaju numeričkih konstanti:

ghci> :type 2
2 :: Num a => a

Za izraz 2 GHCi nije mogao da zaključi konkretan tip. Izraz 2 može predstavljati vrednost tipa Int, Integer, Float ili Double. Međutim svi ovi tipovi, pripadaju tipskoj klasi Num. Zbog toga GHCi zaključuje da je 2 nekog tipa a koji pripada klasi Num. Takav tip se označava sa Num a => a. Deo tipa koji se nalazi pre znaka => naziva se klasno ograničenje.

Ako potražimo tip izraza 3.14 uvidećemo da je u pitanju tip Frac a => a. Svakako izraz 3.14 predstavlja jedan racionalan broj, te ne može biti tipa Int ili Integer. Ali racionalan broj može biti predstavljen i kao Float i kao Double vrednost. Zbog toga ni ovde nemamo konkretan odgovor već klasno ograničenje.